diff options
-rw-r--r-- | CHANGES.txt | 5 | ||||
-rw-r--r-- | coverage/collector.py | 7 | ||||
-rw-r--r-- | coverage/fullcoverage/encodings.py | 15 | ||||
-rw-r--r-- | coverage/tracer.c | 33 | ||||
-rw-r--r-- | test/coveragetest.py | 2 | ||||
-rw-r--r-- | test/test_process.py | 20 |
6 files changed, 69 insertions, 13 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 1c08ef69..8765dc16 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 9752e530..5498cc6c 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 48b2b2e0..4e1ab354 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 a7a4f418..e9fc56b4 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 3242e52f..9bff27eb 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 c32868be..e62fce22 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) |