diff options
-rw-r--r-- | coverage/html.py | 16 | ||||
-rw-r--r-- | coverage/htmlfiles/pyfile.html | 20 | ||||
-rw-r--r-- | coverage/parser.py | 29 | ||||
-rw-r--r-- | coverage/plugin.py | 14 | ||||
-rw-r--r-- | coverage/templite.py | 100 | ||||
-rw-r--r-- | doc/cmd.rst | 4 | ||||
-rw-r--r-- | lab/parser.py | 2 | ||||
-rw-r--r-- | tests/test_arcs.py | 20 | ||||
-rw-r--r-- | tests/test_config.py | 2 | ||||
-rw-r--r-- | tests/test_html.py | 14 | ||||
-rw-r--r-- | tests/test_parser.py | 17 | ||||
-rw-r--r-- | tests/test_templite.py | 22 |
12 files changed, 171 insertions, 89 deletions
diff --git a/coverage/html.py b/coverage/html.py index 0d6e6f96..a7a92095 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -231,7 +231,8 @@ class HtmlReporter(Reporter): else: annotate_long = "%d missed branches: %s" % ( len(longs), - ", ".join("%d) %s" % (num, ann_long) for num, ann_long in enumerate(longs, start=1)), + ", ".join("%d) %s" % (num, ann_long) + for num, ann_long in enumerate(longs, start=1)), ) elif lineno in analysis.statements: line_class.append(c_run) @@ -262,7 +263,7 @@ class HtmlReporter(Reporter): 'fr': fr, 'nums': nums, 'lines': lines, 'time_stamp': self.time_stamp, } - html = spaceless(self.source_tmpl.render(template_values)) + html = self.source_tmpl.render(template_values) html_filename = rootname + ".html" html_path = os.path.join(self.directory, html_filename) @@ -425,17 +426,6 @@ def escape(t): return t.replace("&", "&").replace("<", "<") -def spaceless(html): - """Squeeze out some annoying extra space from an HTML string. - - Nicely-formatted templates mean lots of extra space in the result. - Get rid of some. - - """ - html = re.sub(r">\s+<p ", ">\n<p ", html) - return html - - def pair(ratio): """Format a pair of numbers so JavaScript can read them in an attribute.""" return "%s %s" % ratio diff --git a/coverage/htmlfiles/pyfile.html b/coverage/htmlfiles/pyfile.html index fb8e131e..8542a467 100644 --- a/coverage/htmlfiles/pyfile.html +++ b/coverage/htmlfiles/pyfile.html @@ -71,14 +71,22 @@ <table> <tr> <td class="linenos"> - {% for line in lines %} - <p id="n{{line.number}}" class="{{line.class}}"><a href="#n{{line.number}}">{{line.number}}</a></p> - {% endfor %} +{% for line in lines -%} + <p id="n{{line.number}}" class="{{line.class}}"><a href="#n{{line.number}}">{{line.number}}</a></p> +{% endfor %} </td> <td class="text"> - {% for line in lines %} - <p id="t{{line.number}}" class="{{line.class}}">{% if line.annotate %}<span class="annotate short">{{line.annotate}}</span><span class="annotate long">{{line.annotate_long}}</span>{% endif %}{{line.html}}<span class="strut"> </span></p> - {% endfor %} +{# These are the source lines, which are very sensitive to whitespace. -#} +{# The `{ # - # }` below are comments which slurp up the following space. -#} +{% for line in lines -%} + <p id="t{{line.number}}" class="{{line.class}}">{#-#} + {% if line.annotate -%} + <span class="annotate short">{{line.annotate}}</span>{#-#} + <span class="annotate long">{{line.annotate_long}}</span>{#-#} + {% endif -%} + {{line.html}}<span class="strut"> </span>{#-#} + </p> +{% endfor %} </td> </tr> </table> diff --git a/coverage/parser.py b/coverage/parser.py index f84133d2..f34a26fb 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -305,6 +305,7 @@ class PythonParser(object): return exit_counts def missing_arc_description(self, start, end): + """Provide an English sentence describing a missing arc.""" if self._missing_arc_fragments is None: self._analyze_ast() @@ -316,6 +317,9 @@ class PythonParser(object): if emsg is None: if end < 0: + # Hmm, maybe we have a one-line callable, let's check. + if (-end, end) in self._missing_arc_fragments: + return self.missing_arc_description(-end, end) emsg = "didn't jump to the function exit" else: emsg = "didn't jump to line {lineno}" @@ -491,6 +495,7 @@ class AstArcAnalyzer(object): code_object_handler(node) def add_arc(self, start, end, smsg=None, emsg=None): + """Add an arc, including message fragments to use if it is missing.""" if self.debug: print("\nAdding arc: ({}, {}): {!r}, {!r}".format(start, end, smsg, emsg)) print(short_stack(limit=6)) @@ -672,7 +677,8 @@ class AstArcAnalyzer(object): @contract(returns='ArcStarts') def _handle__Break(self, node): here = self.line_for_node(node) - self.process_break_exits([ArcStart(here, cause="the break on line {lineno} wasn't executed")]) + break_start = ArcStart(here, cause="the break on line {lineno} wasn't executed") + self.process_break_exits([break_start]) return set() @contract(returns='ArcStarts') @@ -703,7 +709,8 @@ class AstArcAnalyzer(object): @contract(returns='ArcStarts') def _handle__Continue(self, node): here = self.line_for_node(node) - self.process_continue_exits([ArcStart(here, cause="the continue on line {lineno} wasn't executed")]) + continue_start = ArcStart(here, cause="the continue on line {lineno} wasn't executed") + self.process_continue_exits([continue_start]) return set() @contract(returns='ArcStarts') @@ -743,14 +750,16 @@ class AstArcAnalyzer(object): @contract(returns='ArcStarts') def _handle__Raise(self, node): here = self.line_for_node(node) - self.process_raise_exits([ArcStart(here, cause="the raise on line {lineno} wasn't executed")]) + raise_start = ArcStart(here, cause="the raise on line {lineno} wasn't executed") + self.process_raise_exits([raise_start]) # `raise` statement jumps away, no exits from here. return set() @contract(returns='ArcStarts') def _handle__Return(self, node): here = self.line_for_node(node) - self.process_return_exits([ArcStart(here, cause="the return on line {lineno} wasn't executed")]) + return_start = ArcStart(here, cause="the return on line {lineno} wasn't executed") + self.process_return_exits([return_start]) # `return` statement jumps away, no exits from here. return set() @@ -794,7 +803,8 @@ class AstArcAnalyzer(object): if last_handler_start is not None: self.add_arc(last_handler_start, handler_start) last_handler_start = handler_start - from_start = ArcStart(handler_start, cause="the exception caught by line {lineno} didn't happen") + from_cause = "the exception caught by line {lineno} didn't happen" + from_start = ArcStart(handler_start, cause=from_cause) handler_exits |= self.add_body_arcs(handler_node.body, from_start=from_start) if node.orelse: @@ -900,7 +910,7 @@ class AstArcAnalyzer(object): if node.body: exits = self.add_body_arcs(node.body, from_start=ArcStart(-1)) for xit in exits: - self.add_arc(xit.lineno, -start, xit.cause, 'exit the module') + self.add_arc(xit.lineno, -start, xit.cause, "didn't exit the module") else: # Empty module. self.add_arc(-1, start) @@ -922,18 +932,19 @@ class AstArcAnalyzer(object): for xit in exits: self.add_arc( xit.lineno, -start, xit.cause, - "exit the body of class '{0}'".format(node.name), + "didn't exit the body of class '{0}'".format(node.name), ) def _make_oneline_code_method(noun): # pylint: disable=no-self-argument - def _method(self, node): + """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(-1, start) self.add_arc( start, -start, None, "didn't run the {0} on line {1}".format(noun, start), ) - return _method + return _code_object__oneline_callable _code_object__Lambda = _make_oneline_code_method("lambda") _code_object__GeneratorExp = _make_oneline_code_method("generator expression") diff --git a/coverage/plugin.py b/coverage/plugin.py index 85521e34..97d9c16e 100644 --- a/coverage/plugin.py +++ b/coverage/plugin.py @@ -330,19 +330,13 @@ class FileReporter(object): return {} def missing_arc_description(self, start, end): - """Provide an English phrase describing a missing arc. + """Provide an English sentence 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, missing_arc_description(123, 456)) - - TODO: say more. - - By default, this simply returns the string "jump to {end}". + By default, this simply returns the string "Line {start} didn't jump + to {end}". """ - return "jump to line {end}".format(end=end) + return "Line {start} didn't jump to line {end}".format(start=start, end=end) def source_token_lines(self): """Generate a series of tokenized lines, one for each line in `source`. diff --git a/coverage/templite.py b/coverage/templite.py index f131f748..4c09c113 100644 --- a/coverage/templite.py +++ b/coverage/templite.py @@ -90,6 +90,9 @@ class Templite(object): {# This will be ignored #} + Any of these constructs can have a hypen at the end (`-}}`, `-%}`, `-#}`), + which will collapse the whitespace following the tag. + Construct a Templite with the template text, then use `render` against a dictionary context to create a finished string:: @@ -151,53 +154,64 @@ class Templite(object): # Split the text to form a list of tokens. tokens = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text) + squash = False + for token in tokens: - if token.startswith('{#'): - # Comment: ignore it and move on. - continue - elif token.startswith('{{'): - # An expression to evaluate. - expr = self._expr_code(token[2:-2].strip()) - buffered.append("to_str(%s)" % expr) - elif token.startswith('{%'): - # Action tag: split into words and parse further. - flush_output() - words = token[2:-2].strip().split() - if words[0] == 'if': - # An if statement: evaluate the expression to determine if. - if len(words) != 2: - self._syntax_error("Don't understand if", token) - ops_stack.append('if') - code.add_line("if %s:" % self._expr_code(words[1])) - code.indent() - elif words[0] == 'for': - # A loop: iterate over expression result. - if len(words) != 4 or words[2] != 'in': - self._syntax_error("Don't understand for", token) - ops_stack.append('for') - self._variable(words[1], self.loop_vars) - code.add_line( - "for c_%s in %s:" % ( - words[1], - self._expr_code(words[3]) + if token.startswith('{'): + start, end = 2, -2 + squash = (token[-3] == '-') + if squash: + end = -3 + + if token.startswith('{#'): + # Comment: ignore it and move on. + continue + elif token.startswith('{{'): + # An expression to evaluate. + expr = self._expr_code(token[start:end].strip()) + buffered.append("to_str(%s)" % expr) + elif token.startswith('{%'): + # Action tag: split into words and parse further. + flush_output() + + words = token[start:end].strip().split() + if words[0] == 'if': + # An if statement: evaluate the expression to determine if. + if len(words) != 2: + self._syntax_error("Don't understand if", token) + ops_stack.append('if') + code.add_line("if %s:" % self._expr_code(words[1])) + code.indent() + elif words[0] == 'for': + # A loop: iterate over expression result. + if len(words) != 4 or words[2] != 'in': + self._syntax_error("Don't understand for", token) + ops_stack.append('for') + self._variable(words[1], self.loop_vars) + code.add_line( + "for c_%s in %s:" % ( + words[1], + self._expr_code(words[3]) + ) ) - ) - code.indent() - elif words[0].startswith('end'): - # Endsomething. Pop the ops stack. - if len(words) != 1: - self._syntax_error("Don't understand end", token) - end_what = words[0][3:] - if not ops_stack: - self._syntax_error("Too many ends", token) - start_what = ops_stack.pop() - if start_what != end_what: - self._syntax_error("Mismatched end tag", end_what) - code.dedent() - else: - self._syntax_error("Don't understand tag", words[0]) + code.indent() + elif words[0].startswith('end'): + # Endsomething. Pop the ops stack. + if len(words) != 1: + self._syntax_error("Don't understand end", token) + end_what = words[0][3:] + if not ops_stack: + self._syntax_error("Too many ends", token) + start_what = ops_stack.pop() + if start_what != end_what: + self._syntax_error("Mismatched end tag", end_what) + code.dedent() + else: + self._syntax_error("Don't understand tag", words[0]) else: # Literal content. If it isn't empty, output it. + if squash: + token = token.lstrip() if token: buffered.append(repr(token)) diff --git a/doc/cmd.rst b/doc/cmd.rst index a66df82d..5f3cc539 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -113,7 +113,7 @@ Give it a value of ``greenlet``, ``eventlet``, or ``gevent``. By default, coverage.py does not measure code installed with the Python interpreter, for example, the standard library. If you want to measure that -code as well as your own, add the ``-L`` flag. +code as well as your own, add the ``-L`` (or ``--pylib``) flag. If your coverage results seem to be overlooking code that you know has been executed, try running coverage.py again with the ``--timid`` flag. This uses a @@ -168,7 +168,7 @@ Coverage.py collects execution data in a file called ".coverage". If need be, you can set a new file name with the COVERAGE_FILE environment variable. This can include a path to another directory. -By default,each run of your program starts with an empty data set. If you need +By default, each run of your program starts with an empty data set. If you need to run your program multiple times to get complete data (for example, because you need to supply disjoint options), you can accumulate data across runs with the ``-a`` flag on the **run** command. diff --git a/lab/parser.py b/lab/parser.py index 4deb93e9..0393d209 100644 --- a/lab/parser.py +++ b/lab/parser.py @@ -167,7 +167,6 @@ class ParserMain(object): that line. """ - plus_ones = set() arc_chars = collections.defaultdict(str) for lfrom, lto in sorted(arcs): @@ -178,6 +177,7 @@ class ParserMain(object): else: if lfrom == lto - 1: plus_ones.add(lfrom) + arc_chars[lfrom] += "" # ensure this line is in arc_chars continue if lfrom < lto: l1, l2 = lfrom, lto diff --git a/tests/test_arcs.py b/tests/test_arcs.py index dd7e9b37..d18faf74 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -1246,6 +1246,26 @@ class LambdaArcTest(CoverageTest): arcz_unpredicted="58", ) + def test_lambda_in_dict(self): + self.check_coverage("""\ + x = 1 + x = 2 + d = { + 4: lambda: [], + 5: lambda: [], + 6: lambda: [], + 7: lambda: [], + } + + for k, v in d.items(): # 10 + if k & 1: + v() + """, + arcz=".1 12 23 3A AB BC BA CA A. .3 3-4 3-5 3-6 3-7", + arcz_missing="3-4 3-6", + arcz_unpredicted="", + ) + class AsyncTest(CoverageTest): """Tests of the new async and await keywords in Python 3.5""" diff --git a/tests/test_config.py b/tests/test_config.py index 5667e930..2f32c525 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -231,6 +231,7 @@ class ConfigFileTest(CoverageTest): parallel = on include = a/ , b/ concurrency = thread + source = myapp plugins = plugins.a_plugin plugins.another @@ -300,6 +301,7 @@ class ConfigFileTest(CoverageTest): self.assertTrue(cov.config.cover_pylib) self.assertTrue(cov.config.parallel) self.assertEqual(cov.config.concurrency, "thread") + self.assertEqual(cov.config.source, ["myapp"]) self.assertEqual(cov.get_exclude_list(), ["if 0:", r"pragma:?\s+no cover", "another_tab"]) self.assertTrue(cov.config.ignore_errors) diff --git a/tests/test_html.py b/tests/test_html.py index 67c6d9b6..e67e4f5b 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -525,12 +525,18 @@ class HtmlGoldTests(CoverageGoldTest): '<span class="num">3</span>'), '<span class="pc_cov">70%</span>', ('<span class="annotate short">8 ↛ 11</span>' - '<span class="annotate long">line 8 didn\'t jump to line 11, because the condition on line 8 was never false</span>'), + '<span class="annotate long">line 8 didn\'t jump to line 11, ' + 'because the condition on line 8 was never false</span>'), ('<span class="annotate short">17 ↛ exit</span>' - '<span class="annotate long">line 17 didn\'t return from function \'two\', because the condition on line 17 was never false</span>'), + '<span class="annotate long">line 17 didn\'t return from function \'two\', ' + 'because the condition on line 17 was never false</span>'), ('<span class="annotate short">25 ↛ 26, ' - '25 ↛ 28</span>' - '<span class="annotate long">2 missed branches: 1) line 25 didn\'t jump to line 26, because the condition on line 25 was never true, 2) line 25 didn\'t jump to line 28, because the condition on line 25 was never false</span>'), + '25 ↛ 28</span>' + '<span class="annotate long">2 missed branches: ' + '1) line 25 didn\'t jump to line 26, ' + 'because the condition on line 25 was never true, ' + '2) line 25 didn\'t jump to line 28, ' + 'because the condition on line 25 was never false</span>'), ) contains( "out/b_branch/index.html", diff --git a/tests/test_parser.py b/tests/test_parser.py index 1be5e16c..17f81ad8 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -224,7 +224,8 @@ class ParserMissingArcDescriptionTest(CoverageTest): ) 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" + "line 6 didn't return from function 'func5', " + "because the loop on line 6 didn't complete" ) self.assertEqual( parser.missing_arc_description(6, 7), @@ -328,6 +329,20 @@ class ParserMissingArcDescriptionTest(CoverageTest): "because the return on line 12 wasn't executed" ) + def test_missing_arc_descriptions_bug460(self): + parser = self.parse_text(u"""\ + x = 1 + d = { + 3: lambda: [], + 4: lambda: [], + } + x = 6 + """) + self.assertEqual( + parser.missing_arc_description(2, -3), + "line 3 didn't run the lambda on line 3", + ) + class ParserFileTest(CoverageTest): """Tests for coverage.py's code parsing from files.""" diff --git a/tests/test_templite.py b/tests/test_templite.py index 2f9b2dbd..1df942ee 100644 --- a/tests/test_templite.py +++ b/tests/test_templite.py @@ -33,6 +33,7 @@ class TempliteTest(CoverageTest): Result defaults to None so we can shorten the calls where we expect an exception and never get to the result comparison. + """ actual = Templite(text).render(ctx or {}) # If result is None, then an exception should have prevented us getting @@ -44,6 +45,7 @@ class TempliteTest(CoverageTest): """Assert that a `TempliteSyntaxError` will happen. A context manager, and the message should be `msg`. + """ pat = "^" + re.escape(msg) + "$" return self.assertRaisesRegex(TempliteSyntaxError, pat) @@ -238,6 +240,26 @@ class TempliteTest(CoverageTest): "@a0b0c0a1b1c1a2b2c2!" ) + def test_whitespace_handling(self): + self.try_render( + "@{% for n in nums %}\n" + " {% for a in abc %}{{a}}{{n}}{% endfor %}\n" + "{% endfor %}!\n", + {'nums': [0, 1, 2], 'abc': ['a', 'b', 'c']}, + "@\n a0b0c0\n\n a1b1c1\n\n a2b2c2\n!\n" + ) + self.try_render( + "@{% for n in nums -%}\n" + " {% for a in abc -%}\n" + " {# this disappears completely -#}\n" + " {{a -}}\n" + " {{n -}}\n" + " {% endfor %}\n" + "{% endfor %}!\n", + {'nums': [0, 1, 2], 'abc': ['a', 'b', 'c']}, + "@a0b0c0\na1b1c1\na2b2c2\n!\n" + ) + def test_non_ascii(self): self.try_render( u"{{where}} ollǝɥ", |