Skip to content

Commit

Permalink
Dynamic contexts
Browse files Browse the repository at this point in the history
  • Loading branch information
nedbat committed Sep 23, 2018
1 parent b609117 commit 106828c
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 40 deletions.
63 changes: 26 additions & 37 deletions coverage/collector.py
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand All @@ -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 = {}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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`."""
Expand Down Expand Up @@ -415,20 +411,13 @@ 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))
else:
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
2 changes: 2 additions & 0 deletions coverage/config.py
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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'),
Expand Down
20 changes: 20 additions & 0 deletions coverage/control.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions coverage/ctracer/tracer.c
Expand Up @@ -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) {
Expand All @@ -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++; )
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion coverage/ctracer/tracer.h
Expand Up @@ -27,14 +27,15 @@ typedef struct CTracer {
PyObject * trace_arcs;
PyObject * should_start_context;
PyObject * switch_context;
PyObject * context;

/* Has the tracer been started? */
BOOL started;
/* Are we tracing arcs, or just lines? */
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
Expand Down
2 changes: 2 additions & 0 deletions coverage/sqldata.py
Expand Up @@ -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:
Expand Down
61 changes: 61 additions & 0 deletions tests/test_context.py
Expand Up @@ -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

Expand Down Expand Up @@ -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()

0 comments on commit 106828c

Please sign in to comment.