diff options
-rw-r--r-- | CHANGES.txt | 3 | ||||
-rw-r--r-- | coverage/collector.py | 73 | ||||
-rw-r--r-- | coverage/tracer.c | 68 |
3 files changed, 72 insertions, 72 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 24f2ab6f..69447d91 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -20,8 +20,7 @@ Version 3.next invocations could be overlooked.
- On Python 2.3, coverage.py could mis-measure code with exceptions being
- raised. This is now fixed, but as a result, on Python 2.3, the Python trace
- function is used, reversing speed gains acheived in coverage.py 3.0.
+ raised. This is now fixed.
Version 3.0, 13 June 2009
diff --git a/coverage/collector.py b/coverage/collector.py index d196062b..0a5349ad 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -2,28 +2,13 @@ import sys, threading -# Before Python 2.4, the trace function could be called with "call", and then -# leave the scope without calling "return", for example when an exception -# caused the scope to end. We use the matching call and return traces to do -# bookkeeping for speed, so we need to know up front if they match our -# expectations. -CALL_AND_RETURN_MATCH = (sys.hexversion >= 0x02040000) - - -Tracer = None -if CALL_AND_RETURN_MATCH: - # We have matching calls and returns, so we can use the C extension. - try: - # Use the C extension code when we can, for speed. - from coverage.tracer import Tracer - except ImportError: - # Couldn't import the C extension, maybe it isn't built. - pass - - -if not Tracer: - # If for whatever reason we don't have a Tracer yet, use this Python one. - class Tracer: # pylint: disable-msg=E0102 +try: + # Use the C extension code when we can, for speed. + from coverage.tracer import Tracer +except ImportError: + # Couldn't import the C extension, maybe it isn't built. + + class Tracer: """Python implementation of the raw data tracer.""" def __init__(self): self.data = None @@ -31,7 +16,8 @@ if not Tracer: self.should_trace_cache = None self.cur_filename = None self.filename_stack = [] - + self.last_exc_back = None + def _global_trace(self, frame, event, arg_unused): """The trace function passed to sys.settrace.""" if event == 'call': @@ -54,32 +40,23 @@ if not Tracer: return None return self._global_trace - if CALL_AND_RETURN_MATCH: - def _local_trace(self, frame, event, arg_unused): - """The trace function used within a function.""" - if event == 'line': - # Record an executed line. - self.data[(self.cur_filename, frame.f_lineno)] = True - elif event == 'return': - # Leaving this function, pop the filename stack. + def _local_trace(self, frame, event, arg_unused): + """The trace function used within a function.""" + if self.last_exc_back: + if frame == self.last_exc_back: + # Someone forgot a return event. self.cur_filename = self.filename_stack.pop() - return self._local_trace - else: - def _local_trace(self, frame, event, arg_unused): - """The trace function used within a function.""" - if event == 'line': - if not self.cur_filename: - # Kinda hacky: call _global_trace to do the work of - # setting the cur_filename. - if not self._global_trace(frame, 'call', None): - return None - # Record an executed line. - self.data[(self.cur_filename, frame.f_lineno)] = True - elif event == 'return' or event == 'exception': - # Leaving this function, discard the current filename so - # we'll re-compute it at the next line. - self.cur_filename = None - return self._local_trace + self.last_exc_back = None + + if event == 'line': + # Record an executed line. + self.data[(self.cur_filename, frame.f_lineno)] = True + elif event == 'return': + # Leaving this function, pop the filename stack. + self.cur_filename = self.filename_stack.pop() + elif event == 'exception': + self.last_exc_back = frame.f_back + return self._local_trace def start(self): """Start this Tracer.""" diff --git a/coverage/tracer.c b/coverage/tracer.c index ba84204f..8cd7574a 100644 --- a/coverage/tracer.c +++ b/coverage/tracer.c @@ -22,6 +22,9 @@ typedef struct { /* Filenames to record at each level, or NULL if not recording. */
PyObject ** tracenames; /* PyMem_Malloc'ed. */
int tracenames_alloc; /* number of entries at tracenames. */
+
+ /* The parent frame for the last exception event, to fix missing returns. */
+ PyFrameObject * last_exc_back;
} Tracer;
#define TRACENAMES_DELTA 100
@@ -39,6 +42,7 @@ Tracer_init(Tracer *self, PyObject *args, PyObject *kwds) return -1;
}
self->tracenames_alloc = TRACENAMES_DELTA;
+ self->last_exc_back = NULL;
return 0;
}
@@ -129,6 +133,29 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg) }
#endif
+ /* See below for details on missing-return detection. */
+ if (self->last_exc_back) {
+ if (frame == self->last_exc_back) {
+ /* Looks like someone forgot to send a return event. We'll clear
+ the exception state and do the RETURN code here. Notice that the
+ frame we have in hand here is not the correct frame for the RETURN,
+ that frame is gone. Our handling for RETURN doesn't need the
+ actual frame, but we do log it, so that will look a little off if
+ you're looking at the detailed log.
+
+ If someday we need to examine the frame when doing RETURN, then
+ we'll need to keep more of the missed frame's state.
+ */
+ if (self->depth >= 0) {
+ SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "missedreturn");
+ Py_XDECREF(self->tracenames[self->depth]);
+ self->depth--;
+ }
+ }
+ self->last_exc_back = NULL;
+ }
+
+
switch (what) {
case PyTrace_CALL: /* 0 */
self->depth++;
@@ -175,6 +202,7 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg) break;
case PyTrace_RETURN: /* 3 */
+ /* A near-copy of this code is above in the missing-return handler. */
if (self->depth >= 0) {
SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "return");
Py_XDECREF(self->tracenames[self->depth]);
@@ -196,28 +224,24 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg) }
}
break;
- }
-
- /* UGLY HACK: for some reason, pyexpat invokes the systrace function directly.
- It uses "pyexpat.c" as the filename, which is strange enough, but it calls
- it incorrectly: when an exception passes through the C code, it calls trace
- with an EXCEPTION, but never calls RETURN. This throws off our bookkeeping.
- To make things right, if this is an EXCEPTION from pyexpat.c, then inject
- a RETURN event also.
-
- I've reported the problem with pyexpat.c as http://bugs.python.org/issue6359 .
- If the bug in pyexpat.c gets fixed someday, we'll either have to put a
- version check here, or do something more sophisticated to detect the
- EXCEPTION-without-RETURN case that has to be fixed up.
- */
- if (what == PyTrace_EXCEPTION) {
- if (strstr(PyString_AS_STRING(frame->f_code->co_filename), "pyexpat.c")) {
- /* Stupid pyexpat: pretend it gave us the RETURN it should have. */
- SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "wrongexc");
- if (Tracer_trace(self, frame, PyTrace_RETURN, arg) < 0) {
- return -1;
- }
- }
+
+ case PyTrace_EXCEPTION:
+ /* Some code (Python 2.3, and pyexpat anywhere) fires an exception event
+ without a return event. To detect that, we'll keep a copy of the
+ parent frame for an exception event. If the next event is in that
+ frame, then we must have returned without a return event. We can
+ synthesize the missing event then.
+
+ Python itself fixed this problem in 2.4. Pyexpat still has the bug.
+ I've reported the problem with pyexpat as http://bugs.python.org/issue6359 .
+ If it gets fixed, this code should still work properly. Maybe someday
+ the bug will be fixed everywhere coverage.py is supported, and we can
+ remove this missing-return detection.
+
+ More about this fix: http://nedbatchelder.com/blog/200907/a_nasty_little_bug.html
+ */
+ self->last_exc_back = frame->f_back;
+ break;
}
return 0;
|