summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2011-08-08 20:37:38 -0400
committerNed Batchelder <ned@nedbatchelder.com>2011-08-08 20:37:38 -0400
commit1966f545699490822117d66a86f2de91734a6ee9 (patch)
tree7b052bb6f115e455b7d7f21f01dd843c56c296db
parentced439588b3b3a66d5e68f00e8542d56250844d3 (diff)
downloadpython-coveragepy-1966f545699490822117d66a86f2de91734a6ee9.tar.gz
A fullcoverage tracer that works. Events are stashed, then replayed when coverage is started.
-rw-r--r--CHANGES.txt5
-rw-r--r--coverage/collector.py7
-rw-r--r--coverage/fullcoverage/encodings.py15
-rw-r--r--coverage/tracer.c33
-rw-r--r--test/coveragetest.py2
-rw-r--r--test/test_process.py20
6 files changed, 69 insertions, 13 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index 1c08ef6..8765dc1 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -17,6 +17,11 @@ Version 3.5.1
directive takes precendence, and the files will be measured. Fixes
`issue 138`_.
+- In order to help the core developers measure the test coverage of the
+ standard library, Brandon Rhodes devised an aggressive hack to trick Python
+ into running some coverage code before anything else in the process.
+ See the fullcoverage directory if you are interested.
+
.. _issue 122: http://bitbucket.org/ned/coveragepy/issue/122/for-else-always-reports-missing-branch
.. _issue 138: https://bitbucket.org/ned/coveragepy/issue/138/include-should-take-precedence-over-is
diff --git a/coverage/collector.py b/coverage/collector.py
index 9752e53..5498cc6 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -234,7 +234,7 @@ class Collector(object):
self._collectors.append(self)
#print >>sys.stderr, "Started: %r" % self._collectors
- # Check to see whether we had a fullcoverage tracer installed.
+ # Check to see whether we had a fullcoverage tracer installed.
traces0 = None
if hasattr(sys, "gettrace"):
fn0 = sys.gettrace()
@@ -247,10 +247,9 @@ class Collector(object):
fn = self._start_tracer()
if traces0:
- #print("traces0 has %d" % len(traces0))
for args in traces0:
- frame, event, arg = args
- fn(*args)
+ (frame, event, arg), lineno = args
+ fn(frame, event, arg, lineno=lineno)
# Install our installation tracer in threading, to jump start other
# threads.
diff --git a/coverage/fullcoverage/encodings.py b/coverage/fullcoverage/encodings.py
index 48b2b2e..4e1ab35 100644
--- a/coverage/fullcoverage/encodings.py
+++ b/coverage/fullcoverage/encodings.py
@@ -17,13 +17,22 @@ import sys
class FullCoverageTracer(object):
def __init__(self):
+ # `traces` is a list of trace events. Frames are tricky: the same
+ # frame object is used for a whole scope, with new line numbers
+ # written into it. So in one scope, all the frame objects are the
+ # same object, and will eventually all will point to the last line
+ # executed. So we keep the line numbers alongside the frames.
+ # The list looks like:
+ #
+ # traces = [
+ # ((frame, event, arg), lineno), ...
+ # ]
+ #
self.traces = []
def fullcoverage_trace(self, *args):
frame, event, arg = args
- #if "os.py" in frame.f_code.co_filename:
- # print("%s @ %d" % (frame.f_code.co_filename, frame.f_lineno))
- self.traces.append(args)
+ self.traces.append((args, frame.f_lineno))
return self.fullcoverage_trace
sys.settrace(FullCoverageTracer().fullcoverage_trace)
diff --git a/coverage/tracer.c b/coverage/tracer.c
index a7a4f41..e9fc56b 100644
--- a/coverage/tracer.c
+++ b/coverage/tracer.c
@@ -468,14 +468,26 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg_unused)
* means it must be callable to be used in sys.settrace().
*
* So we make our self callable, equivalent to invoking our trace function.
+ *
+ * To help with the process of replaying stored frames, this function has an
+ * optional keyword argument:
+ *
+ * def Tracer_call(frame, event, arg, lineno=0)
+ *
+ * If provided, the lineno argument is used as the line number, and the
+ * frame's f_lineno member is ignored.
*/
static PyObject *
-Tracer_call(Tracer *self, PyObject *args, PyObject *kwds_unused)
+Tracer_call(Tracer *self, PyObject *args, PyObject *kwds)
{
PyFrameObject *frame;
PyObject *what_str;
PyObject *arg;
+ int lineno = 0;
int what;
+ int orig_lineno;
+ PyObject *ret = NULL;
+
static char *what_names[] = {
"call", "exception", "line", "return",
"c_call", "c_exception", "c_return",
@@ -486,8 +498,10 @@ Tracer_call(Tracer *self, PyObject *args, PyObject *kwds_unused)
printf("pytrace\n");
#endif
- if (!PyArg_ParseTuple(args, "O!O!O:Tracer_call",
- &PyFrame_Type, &frame, &MyText_Type, &what_str, &arg)) {
+ static char *kwlist[] = {"frame", "event", "arg", "lineno", NULL};
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O!O|i:Tracer_call", kwlist,
+ &PyFrame_Type, &frame, &MyText_Type, &what_str, &arg, &lineno)) {
goto done;
}
@@ -499,14 +513,23 @@ Tracer_call(Tracer *self, PyObject *args, PyObject *kwds_unused)
}
}
+ /* Save off the frame's lineno, and use the forced one, if provided. */
+ orig_lineno = frame->f_lineno;
+ if (lineno > 0) {
+ frame->f_lineno = lineno;
+ }
+
/* Invoke the C function, and return ourselves. */
if (Tracer_trace(self, frame, what, arg) == RET_OK) {
Py_INCREF(self);
- return (PyObject *)self;
+ ret = (PyObject *)self;
}
+ /* Clean up. */
+ frame->f_lineno = orig_lineno;
+
done:
- return NULL;
+ return ret;
}
static PyObject *
diff --git a/test/coveragetest.py b/test/coveragetest.py
index 3242e52..9bff27e 100644
--- a/test/coveragetest.py
+++ b/test/coveragetest.py
@@ -426,7 +426,7 @@ class CoverageTest(TestCase):
here = os.path.dirname(self.nice_file(coverage.__file__, ".."))
testmods = self.nice_file(here, 'test/modules')
zipfile = self.nice_file(here, 'test/zipmods.zip')
- pypath = self.original_environ('PYTHONPATH', "")
+ pypath = os.getenv('PYTHONPATH')
if pypath:
pypath += os.pathsep
pypath += testmods + os.pathsep + zipfile
diff --git a/test/test_process.py b/test/test_process.py
index c32868b..e62fce2 100644
--- a/test/test_process.py
+++ b/test/test_process.py
@@ -292,3 +292,23 @@ class ProcessTest(CoverageTest):
self.assertTrue("No module named no_such_module" in out)
self.assertTrue("warning" not in out)
+ if sys.version_info >= (2, 7): # Need coverage runnable as a module.
+ def test_fullcoverage(self):
+ # fullcoverage is a trick to get stdlib modules measured from the
+ # very beginning of the process. Here we import os and then check
+ # how many lines are measured.
+ self.make_file("getenv.py", """\
+ import os
+ print("FOOEY == %s" % os.getenv("FOOEY"))
+ """)
+
+ fullcov = os.path.join(os.path.dirname(coverage.__file__), "fullcoverage")
+ self.set_environ("FOOEY", "BOO")
+ self.set_environ("PYTHONPATH", fullcov)
+ out = self.run_command("python -m coverage run -L getenv.py")
+ self.assertEqual(out, "FOOEY == BOO\n")
+ data = coverage.CoverageData()
+ data.read_file(".coverage")
+ # The actual number of lines in os.py executed when it is imported
+ # is 120 or so. Just running os.getenv executes about 5.
+ self.assertGreater(data.summary()['os.py'], 50)