Skip to content

Commit 106828c

Browse files
committed
Dynamic contexts
1 parent b609117 commit 106828c

File tree

7 files changed

+116
-40
lines changed

7 files changed

+116
-40
lines changed

coverage/collector.py

+26-37
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,6 @@
3434
CTracer = None
3535

3636

37-
def should_start_context(frame):
38-
"""Who-Tests-What hack: Determine whether this frame begins a new who-context."""
39-
fn_name = frame.f_code.co_name
40-
if fn_name.startswith("test"):
41-
return fn_name
42-
return None
43-
44-
4537
class Collector(object):
4638
"""Collects trace data.
4739
@@ -66,7 +58,10 @@ class Collector(object):
6658
# The concurrency settings we support here.
6759
SUPPORTED_CONCURRENCIES = set(["greenlet", "eventlet", "gevent", "thread"])
6860

69-
def __init__(self, should_trace, check_include, timid, branch, warn, concurrency):
61+
def __init__(
62+
self, should_trace, check_include, should_start_context,
63+
timid, branch, warn, concurrency,
64+
):
7065
"""Create a collector.
7166
7267
`should_trace` is a function, taking a file name and a frame, and
@@ -75,6 +70,11 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency
7570
`check_include` is a function taking a file name and a frame. It returns
7671
a boolean: True if the file should be traced, False if not.
7772
73+
`should_start_context` is a function taking a frame, and returning a
74+
string. If the frame should be the start of a new context, the string
75+
is the new context. If the frame should not be the start of a new
76+
context, return None.
77+
7878
If `timid` is true, then a slower simpler trace function will be
7979
used. This is important for some environments where manipulation of
8080
tracing functions make the faster more sophisticated trace function not
@@ -96,6 +96,7 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency
9696
"""
9797
self.should_trace = should_trace
9898
self.check_include = check_include
99+
self.should_start_context = should_start_context
99100
self.warn = warn
100101
self.branch = branch
101102
self.threading = None
@@ -139,10 +140,6 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency
139140
)
140141
)
141142

142-
# Who-Tests-What is just a hack at the moment, so turn it on with an
143-
# environment variable.
144-
self.wtw = int(os.getenv('COVERAGE_WTW', 0))
145-
146143
self.reset()
147144

148145
if timid:
@@ -175,7 +172,11 @@ def tracer_name(self):
175172

176173
def _clear_data(self):
177174
"""Clear out existing data, but stay ready for more collection."""
178-
self.data.clear()
175+
# We used to used self.data.clear(), but that would remove filename
176+
# keys and data values that were still in use higher up the stack
177+
# when we are called as part of switch_context.
178+
for d in self.data.values():
179+
d.clear()
179180

180181
for tracer in self.tracers:
181182
tracer.reset_activity()
@@ -187,10 +188,6 @@ def reset(self):
187188
# pairs as keys (if branch coverage).
188189
self.data = {}
189190

190-
# A dict mapping contexts to data dictionaries.
191-
self.contexts = {}
192-
self.contexts[None] = self.data
193-
194191
# A dictionary mapping file names to file tracer plugin names that will
195192
# handle them.
196193
self.file_tracers = {}
@@ -252,11 +249,13 @@ def _start_tracer(self):
252249
tracer.threading = self.threading
253250
if hasattr(tracer, 'check_include'):
254251
tracer.check_include = self.check_include
255-
if self.wtw:
256-
if hasattr(tracer, 'should_start_context'):
257-
tracer.should_start_context = should_start_context
258-
if hasattr(tracer, 'switch_context'):
259-
tracer.switch_context = self.switch_context
252+
if hasattr(tracer, 'should_start_context'):
253+
tracer.should_start_context = self.should_start_context
254+
tracer.switch_context = self.switch_context
255+
elif self.should_start_context:
256+
raise CoverageException(
257+
"Can't support dynamic contexts with {}".format(self.tracer_name())
258+
)
260259

261260
fn = tracer.start()
262261
self.tracers.append(tracer)
@@ -372,12 +371,9 @@ def _activity(self):
372371
return any(tracer.activity() for tracer in self.tracers)
373372

374373
def switch_context(self, new_context):
375-
"""Who-Tests-What hack: switch to a new who-context."""
376-
# Make a new data dict, or find the existing one, and switch all the
377-
# tracers to use it.
378-
data = self.contexts.setdefault(new_context, {})
379-
for tracer in self.tracers:
380-
tracer.data = data
374+
"""Switch to a new dynamic context."""
375+
self.flush_data()
376+
self.covdata.set_context(new_context)
381377

382378
def cached_abs_file(self, filename):
383379
"""A locally cached version of `abs_file`."""
@@ -415,20 +411,13 @@ def abs_file_dict(d):
415411
else:
416412
raise runtime_err # pylint: disable=raising-bad-type
417413

418-
return dict((self.cached_abs_file(k), v) for k, v in items)
414+
return dict((self.cached_abs_file(k), v) for k, v in items if v)
419415

420416
if self.branch:
421417
self.covdata.add_arcs(abs_file_dict(self.data))
422418
else:
423419
self.covdata.add_lines(abs_file_dict(self.data))
424420
self.covdata.add_file_tracers(abs_file_dict(self.file_tracers))
425421

426-
if self.wtw:
427-
# Just a hack, so just hack it.
428-
import pprint
429-
out_file = "coverage_wtw_{:06}.py".format(os.getpid())
430-
with open(out_file, "w") as wtw_out:
431-
pprint.pprint(self.contexts, wtw_out)
432-
433422
self._clear_data()
434423
return True

coverage/config.py

+2
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ def __init__(self):
180180
self.data_file = ".coverage"
181181
self.debug = []
182182
self.disable_warnings = []
183+
self.dynamic_context = None
183184
self.note = None
184185
self.parallel = False
185186
self.plugins = []
@@ -324,6 +325,7 @@ def from_file(self, filename, our_file):
324325
('data_file', 'run:data_file'),
325326
('debug', 'run:debug', 'list'),
326327
('disable_warnings', 'run:disable_warnings', 'list'),
328+
('dynamic_context', 'run:dynamic_context'),
327329
('note', 'run:note'),
328330
('parallel', 'run:parallel', 'boolean'),
329331
('plugins', 'run:plugins', 'list'),

coverage/control.py

+20
Original file line numberDiff line numberDiff line change
@@ -347,9 +347,19 @@ def _init_for_start(self):
347347
# it for the main process.
348348
self.config.parallel = True
349349

350+
if self.config.dynamic_context is None:
351+
should_start_context = None
352+
elif self.config.dynamic_context == "test_function":
353+
should_start_context = should_start_context_test_function
354+
else:
355+
raise CoverageException(
356+
"Don't understand dynamic_context setting: {!r}".format(self.config.dynamic_context)
357+
)
358+
350359
self._collector = Collector(
351360
should_trace=self._should_trace,
352361
check_include=self._check_include_omit_etc,
362+
should_start_context=should_start_context,
353363
timid=self.config.timid,
354364
branch=self.config.branch,
355365
warn=self._warn,
@@ -886,6 +896,16 @@ def plugin_info(plugins):
886896
Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage)
887897

888898

899+
def should_start_context_test_function(frame):
900+
"""Who-Tests-What hack: Determine whether this frame begins a new who-context."""
901+
with open("/tmp/ssc.txt", "a") as f:
902+
f.write("hello\n")
903+
fn_name = frame.f_code.co_name
904+
if fn_name.startswith("test"):
905+
return fn_name
906+
return None
907+
908+
889909
def process_startup():
890910
"""Call this at Python start-up to perhaps measure coverage.
891911

coverage/ctracer/tracer.c

+3-2
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,6 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame)
341341
CFileDisposition * pdisp = NULL;
342342

343343
STATS( self->stats.calls++; )
344-
self->activity = TRUE;
345344

346345
/* Grow the stack. */
347346
if (CTracer_set_pdata_stack(self) < 0) {
@@ -353,7 +352,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame)
353352
self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth];
354353

355354
/* See if this frame begins a new context. */
356-
if (self->should_start_context && self->context == Py_None) {
355+
if (self->should_start_context != Py_None && self->context == Py_None) {
357356
PyObject * context;
358357
/* We're looking for our context, ask should_start_context if this is the start. */
359358
STATS( self->stats.start_context_calls++; )
@@ -866,6 +865,8 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse
866865
goto error;
867866
}
868867

868+
self->activity = TRUE;
869+
869870
switch (what) {
870871
case PyTrace_CALL:
871872
if (CTracer_handle_call(self, frame) < 0) {

coverage/ctracer/tracer.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ typedef struct CTracer {
2727
PyObject * trace_arcs;
2828
PyObject * should_start_context;
2929
PyObject * switch_context;
30-
PyObject * context;
3130

3231
/* Has the tracer been started? */
3332
BOOL started;
3433
/* Are we tracing arcs, or just lines? */
3534
BOOL tracing_arcs;
3635
/* Have we had any activity? */
3736
BOOL activity;
37+
/* The current dynamic context. */
38+
PyObject * context;
3839

3940
/*
4041
The data stack is a stack of dictionaries. Each dictionary collects

coverage/sqldata.py

+2
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ def _context_id(self, context):
199199

200200
def set_context(self, context):
201201
"""Set the current context for future `add_lines` etc."""
202+
if self._debug and self._debug.should('dataop'):
203+
self._debug.write("Setting context: %r" % (context,))
202204
self._start_using()
203205
context = context or ""
204206
with self._connect() as con:

tests/test_context.py

+61
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
import os.path
77

88
import coverage
9+
from coverage import env
910
from coverage.data import CoverageData
11+
from coverage.misc import CoverageException
1012

1113
from tests.coveragetest import CoverageTest
1214

@@ -102,3 +104,62 @@ def test_combining_arc_contexts(self):
102104
self.assertEqual(combined.arcs(fred, context='blue'), [])
103105
self.assertEqual(combined.arcs(fblue, context='red'), [])
104106
self.assertEqual(combined.arcs(fblue, context='blue'), self.ARCS)
107+
108+
109+
class DynamicContextTest(CoverageTest):
110+
"""Tests of dynamically changing contexts."""
111+
112+
def setUp(self):
113+
super(DynamicContextTest, self).setUp()
114+
self.skip_unless_data_storage_is("sql")
115+
if not env.C_TRACER:
116+
self.skipTest("Only the C tracer supports dynamic contexts")
117+
118+
def test_simple(self):
119+
self.make_file("two_tests.py", """\
120+
def helper(lineno):
121+
x = 2
122+
123+
def test_one():
124+
a = 5
125+
helper(6)
126+
127+
def test_two():
128+
a = 9
129+
b = 10
130+
if a > 11:
131+
b = 12
132+
assert a == (13-4)
133+
assert b == (14-4)
134+
helper(15)
135+
136+
test_one()
137+
x = 18
138+
helper(19)
139+
test_two()
140+
""")
141+
cov = coverage.Coverage(source=["."])
142+
cov.set_option("run:dynamic_context", "test_function")
143+
self.start_import_stop(cov, "two_tests")
144+
data = cov.get_data()
145+
146+
fname = os.path.abspath("two_tests.py")
147+
self.assertCountEqual(data.measured_contexts(), ["", "test_one", "test_two"])
148+
self.assertCountEqual(data.lines(fname, ""), [1, 4, 8, 17, 18, 19, 2, 20])
149+
self.assertCountEqual(data.lines(fname, "test_one"), [5, 6, 2])
150+
self.assertCountEqual(data.lines(fname, "test_two"), [9, 10, 11, 13, 14, 15, 2])
151+
152+
153+
class DynamicContextWithPythonTracerTest(CoverageTest):
154+
"""The Python tracer doesn't do dynamic contexts at all."""
155+
156+
run_in_temp_dir = False
157+
158+
def test_python_tracer_fails_properly(self):
159+
if env.C_TRACER:
160+
self.skipTest("This test is specifically about the Python tracer.")
161+
cov = coverage.Coverage()
162+
cov.set_option("run:dynamic_context", "test_function")
163+
msg = r"Can't support dynamic contexts with PyTracer"
164+
with self.assertRaisesRegex(CoverageException, msg):
165+
cov.start()

0 commit comments

Comments
 (0)