diff --git a/coverage/collector.py b/coverage/collector.py index e01449798..686d4a7e5 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -34,14 +34,6 @@ CTracer = None -def should_start_context(frame): - """Who-Tests-What hack: Determine whether this frame begins a new who-context.""" - fn_name = frame.f_code.co_name - if fn_name.startswith("test"): - return fn_name - return None - - class Collector(object): """Collects trace data. @@ -66,7 +58,10 @@ class Collector(object): # The concurrency settings we support here. SUPPORTED_CONCURRENCIES = set(["greenlet", "eventlet", "gevent", "thread"]) - def __init__(self, should_trace, check_include, timid, branch, warn, concurrency): + def __init__( + self, should_trace, check_include, should_start_context, + timid, branch, warn, concurrency, + ): """Create a collector. `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 `check_include` is a function taking a file name and a frame. It returns a boolean: True if the file should be traced, False if not. + `should_start_context` is a function taking a frame, and returning a + string. If the frame should be the start of a new context, the string + is the new context. If the frame should not be the start of a new + context, return None. + If `timid` is true, then a slower simpler trace function will be used. This is important for some environments where manipulation of 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 """ self.should_trace = should_trace self.check_include = check_include + self.should_start_context = should_start_context self.warn = warn self.branch = branch self.threading = None @@ -139,10 +140,6 @@ def __init__(self, should_trace, check_include, timid, branch, warn, concurrency ) ) - # Who-Tests-What is just a hack at the moment, so turn it on with an - # environment variable. - self.wtw = int(os.getenv('COVERAGE_WTW', 0)) - self.reset() if timid: @@ -175,7 +172,11 @@ def tracer_name(self): def _clear_data(self): """Clear out existing data, but stay ready for more collection.""" - self.data.clear() + # We used to used self.data.clear(), but that would remove filename + # keys and data values that were still in use higher up the stack + # when we are called as part of switch_context. + for d in self.data.values(): + d.clear() for tracer in self.tracers: tracer.reset_activity() @@ -187,10 +188,6 @@ def reset(self): # pairs as keys (if branch coverage). self.data = {} - # A dict mapping contexts to data dictionaries. - self.contexts = {} - self.contexts[None] = self.data - # A dictionary mapping file names to file tracer plugin names that will # handle them. self.file_tracers = {} @@ -252,11 +249,13 @@ def _start_tracer(self): tracer.threading = self.threading if hasattr(tracer, 'check_include'): tracer.check_include = self.check_include - if self.wtw: - if hasattr(tracer, 'should_start_context'): - tracer.should_start_context = should_start_context - if hasattr(tracer, 'switch_context'): - tracer.switch_context = self.switch_context + if hasattr(tracer, 'should_start_context'): + tracer.should_start_context = self.should_start_context + tracer.switch_context = self.switch_context + elif self.should_start_context: + raise CoverageException( + "Can't support dynamic contexts with {}".format(self.tracer_name()) + ) fn = tracer.start() self.tracers.append(tracer) @@ -372,12 +371,9 @@ def _activity(self): return any(tracer.activity() for tracer in self.tracers) def switch_context(self, new_context): - """Who-Tests-What hack: switch to a new who-context.""" - # Make a new data dict, or find the existing one, and switch all the - # tracers to use it. - data = self.contexts.setdefault(new_context, {}) - for tracer in self.tracers: - tracer.data = data + """Switch to a new dynamic context.""" + self.flush_data() + self.covdata.set_context(new_context) def cached_abs_file(self, filename): """A locally cached version of `abs_file`.""" @@ -415,7 +411,7 @@ def abs_file_dict(d): else: raise runtime_err # pylint: disable=raising-bad-type - return dict((self.cached_abs_file(k), v) for k, v in items) + return dict((self.cached_abs_file(k), v) for k, v in items if v) if self.branch: self.covdata.add_arcs(abs_file_dict(self.data)) @@ -423,12 +419,5 @@ def abs_file_dict(d): self.covdata.add_lines(abs_file_dict(self.data)) self.covdata.add_file_tracers(abs_file_dict(self.file_tracers)) - if self.wtw: - # Just a hack, so just hack it. - import pprint - out_file = "coverage_wtw_{:06}.py".format(os.getpid()) - with open(out_file, "w") as wtw_out: - pprint.pprint(self.contexts, wtw_out) - self._clear_data() return True diff --git a/coverage/config.py b/coverage/config.py index 9a11323d8..2a2818759 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -180,6 +180,7 @@ def __init__(self): self.data_file = ".coverage" self.debug = [] self.disable_warnings = [] + self.dynamic_context = None self.note = None self.parallel = False self.plugins = [] @@ -324,6 +325,7 @@ def from_file(self, filename, our_file): ('data_file', 'run:data_file'), ('debug', 'run:debug', 'list'), ('disable_warnings', 'run:disable_warnings', 'list'), + ('dynamic_context', 'run:dynamic_context'), ('note', 'run:note'), ('parallel', 'run:parallel', 'boolean'), ('plugins', 'run:plugins', 'list'), diff --git a/coverage/control.py b/coverage/control.py index f7d97cf61..23f0cbdd0 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -347,9 +347,19 @@ def _init_for_start(self): # it for the main process. self.config.parallel = True + if self.config.dynamic_context is None: + should_start_context = None + elif self.config.dynamic_context == "test_function": + should_start_context = should_start_context_test_function + else: + raise CoverageException( + "Don't understand dynamic_context setting: {!r}".format(self.config.dynamic_context) + ) + self._collector = Collector( should_trace=self._should_trace, check_include=self._check_include_omit_etc, + should_start_context=should_start_context, timid=self.config.timid, branch=self.config.branch, warn=self._warn, @@ -886,6 +896,16 @@ def plugin_info(plugins): Coverage = decorate_methods(show_calls(show_args=True), butnot=['get_data'])(Coverage) +def should_start_context_test_function(frame): + """Who-Tests-What hack: Determine whether this frame begins a new who-context.""" + with open("/tmp/ssc.txt", "a") as f: + f.write("hello\n") + fn_name = frame.f_code.co_name + if fn_name.startswith("test"): + return fn_name + return None + + def process_startup(): """Call this at Python start-up to perhaps measure coverage. diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c index 01f8b19ba..7d639112d 100644 --- a/coverage/ctracer/tracer.c +++ b/coverage/ctracer/tracer.c @@ -341,7 +341,6 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) CFileDisposition * pdisp = NULL; STATS( self->stats.calls++; ) - self->activity = TRUE; /* Grow the stack. */ if (CTracer_set_pdata_stack(self) < 0) { @@ -353,7 +352,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth]; /* See if this frame begins a new context. */ - if (self->should_start_context && self->context == Py_None) { + if (self->should_start_context != Py_None && self->context == Py_None) { PyObject * context; /* We're looking for our context, ask should_start_context if this is the start. */ STATS( self->stats.start_context_calls++; ) @@ -866,6 +865,8 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse goto error; } + self->activity = TRUE; + switch (what) { case PyTrace_CALL: if (CTracer_handle_call(self, frame) < 0) { diff --git a/coverage/ctracer/tracer.h b/coverage/ctracer/tracer.h index 61c01b41a..a83742ddf 100644 --- a/coverage/ctracer/tracer.h +++ b/coverage/ctracer/tracer.h @@ -27,7 +27,6 @@ typedef struct CTracer { PyObject * trace_arcs; PyObject * should_start_context; PyObject * switch_context; - PyObject * context; /* Has the tracer been started? */ BOOL started; @@ -35,6 +34,8 @@ typedef struct CTracer { BOOL tracing_arcs; /* Have we had any activity? */ BOOL activity; + /* The current dynamic context. */ + PyObject * context; /* The data stack is a stack of dictionaries. Each dictionary collects diff --git a/coverage/sqldata.py b/coverage/sqldata.py index fb2279c9a..738fccef1 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -199,6 +199,8 @@ def _context_id(self, context): def set_context(self, context): """Set the current context for future `add_lines` etc.""" + if self._debug and self._debug.should('dataop'): + self._debug.write("Setting context: %r" % (context,)) self._start_using() context = context or "" with self._connect() as con: diff --git a/tests/test_context.py b/tests/test_context.py index ad010a37c..919286c52 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -6,7 +6,9 @@ import os.path import coverage +from coverage import env from coverage.data import CoverageData +from coverage.misc import CoverageException from tests.coveragetest import CoverageTest @@ -102,3 +104,62 @@ def test_combining_arc_contexts(self): self.assertEqual(combined.arcs(fred, context='blue'), []) self.assertEqual(combined.arcs(fblue, context='red'), []) self.assertEqual(combined.arcs(fblue, context='blue'), self.ARCS) + + +class DynamicContextTest(CoverageTest): + """Tests of dynamically changing contexts.""" + + def setUp(self): + super(DynamicContextTest, self).setUp() + self.skip_unless_data_storage_is("sql") + if not env.C_TRACER: + self.skipTest("Only the C tracer supports dynamic contexts") + + def test_simple(self): + self.make_file("two_tests.py", """\ + def helper(lineno): + x = 2 + + def test_one(): + a = 5 + helper(6) + + def test_two(): + a = 9 + b = 10 + if a > 11: + b = 12 + assert a == (13-4) + assert b == (14-4) + helper(15) + + test_one() + x = 18 + helper(19) + test_two() + """) + cov = coverage.Coverage(source=["."]) + cov.set_option("run:dynamic_context", "test_function") + self.start_import_stop(cov, "two_tests") + data = cov.get_data() + + fname = os.path.abspath("two_tests.py") + self.assertCountEqual(data.measured_contexts(), ["", "test_one", "test_two"]) + self.assertCountEqual(data.lines(fname, ""), [1, 4, 8, 17, 18, 19, 2, 20]) + self.assertCountEqual(data.lines(fname, "test_one"), [5, 6, 2]) + self.assertCountEqual(data.lines(fname, "test_two"), [9, 10, 11, 13, 14, 15, 2]) + + +class DynamicContextWithPythonTracerTest(CoverageTest): + """The Python tracer doesn't do dynamic contexts at all.""" + + run_in_temp_dir = False + + def test_python_tracer_fails_properly(self): + if env.C_TRACER: + self.skipTest("This test is specifically about the Python tracer.") + cov = coverage.Coverage() + cov.set_option("run:dynamic_context", "test_function") + msg = r"Can't support dynamic contexts with PyTracer" + with self.assertRaisesRegex(CoverageException, msg): + cov.start()