diff options
-rw-r--r-- | CHANGES.txt | 4 | ||||
-rw-r--r-- | coverage/collector.py | 10 | ||||
-rw-r--r-- | coverage/html.py | 2 | ||||
-rw-r--r-- | coverage/parser.py | 36 | ||||
-rw-r--r-- | coverage/tracer.c | 7 | ||||
-rw-r--r-- | test/coveragetest.py | 36 | ||||
-rw-r--r-- | test/test_arcs.py | 18 |
7 files changed, 80 insertions, 33 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 92ac8828..4cdbeffb 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -25,9 +25,13 @@ Next version - Source files with DOS line endings are now properly tokenized for syntax coloring on non-DOS machines. Fixes `issue 53`_. +- Unusual code structure that confused exits from methods with exits from + classes is now properly analyzed. See `issue 62`_. + .. _issue 46: http://bitbucket.org/ned/coveragepy/issue/46 .. _issue 53: http://bitbucket.org/ned/coveragepy/issue/53 .. _issue 56: http://bitbucket.org/ned/coveragepy/issue/56 +.. _issue 62: http://bitbucket.org/ned/coveragepy/issue/62 Version 3.3.1, 6 March 2010 diff --git a/coverage/collector.py b/coverage/collector.py index 1837aae2..06ccda7e 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -37,6 +37,7 @@ class PyTracer(object): self.last_line = 0 self.data_stack = [] self.last_exc_back = None + self.last_exc_firstlineno = 0 self.arcs = False def _trace(self, frame, event, arg_unused): @@ -49,7 +50,8 @@ class PyTracer(object): if frame == self.last_exc_back: # Someone forgot a return event. if self.arcs and self.cur_file_data: - self.cur_file_data[(self.last_line, -1)] = None + pair = (self.last_line, -self.last_exc_firstlineno) + self.cur_file_data[pair] = None self.cur_file_data, self.last_line = self.data_stack.pop() self.last_exc_back = None @@ -65,6 +67,8 @@ class PyTracer(object): self.cur_file_data = self.data[tracename] else: self.cur_file_data = None + # Set the last_line to -1 because the next arc will be entering a + # code block, indicated by (-1, n). self.last_line = -1 elif event == 'line': # Record an executed line. @@ -78,12 +82,14 @@ class PyTracer(object): self.last_line = frame.f_lineno elif event == 'return': if self.arcs and self.cur_file_data: - self.cur_file_data[(self.last_line, -1)] = None + first = frame.f_code.co_firstlineno + self.cur_file_data[(self.last_line, -first)] = None # Leaving this function, pop the filename stack. self.cur_file_data, self.last_line = self.data_stack.pop() elif event == 'exception': #print "exc", self.last_line, frame.f_lineno self.last_exc_back = frame.f_back + self.last_exc_firstlineno = frame.f_code.co_firstlineno return self._trace def start(self): diff --git a/coverage/html.py b/coverage/html.py index f8de2e4c..7d0d064b 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -93,7 +93,7 @@ class HtmlReporter(Reporter): n_par += 1 annlines = [] for b in missing_branch_arcs[lineno]: - if b == -1: + if b < 0: annlines.append("exit") else: annlines.append(str(b)) diff --git a/coverage/parser.py b/coverage/parser.py index aea05f3d..b090f02d 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -214,7 +214,7 @@ class CodeParser(object): excluded_lines = self.first_lines(self.excluded) exit_counts = {} for l1, l2 in self.arcs(): - if l1 == -1: + if l1 < 0: # Don't ever report -1 as a line number continue if l1 in excluded_lines: @@ -434,7 +434,7 @@ class ByteParser(object): if bc.op in OPS_CODE_END: # The opcode can exit the code object. - chunk.exits.add(-1) + chunk.exits.add(-self.code.co_firstlineno) if bc.op in OPS_PUSH_BLOCK: # The opcode adds a block to the block_stack. block_stack.append((bc.op, bc.jump_to)) @@ -479,12 +479,13 @@ class ByteParser(object): # This is "return None", but is it dummy? A real line # would be a last chunk all by itself. if chunks[-1].byte != penult.offset: + exit = -self.code.co_firstlineno # Split the last chunk last_chunk = chunks[-1] - last_chunk.exits.remove(-1) + last_chunk.exits.remove(exit) last_chunk.exits.add(penult.offset) chunk = Chunk(penult.offset) - chunk.exits.add(-1) + chunk.exits.add(exit) chunks.append(chunk) # Give all the chunks a length. @@ -498,8 +499,8 @@ class ByteParser(object): """Find the executable arcs in the code. Returns a set of pairs, (from,to). From and to are integer line - numbers. If from is -1, then the arc is an entrance into the code - object. If to is -1, the arc is an exit from the code object. + numbers. If from is < 0, then the arc is an entrance into the code + object. If to is < 0, the arc is an exit from the code object. """ chunks = self._split_into_chunks() @@ -508,12 +509,12 @@ class ByteParser(object): byte_chunks = dict([(c.byte, c) for c in chunks]) # Build a map from byte offsets to actual lines reached. - byte_lines = {-1:[-1]} + byte_lines = {} bytes_to_add = set([c.byte for c in chunks]) while bytes_to_add: byte_to_add = bytes_to_add.pop() - if byte_to_add in byte_lines or byte_to_add == -1: + if byte_to_add in byte_lines or byte_to_add < 0: continue # Which lines does this chunk lead to? @@ -541,8 +542,8 @@ class ByteParser(object): lines.add(ch.line) else: for ex in ch.exits: - if ex == -1: - lines.add(-1) + if ex < 0: + lines.add(ex) elif ex not in bytes_considered: bytes_to_consider.append(ex) @@ -555,7 +556,11 @@ class ByteParser(object): for chunk in chunks: if chunk.line: for ex in chunk.exits: - for exit_line in byte_lines[ex]: + if ex < 0: + exit_lines = [ex] + else: + exit_lines = byte_lines[ex] + for exit_line in exit_lines: if chunk.line != exit_line: arcs.add((chunk.line, exit_line)) for line in byte_lines[0]: @@ -601,7 +606,8 @@ class Chunk(object): .. _basic block: http://en.wikipedia.org/wiki/Basic_block - An exit of -1 means the chunk can leave the code (return). + An exit < 0 means the chunk can leave the code (return). The exit is + the negative of the starting line number of the code block. """ def __init__(self, byte, line=0): @@ -723,12 +729,12 @@ class AdHocMain(object): # pragma: no cover """ arc_chars = {} for lfrom, lto in sorted(arcs): - if lfrom == -1: + if lfrom < 0: arc_chars[lto] = arc_chars.get(lto, '') + 'v' - elif lto == -1: + elif lto < 0: arc_chars[lfrom] = arc_chars.get(lfrom, '') + '^' else: - if lfrom == lto-1: + if lfrom == lto - 1: # Don't show obvious arcs. continue if lfrom < lto: diff --git a/coverage/tracer.c b/coverage/tracer.c index 448f9452..1d227295 100644 --- a/coverage/tracer.c +++ b/coverage/tracer.c @@ -97,6 +97,7 @@ typedef struct { /* The parent frame for the last exception event, to fix missing returns. */
PyFrameObject * last_exc_back;
+ int last_exc_firstlineno;
#if COLLECT_STATS
struct {
@@ -280,7 +281,7 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg) STATS( self->stats.missed_returns++; )
if (self->depth >= 0) {
if (self->tracing_arcs && self->cur_file_data) {
- if (Tracer_record_pair(self, self->last_line, -1) < 0) {
+ if (Tracer_record_pair(self, self->last_line, -self->last_exc_firstlineno) < 0) {
return RET_ERROR;
}
}
@@ -376,7 +377,8 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg) /* A near-copy of this code is above in the missing-return handler. */
if (self->depth >= 0) {
if (self->tracing_arcs && self->cur_file_data) {
- if (Tracer_record_pair(self, self->last_line, -1) < 0) {
+ int first = frame->f_code->co_firstlineno;
+ if (Tracer_record_pair(self, self->last_line, -first) < 0) {
return RET_ERROR;
}
}
@@ -436,6 +438,7 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg) */
STATS( self->stats.exceptions++; )
self->last_exc_back = frame->f_back;
+ self->last_exc_firstlineno = frame->f_code->co_firstlineno;
break;
default:
diff --git a/test/coveragetest.py b/test/coveragetest.py index 981beeea..277b4f03 100644 --- a/test/coveragetest.py +++ b/test/coveragetest.py @@ -176,18 +176,34 @@ class CoverageTest(TestCase): ".1 12 2." --> [(-1,1), (1,2), (2,-1)] + Minus signs can be included in the pairs: + + "-11, 12, 2-5" --> [(-1,1), (1,2), (2,-5)] + """ arcs = [] - for a,b in arcz.split(): - arcs.append((self._arcz_map[a], self._arcz_map[b])) + for pair in arcz.split(): + asgn = bsgn = 1 + if len(pair) == 2: + a,b = pair + else: + assert len(pair) == 3 + if pair[0] == '-': + _,a,b = pair + asgn = -1 + else: + assert pair[1] == '-' + a,_,b = pair + bsgn = -1 + arcs.append((asgn*self._arcz_map[a], bsgn*self._arcz_map[b])) return sorted(arcs) - def assertEqualArcs(self, a1, a2): + def assertEqualArcs(self, a1, a2, msg=None): """Assert that the arc lists `a1` and `a2` are equal.""" # Make them into multi-line strings so we can see what's going wrong. s1 = "\n".join([repr(a) for a in a1]) + "\n" s2 = "\n".join([repr(a) for a in a2]) + "\n" - self.assertMultiLineEqual(s1, s2) + self.assertMultiLineEqual(s1, s2, msg) def check_coverage(self, text, lines=None, missing="", excludes=None, report="", arcz=None, arcz_missing="", arcz_unpredicted=""): @@ -265,14 +281,20 @@ class CoverageTest(TestCase): ) if arcs is not None: - self.assertEqualArcs(analysis.arc_possibilities(), arcs) + self.assertEqualArcs( + analysis.arc_possibilities(), arcs, "Possible arcs differ" + ) if arcs_missing is not None: - self.assertEqualArcs(analysis.arcs_missing(), arcs_missing) + self.assertEqualArcs( + analysis.arcs_missing(), arcs_missing, + "Missing arcs differ" + ) if arcs_unpredicted is not None: self.assertEqualArcs( - analysis.arcs_unpredicted(), arcs_unpredicted + analysis.arcs_unpredicted(), arcs_unpredicted, + "Unpredicted arcs differ" ) if report: diff --git a/test/test_arcs.py b/test/test_arcs.py index 5698ca5c..41731dba 100644 --- a/test/test_arcs.py +++ b/test/test_arcs.py @@ -28,7 +28,7 @@ class SimpleArcTest(CoverageTest): c = 5 """, - arcz=".2 23 35 5.") + arcz=".2 23 35 5-2") def test_function_def(self): self.check_coverage("""\ @@ -90,6 +90,12 @@ class SimpleArcTest(CoverageTest): arcz=".1 14 45 5. .2 2. 23 3.", arcz_missing="23 3.") def test_multiline(self): + # The firstlineno of the a assignment below differs among Python + # versions. + if sys.version_info >= (2, 5): + arcz = ".1 15 5-2" + else: + arcz = ".1 15 5-1" self.check_coverage("""\ a = ( 2 + @@ -98,7 +104,7 @@ class SimpleArcTest(CoverageTest): b = \\ 6 """, - arcz=".1 15 5.", arcz_missing="") + arcz=arcz, arcz_missing="") def test_if_return(self): self.check_coverage("""\ @@ -252,7 +258,7 @@ class LoopArcTest(CoverageTest): arcz= ".1 18 8G GH H. " ".2 23 34 43 26 3. 6. " - ".9 9A 9. AB BC CB B9 AE E9", + ".9 9A 9-8 AB BC CB B9 AE E9", arcz_missing="26 6." ) @@ -296,7 +302,7 @@ class ExceptionArcTest(CoverageTest): b = 9 assert a == 5 and b == 9 """, - arcz=".1 12 .3 3. 24 45 56 67 7A 89 9A A.", + arcz=".1 12 .3 3-2 24 45 56 67 7A 89 9A A.", arcz_missing="67 7A", arcz_unpredicted="68") def test_except_with_type(self): @@ -315,7 +321,7 @@ class ExceptionArcTest(CoverageTest): assert try_it(0) == 8 # C assert try_it(1) == 6 # D """, - arcz=".1 12 .3 3. 24 4C CD D. .5 56 67 78 8B 9A AB B.", + arcz=".1 12 .3 3-2 24 4C CD D. .5 56 67 78 8B 9A AB B-4", arcz_missing="", arcz_unpredicted="79") @@ -442,7 +448,7 @@ class ExceptionArcTest(CoverageTest): c = 11 assert a == 5 and b == 9 and c == 11 """, - arcz=".1 12 .3 3. 24 45 56 67 7B 89 9B BC C.", + arcz=".1 12 .3 3-2 24 45 56 67 7B 89 9B BC C.", arcz_missing="67 7B", arcz_unpredicted="68") |