diff options
-rw-r--r-- | coverage/html.py | 3 | ||||
-rw-r--r-- | coverage/parser.py | 28 | ||||
-rw-r--r-- | coverage/plugin.py | 8 | ||||
-rw-r--r-- | coverage/python.py | 4 | ||||
-rw-r--r-- | tests/test_arcs.py | 30 | ||||
-rw-r--r-- | tests/test_parser.py | 10 |
6 files changed, 68 insertions, 15 deletions
diff --git a/coverage/html.py b/coverage/html.py index 9471db93..d4fb7516 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -190,6 +190,7 @@ class HtmlReporter(Reporter): if self.has_arcs: missing_branch_arcs = analysis.missing_branch_arcs() + arcs_executed = analysis.arcs_executed() # These classes determine which lines are highlighted by default. c_run = "run hide_run" @@ -219,7 +220,7 @@ class HtmlReporter(Reporter): shorts.append("exit") else: shorts.append(b) - longs.append(fr.missing_arc_description(lineno, b)) + longs.append(fr.missing_arc_description(lineno, b, arcs_executed)) # 202F is NARROW NO-BREAK SPACE. # 219B is RIGHTWARDS ARROW WITH STROKE. short_fmt = "%s ↛ %s" diff --git a/coverage/parser.py b/coverage/parser.py index f4dd02d4..8fb5d89b 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -21,8 +21,12 @@ from coverage.phystokens import compile_unicode, generate_tokens, neuter_encodin class PythonParser(object): - """Parse code to find executable lines, excluded lines, etc.""" + """Parse code to find executable lines, excluded lines, etc. + This information is all based on static analysis: no code execution is + involved. + + """ @contract(text='unicode|None') def __init__(self, text=None, filename=None, exclude=None): """ @@ -304,11 +308,23 @@ class PythonParser(object): return exit_counts - def missing_arc_description(self, start, end): + def missing_arc_description(self, start, end, executed_arcs=None): """Provide an English sentence describing a missing arc.""" if self._missing_arc_fragments is None: self._analyze_ast() + actual_start = start + + if ( + executed_arcs and + end < 0 and end == -start and + (end, start) not in executed_arcs and + (end, start) in self._missing_arc_fragments + ): + # It's a one-line callable, and we never even started it, + # and we have a message about not starting it. + start, end = end, start + fragment_pairs = self._missing_arc_fragments.get((start, end), [(None, None)]) msgs = [] @@ -325,9 +341,9 @@ class PythonParser(object): emsg = "didn't jump to line {lineno}" emsg = emsg.format(lineno=end) - msg = "line {start} {emsg}".format(start=start, emsg=emsg) + msg = "line {start} {emsg}".format(start=actual_start, emsg=emsg) if smsg is not None: - msg += ", because {smsg}".format(smsg=smsg.format(lineno=start)) + msg += ", because {smsg}".format(smsg=smsg.format(lineno=actual_start)) msgs.append(msg) @@ -939,10 +955,10 @@ class AstArcAnalyzer(object): """A function to make methods for online callable _code_object__ methods.""" def _code_object__oneline_callable(self, node): start = self.line_for_node(node) - self.add_arc(-start, start) + self.add_arc(-start, start, None, "didn't run the {0} on line {1}".format(noun, start)) self.add_arc( start, -start, None, - "didn't run the {0} on line {1}".format(noun, start), + "didn't finish the {0} on line {1}".format(noun, start), ) return _code_object__oneline_callable diff --git a/coverage/plugin.py b/coverage/plugin.py index 97d9c16e..a7b95466 100644 --- a/coverage/plugin.py +++ b/coverage/plugin.py @@ -329,9 +329,15 @@ class FileReporter(object): """ return {} - def missing_arc_description(self, start, end): + def missing_arc_description(self, start, end, executed_arcs=None): # pylint: disable=unused-argument """Provide an English sentence describing a missing arc. + The `start` and `end` arguments are the line numbers of the missing + arc. Negative numbers indicate entering or exiting code objects. + + The `executed_arcs` argument is a set of line number pairs, the arcs + that were executed in this file. + By default, this simply returns the string "Line {start} didn't jump to {end}". diff --git a/coverage/python.py b/coverage/python.py index 0d2fb3b4..0cd2181c 100644 --- a/coverage/python.py +++ b/coverage/python.py @@ -165,8 +165,8 @@ class PythonFileReporter(FileReporter): def exit_counts(self): return self.parser.exit_counts() - def missing_arc_description(self, start, end): - return self.parser.missing_arc_description(start, end) + def missing_arc_description(self, start, end, executed_arcs=None): + return self.parser.missing_arc_description(start, end, executed_arcs) @contract(returns='unicode') def source(self): diff --git a/tests/test_arcs.py b/tests/test_arcs.py index 826256b0..16efbcdf 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -1105,6 +1105,36 @@ class MiscArcTest(CoverageTest): arcz_missing="27", ) + def test_partial_generators(self): + # https://bitbucket.org/ned/coveragepy/issues/475/generator-expression-is-marked-as-not + # Line 2 is executed completely. + # Line 3 is started but not finished, because zip ends when #2 ends. + # Line 4 is never started. + cov = self.check_coverage("""\ + def f(a, b): + c = (i for i in a) # 2 + d = (j for j in b) # 3 + e = (k for k in b) # 4 + return dict(zip(c, d)) + + f(['a', 'b'], [1, 2]) + """, + arcz=".1 17 7. .2 23 34 45 5. -22 2-2 -33 3-3 -44 4-4", + arcz_missing="3-3 -44 4-4", + ) + # ugh, unexposed methods?? + filename = self.last_module_name + ".py" + fr = cov._get_file_reporter(filename) + arcs_executed = cov._analyze(filename).arcs_executed() + self.assertEqual( + fr.missing_arc_description(3, -3, arcs_executed), + "line 3 didn't finish the generator expression on line 3" + ) + self.assertEqual( + fr.missing_arc_description(4, -4, arcs_executed), + "line 4 didn't run the generator expression on line 4" + ) + class DecoratorArcTest(CoverageTest): """Tests of arcs with decorators.""" diff --git a/tests/test_parser.py b/tests/test_parser.py index 17f81ad8..ed18ccaa 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -256,19 +256,19 @@ class ParserMissingArcDescriptionTest(CoverageTest): """) self.assertEqual( parser.missing_arc_description(2, -2), - "line 2 didn't run the lambda on line 2" + "line 2 didn't finish the lambda on line 2" ) self.assertEqual( parser.missing_arc_description(3, -3), - "line 3 didn't run the generator expression on line 3" + "line 3 didn't finish the generator expression on line 3" ) self.assertEqual( parser.missing_arc_description(4, -4), - "line 4 didn't run the dictionary comprehension on line 4" + "line 4 didn't finish the dictionary comprehension on line 4" ) self.assertEqual( parser.missing_arc_description(5, -5), - "line 5 didn't run the set comprehension on line 5" + "line 5 didn't finish the set comprehension on line 5" ) def test_missing_arc_descriptions_for_exceptions(self): @@ -340,7 +340,7 @@ class ParserMissingArcDescriptionTest(CoverageTest): """) self.assertEqual( parser.missing_arc_description(2, -3), - "line 3 didn't run the lambda on line 3", + "line 3 didn't finish the lambda on line 3", ) |