summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/html.py3
-rw-r--r--coverage/parser.py28
-rw-r--r--coverage/plugin.py8
-rw-r--r--coverage/python.py4
-rw-r--r--tests/test_arcs.py30
-rw-r--r--tests/test_parser.py10
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",
)