summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt7
-rw-r--r--coverage/collector.py12
-rw-r--r--coverage/control.py8
-rw-r--r--coverage/tracer.c6
-rw-r--r--test/test_oddball.py49
5 files changed, 78 insertions, 4 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 4604489f..3c04d3b3 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -9,6 +9,13 @@ Version 3.5
- A little bit of Jython support: `coverage run` can now measure Jython
execution by adapting when $py.class files are traced. Thanks, Adi Roiban.
+- Pathological code execution could disable the trace function behind our
+ backs, leading to incorrect code measurement. Now if this happens,
+ coverage.py will issue a warning, at least alerting you to the problem.
+ Closes `issue 93`_. Thanks to Marius Gedminas for the idea.
+
+.. _issue 93: http://bitbucket.org/ned/coveragepy/issue/93/copying-a-mock-object-breaks-coverage
+
Version 3.4 --- 19 September 2010
---------------------------------
diff --git a/coverage/collector.py b/coverage/collector.py
index 6b196e99..9c40d16c 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -33,6 +33,7 @@ class PyTracer(object):
self.data = None
self.should_trace = None
self.should_trace_cache = None
+ self.warn = None
self.cur_file_data = None
self.last_line = 0
self.data_stack = []
@@ -109,6 +110,10 @@ class PyTracer(object):
def stop(self):
"""Stop this Tracer."""
+ if hasattr(sys, "gettrace") and self.warn:
+ if sys.gettrace() != self._trace:
+ msg = "Trace function changed, measurement is likely wrong: %r"
+ self.warn(msg % sys.gettrace())
sys.settrace(None)
def get_stats(self):
@@ -137,7 +142,7 @@ class Collector(object):
# the top, and resumed when they become the top again.
_collectors = []
- def __init__(self, should_trace, timid, branch):
+ def __init__(self, should_trace, timid, branch, warn):
"""Create a collector.
`should_trace` is a function, taking a filename, and returning a
@@ -153,8 +158,12 @@ class Collector(object):
collecting data on which statements followed each other (arcs). Use
`get_arc_data` to get the arc data.
+ `warn` is a warning function, taking a single string message argument,
+ to be used if a warning needs to be issued.
+
"""
self.should_trace = should_trace
+ self.warn = warn
self.branch = branch
self.reset()
@@ -194,6 +203,7 @@ class Collector(object):
tracer.arcs = self.branch
tracer.should_trace = self.should_trace
tracer.should_trace_cache = self.should_trace_cache
+ tracer.warn = self.warn
fn = tracer.start()
self.tracers.append(tracer)
return fn
diff --git a/coverage/control.py b/coverage/control.py
index cdaf9721..4fae198c 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -69,6 +69,9 @@ class coverage(object):
"""
from coverage import __version__
+ # A record of all the warnings that have been issued.
+ self._warnings = []
+
# Build our configuration from a number of sources:
# 1: defaults:
self.config = CoverageConfig()
@@ -120,7 +123,7 @@ class coverage(object):
self.collector = Collector(
self._should_trace, timid=self.config.timid,
- branch=self.config.branch
+ branch=self.config.branch, warn=self._warn
)
# Suffixes are a bit tricky. We want to use the data suffix only when
@@ -274,7 +277,8 @@ class coverage(object):
def _warn(self, msg):
"""Use `msg` as a warning."""
- sys.stderr.write("Coverage.py warning: " + msg + "\n")
+ self._warnings.append(msg)
+ sys.stderr.write("Coverage.py warning: %s\n" % msg)
def _abs_files(self, files):
"""Return a list of absolute file names for the names in `files`."""
diff --git a/coverage/tracer.c b/coverage/tracer.c
index 9eb3cca1..e046596a 100644
--- a/coverage/tracer.c
+++ b/coverage/tracer.c
@@ -63,6 +63,7 @@ typedef struct {
/* Python objects manipulated directly by the Collector class. */
PyObject * should_trace;
+ PyObject * warn;
PyObject * data;
PyObject * should_trace_cache;
PyObject * arcs;
@@ -134,6 +135,7 @@ Tracer_init(Tracer *self, PyObject *args_unused, PyObject *kwds_unused)
#endif /* COLLECT_STATS */
self->should_trace = NULL;
+ self->warn = NULL;
self->data = NULL;
self->should_trace_cache = NULL;
self->arcs = NULL;
@@ -166,6 +168,7 @@ Tracer_dealloc(Tracer *self)
}
Py_XDECREF(self->should_trace);
+ Py_XDECREF(self->warn);
Py_XDECREF(self->data);
Py_XDECREF(self->should_trace_cache);
@@ -539,6 +542,9 @@ Tracer_members[] = {
{ "should_trace", T_OBJECT, offsetof(Tracer, should_trace), 0,
PyDoc_STR("Function indicating whether to trace a file.") },
+ { "warn", T_OBJECT, offsetof(Tracer, warn), 0,
+ PyDoc_STR("Function for issuing warnings.") },
+
{ "data", T_OBJECT, offsetof(Tracer, data), 0,
PyDoc_STR("The raw dictionary of trace data.") },
diff --git a/test/test_oddball.py b/test/test_oddball.py
index 3d5bf097..e94e2bad 100644
--- a/test/test_oddball.py
+++ b/test/test_oddball.py
@@ -63,8 +63,9 @@ class RecursionTest(CoverageTest):
return recur(n-1)+1
recur(495) # We can get at least this many stack frames.
+ i = 8 # and this line will be traced
""",
- [1,2,3,5,7], "")
+ [1,2,3,5,7,8], "")
def test_long_recursion(self):
# We can't finish a very deep recursion, but we don't crash.
@@ -80,6 +81,52 @@ class RecursionTest(CoverageTest):
""",
[1,2,3,5,7], "")
+ def test_long_recursion_recovery(self):
+ # Test the core of bug 93: http://bitbucket.org/ned/coveragepy/issue/93
+ # When recovering from a stack overflow, the Python trace function is
+ # disabled, but the C trace function is not. So if we're using a
+ # Python trace function, we won't trace anything after the stack
+ # overflow, and there should be a warning about it. If we're using
+ # the C trace function, only line 3 will be missing, and all else
+ # will be traced.
+
+ self.make_file("recur.py", """\
+ def recur(n):
+ if n == 0:
+ return 0 # never hit
+ else:
+ return recur(n-1)+1
+
+ try:
+ recur(100000) # This is definitely too many frames.
+ except RuntimeError:
+ i = 10
+ i = 11
+ """)
+
+ cov = coverage.coverage()
+ cov.start()
+ self.import_local_file("recur")
+ cov.stop()
+
+ pytrace = (cov.collector.tracer_name() == "PyTracer")
+ expected_missing = [3]
+ if pytrace:
+ expected_missing += [9,10,11]
+
+ _, statements, missing, _ = cov.analysis("recur.py")
+ self.assertEqual(statements, [1,2,3,5,7,8,9,10,11])
+ self.assertEqual(missing, expected_missing)
+
+ # We can get a warning about the stackoverflow effect on the tracing
+ # function only if we have sys.gettrace
+ if pytrace and hasattr(sys, "gettrace"):
+ self.assertEqual(cov._warnings,
+ ["Trace function changed, measurement is likely wrong: None"]
+ )
+ else:
+ self.assertEqual(cov._warnings, [])
+
class MemoryLeakTest(CoverageTest):
"""Attempt the impossible: test that memory doesn't leak."""