summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/html.py2
-rw-r--r--coverage/parser.py53
-rw-r--r--coverage/plugin.py10
-rw-r--r--coverage/python.py4
-rw-r--r--tests/test_parser.py32
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."""