Commits

Ned Batchelder  committed cb4e940

Properly record multiple exits separately. Fixes #62.

  • Participants
  • Parent commits 13bc67c

Comments (0)

Files changed (7)

 - 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

File coverage/collector.py

         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):
             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
 
                 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.
             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):

File coverage/html.py

                 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))

File coverage/parser.py

         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:
 
             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))
                         # 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.
         """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()
         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?
                     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)
 
         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]:
 
     .. _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):
         """
         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:

File coverage/tracer.c

 
     /* The parent frame for the last exception event, to fix missing returns. */
     PyFrameObject * last_exc_back;
+    int last_exc_firstlineno;
 
 #if COLLECT_STATS
     struct {
             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;
                     }
                 }
         /* 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;
                 }
             }
         */
         STATS( self->stats.exceptions++; )
         self->last_exc_back = frame->f_back;
+        self->last_exc_firstlineno = frame->f_code->co_firstlineno;
         break;
 
     default:

File test/coveragetest.py

 
         ".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=""):
                         )
 
         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:

File test/test_arcs.py

 
             c = 5
             """,
-            arcz=".2 23 35 5.")
+            arcz=".2 23 35 5-2")
 
     def test_function_def(self):
         self.check_coverage("""\
             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 +
             b = \\
                 6
             """,
-            arcz=".1 15 5.", arcz_missing="")
+            arcz=arcz, arcz_missing="")
 
     def test_if_return(self):
         self.check_coverage("""\
             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."
             )
 
                 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):
             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")
 
                     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")