summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt4
-rw-r--r--coverage/collector.py10
-rw-r--r--coverage/html.py2
-rw-r--r--coverage/parser.py36
-rw-r--r--coverage/tracer.c7
-rw-r--r--test/coveragetest.py36
-rw-r--r--test/test_arcs.py18
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")