diff options
-rw-r--r-- | coverage/html.py | 2 | ||||
-rw-r--r-- | coverage/parser.py | 53 | ||||
-rw-r--r-- | coverage/plugin.py | 10 | ||||
-rw-r--r-- | coverage/python.py | 4 | ||||
-rw-r--r-- | tests/test_parser.py | 32 |
5 files changed, 72 insertions, 29 deletions
diff --git a/coverage/html.py b/coverage/html.py index 19898cb..5b792c7 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -220,7 +220,7 @@ class HtmlReporter(Reporter): shorts.append("exit") else: shorts.append(b) - longs.append(fr.arc_destination_description(b)) + longs.append(fr.missing_arc_description(lineno, b)) # 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 3b25b94..7ffe2b9 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -79,10 +79,10 @@ class PythonParser(object): # multi-line statements. self._multiline = {} - # Lazily-created ByteParser, arc data, and arc destination descriptions. + # Lazily-created ByteParser, arc data, and missing arc descriptions. self._byte_parser = None self._all_arcs = None - self._arc_dest_descriptions = None + self._missing_arc_descriptions = None @property def byte_parser(self): @@ -274,7 +274,7 @@ class PythonParser(object): if fl1 != fl2: self._all_arcs.add((fl1, fl2)) - self._arc_dest_descriptions = aaa.arc_dest_descriptions + self._missing_arc_descriptions = aaa.missing_arc_descriptions def exit_counts(self): """Get a count of exits from that each line. @@ -303,17 +303,27 @@ class PythonParser(object): return exit_counts - def arc_destination_description(self, lineno): - if self._arc_dest_descriptions is None: + def missing_arc_description(self, start, end): + if self._missing_arc_descriptions is None: self._analyze_ast() - description = self._arc_dest_descriptions.get(lineno) + description = self._missing_arc_descriptions.get((start, end)) + #TODO: print(self._missing_arc_descriptions) if description is None: - if lineno < 0: - description = "jump to the function exit" + description = None, None + smsg, emsg = description + + if smsg is None: + smsg = "line {lineno}" + smsg = smsg.format(lineno=start) + + if emsg is None: + if end < 0: + emsg = "didn't jump to the function exit" else: - description = "jump to line {lineno}".format(lineno=lineno) - return description + emsg = "didn't jump to line {lineno}" + emsg = emsg.format(lineno=end) + return "line {start} {emsg}, because {smsg}".format(start=start, smsg=smsg, emsg=emsg) class ByteParser(object): @@ -441,8 +451,7 @@ class ArcStart(collections.namedtuple("Arc", "lineno, cause")): """ def __new__(cls, lineno, cause=None): - self = super(ArcStart, cls).__new__(cls, lineno, cause) - return self + return super(ArcStart, cls).__new__(cls, lineno, cause) # Define contract words that PyContract doesn't have. @@ -467,7 +476,7 @@ class AstArcAnalyzer(object): ast_dump(self.root_node) self.arcs = set() - self.arc_dest_descriptions = {} + self.missing_arc_descriptions = {} self.block_stack = [] def analyze(self): @@ -581,6 +590,8 @@ class AstArcAnalyzer(object): continue for prev_start in prev_starts: self.arcs.add((prev_start.lineno, lineno)) + if prev_start.cause is not None: + self.missing_arc_descriptions[(prev_start.lineno, lineno)] = (prev_start.cause, None) prev_starts = self.add_arcs(body_node) return prev_starts @@ -696,12 +707,12 @@ class AstArcAnalyzer(object): def _handle__For(self, node): start = self.line_for_node(node.iter) self.block_stack.append(LoopBlock(start=start)) - exits = self.add_body_arcs(node.body, from_start=ArcStart(start, "didn't enter the loop")) + exits = self.add_body_arcs(node.body, from_start=ArcStart(start, "the loop on line {lineno} never started")) for xit in exits: self.arcs.add((xit.lineno, start)) my_block = self.block_stack.pop() exits = my_block.break_exits - from_start = ArcStart(start, "didn't finish naturally") + from_start = ArcStart(start, "the loop on line {lineno} didn't complete") if node.orelse: else_exits = self.add_body_arcs(node.orelse, from_start=from_start) exits |= else_exits @@ -718,8 +729,8 @@ class AstArcAnalyzer(object): @contract(returns='ArcStarts') def _handle__If(self, node): start = self.line_for_node(node.test) - exits = self.add_body_arcs(node.body, from_start=ArcStart(start, "the condition wasn't true")) - exits |= self.add_body_arcs(node.orelse, from_start=ArcStart(start, "the condition wasn't false")) + exits = self.add_body_arcs(node.body, from_start=ArcStart(start, "the condition on line {lineno} was never true")) + exits |= self.add_body_arcs(node.orelse, from_start=ArcStart(start, "the condition on line {lineno} was never false")) return exits @contract(returns='ArcStarts') @@ -865,7 +876,7 @@ class AstArcAnalyzer(object): exits = self.add_body_arcs(node.body, from_start=ArcStart(-1)) for xit in exits: self.arcs.add((xit.lineno, -start)) - self.arc_dest_descriptions[-start] = 'exit the module' + self.missing_arc_descriptions[(xit.lineno, -start)] = (xit.cause, 'exit the module') else: # Empty module. self.arcs.add((-1, start)) @@ -878,7 +889,7 @@ class AstArcAnalyzer(object): self.block_stack.pop() for xit in exits: self.arcs.add((xit.lineno, -start)) - self.arc_dest_descriptions[-start] = 'exit function "{0}"'.format(node.name) + self.missing_arc_descriptions[(xit.lineno, -start)] = (xit.cause, "didn't return from function '{0}'".format(node.name)) _code_object__AsyncFunctionDef = _code_object__FunctionDef @@ -888,14 +899,14 @@ class AstArcAnalyzer(object): exits = self.add_body_arcs(node.body, from_start=ArcStart(start)) for xit in exits: self.arcs.add((xit.lineno, -start)) - self.arc_dest_descriptions[-start] = 'exit the body of class "{0}"'.format(node.name) + self.missing_arc_descriptions[(xit.lineno, -start)] = (xit.cause, 'exit the body of class "{0}"'.format(node.name)) def _make_oneline_code_method(noun): # pylint: disable=no-self-argument def _method(self, node): start = self.line_for_node(node) self.arcs.add((-1, start)) self.arcs.add((start, -start)) - self.arc_dest_descriptions[-start] = 'run the {0} on line {1}'.format(noun, start) + self.missing_arc_descriptions[(start, -start)] = (None, 'run the {0} on line {1}'.format(noun, start)) return _method _code_object__Lambda = _make_oneline_code_method("lambda") diff --git a/coverage/plugin.py b/coverage/plugin.py index 095b268..85521e3 100644 --- a/coverage/plugin.py +++ b/coverage/plugin.py @@ -329,20 +329,20 @@ class FileReporter(object): """ return {} - def arc_destination_description(self, lineno): - """Provide an English phrase describing an arc destination. + def missing_arc_description(self, start, end): + """Provide an English phrase describing a missing arc. For an arc like (123, 456), it should read well to use the phrase like this:: - "Line {0} didn't {1}".format(123, arc_destination_description(456)) + "Line {0} didn't {1}".format(123, missing_arc_description(123, 456)) TODO: say more. - By default, this simply returns the string "jump to {lineno}". + By default, this simply returns the string "jump to {end}". """ - return "jump to line {lineno}".format(lineno=lineno) + return "jump to line {end}".format(end=end) def source_token_lines(self): """Generate a series of tokenized lines, one for each line in `source`. diff --git a/coverage/python.py b/coverage/python.py index 59e6346..0d2fb3b 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 arc_destination_description(self, lineno): - return self.parser.arc_destination_description(lineno) + def missing_arc_description(self, start, end): + return self.parser.missing_arc_description(start, end) @contract(returns='unicode') def source(self): diff --git a/tests/test_parser.py b/tests/test_parser.py index 470ea15..48b70ef 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -186,6 +186,38 @@ class PythonParserTest(CoverageTest): self.assertEqual(parser.statements, set([1, 2, 3])) +class ParserMissingArcDescriptionTest(CoverageTest): + """Tests for PythonParser.missing_arc_description.""" + + run_in_temp_dir = False + + def test_missing_arc_description(self): + text = textwrap.dedent("""\ + if x: + print(2) + print(3) + + def func5(): + for x in range(6): + if x == 3: + break + """) + parser = PythonParser(text=text) + parser.parse_source() + self.assertEqual( + parser.missing_arc_description(1, 2), + "line 1 didn't jump to line 2, because the condition on line 1 was never true" + ) + self.assertEqual( + parser.missing_arc_description(1, 3), + "line 1 didn't jump to line 3, because the condition on line 1 was never false" + ) + self.assertEqual( + parser.missing_arc_description(6, -5), + "line 6 didn't return from function 'func5', because the loop on line 6 didn't complete" + ) + + class ParserFileTest(CoverageTest): """Tests for coverage.py's code parsing from files.""" |