From 90df419fb875c0bd8e1f5534a16441e0caeef1b8 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 31 May 2011 23:12:40 -0400 Subject: Better handling of the partial-branch exclusion regexes. Finishes issue #113. --- coverage/config.py | 6 +- coverage/control.py | 57 ++++++++++----- coverage/misc.py | 4 +- coverage/results.py | 11 +-- test/coveragetest.py | 7 +- test/farm/html/gold_partial/index.html | 82 +++++++++++++++++++++ test/farm/html/gold_partial/partial.html | 121 +++++++++++++++++++++++++++++++ test/farm/html/run_partial.py | 27 +++++++ test/farm/html/src/partial.py | 19 +++++ test/test_api.py | 35 ++++++++- test/test_arcs.py | 38 ++++++++-- test/test_coverage.py | 58 +++++++-------- 12 files changed, 400 insertions(+), 65 deletions(-) create mode 100644 test/farm/html/gold_partial/index.html create mode 100644 test/farm/html/gold_partial/partial.html create mode 100644 test/farm/html/run_partial.py create mode 100644 test/farm/html/src/partial.py diff --git a/coverage/config.py b/coverage/config.py index 4507bc2..f842964 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -17,10 +17,8 @@ DEFAULT_PARTIAL = [ # These are any Python branching constructs that can't actually execute all # their branches. DEFAULT_PARTIAL_ALWAYS = [ - 'while True:', - 'while 1:', - 'if 0:', - 'if 1:', + 'while (True|1|False|0):', + 'if (True|1|False|0):', ] diff --git a/coverage/control.py b/coverage/control.py index b71887f..2d96439 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -109,8 +109,10 @@ class coverage(object): self.auto_data = auto_data self.atexit_registered = False - self.exclude_re = "" - self._compile_exclude() + # _exclude_re is a dict mapping exclusion list names to compiled + # regexes. + self._exclude_re = {} + self._exclude_regex_stale() self.file_locator = FileLocator() @@ -395,30 +397,49 @@ class coverage(object): self.collector.reset() self.data.erase() - def clear_exclude(self): + def clear_exclude(self, which='exclude'): """Clear the exclude list.""" - self.config.exclude_list = [] - self.exclude_re = "" + setattr(self.config, which + "_list", []) + self._exclude_regex_stale() - def exclude(self, regex): + def exclude(self, regex, which='exclude'): """Exclude source lines from execution consideration. - `regex` is a regular expression. Lines matching this expression are - not considered executable when reporting code coverage. A list of - regexes is maintained; this function adds a new regex to the list. - Matching any of the regexes excludes a source line. + A number of lists of regular expressions are maintained. Each list + selects lines that are treated differently during reporting. + + `which` determines which list is modified. The "exclude" list selects + lines that are not considered executable at all. The "partial" list + indicates lines with branches that are not taken. + + `regex` is a regular expression. The regex is added to the specified + list. If any of the regexes in the list is found in a line, the line + is marked for special treatment during reporting. """ - self.config.exclude_list.append(regex) - self._compile_exclude() + excl_list = getattr(self.config, which + "_list") + excl_list.append(regex) + self._exclude_regex_stale() + + def _exclude_regex_stale(self): + """Drop all the compiled exclusion regexes, a list was modified.""" + self._exclude_re.clear() - def _compile_exclude(self): - """Build the internal usable form of the exclude list.""" - self.exclude_re = join_regex(self.config.exclude_list) + def _exclude_regex(self, which): + """Return a compiled regex for the given exclusion list.""" + if which not in self._exclude_re: + excl_list = getattr(self.config, which + "_list") + self._exclude_re[which] = join_regex(excl_list) + return self._exclude_re[which] - def get_exclude_list(self): - """Return the list of excluded regex patterns.""" - return self.config.exclude_list + def get_exclude_list(self, which='exclude'): + """Return a list of excluded regex patterns. + + `which` indicates which list is desired. See `exclude` for the lists + that are available, and their meaning. + + """ + return getattr(self.config, which + "_list") def save(self): """Save the collected coverage data to the data file.""" diff --git a/coverage/misc.py b/coverage/misc.py index ec0d0ff..fd9be85 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -77,8 +77,10 @@ def join_regex(regexes): """Combine a list of regexes into one that matches any of them.""" if len(regexes) > 1: return "(" + ")|(".join(regexes) + ")" - else: + elif regexes: return regexes[0] + else: + return "" class Hasher(object): diff --git a/coverage/results.py b/coverage/results.py index 7b032f1..adfb8f4 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -25,7 +25,7 @@ class Analysis(object): self.parser = CodeParser( text=source, filename=self.filename, - exclude=self.coverage.exclude_re + exclude=self.coverage._exclude_regex('exclude') ) self.statements, self.excluded = self.parser.parse_source() @@ -70,8 +70,6 @@ class Analysis(object): def arc_possibilities(self): """Returns a sorted list of the arcs in the code.""" arcs = self.parser.arcs() - if self.no_branch: - arcs = [(a,b) for (a,b) in arcs if a not in self.no_branch] return arcs def arcs_executed(self): @@ -85,7 +83,11 @@ class Analysis(object): """Returns a sorted list of the arcs in the code not executed.""" possible = self.arc_possibilities() executed = self.arcs_executed() - missing = [p for p in possible if p not in executed] + missing = [ + p for p in possible + if p not in executed + and p[0] not in self.no_branch + ] return sorted(missing) def arcs_unpredicted(self): @@ -99,7 +101,6 @@ class Analysis(object): e for e in executed if e not in possible and e[0] != e[1] - and e[0] not in self.no_branch ] return sorted(unpredicted) diff --git a/test/coveragetest.py b/test/coveragetest.py index 93cffa8..c227659 100644 --- a/test/coveragetest.py +++ b/test/coveragetest.py @@ -251,8 +251,9 @@ class CoverageTest(TestCase): s2 = "\n".join([repr(a) for a in a2]) + "\n" self.assertMultiLineEqual(s1, s2, msg) - def check_coverage(self, text, lines=None, missing="", excludes=None, - report="", arcz=None, arcz_missing="", arcz_unpredicted=""): + def check_coverage(self, text, lines=None, missing="", report="", + excludes=None, partials="", + arcz=None, arcz_missing="", arcz_unpredicted=""): """Check the coverage measurement of `text`. The source `text` is run and measured. `lines` are the line numbers @@ -285,6 +286,8 @@ class CoverageTest(TestCase): cov.erase() for exc in excludes or []: cov.exclude(exc) + for par in partials or []: + cov.exclude(par, which='partial') cov.start() try: # pragma: recursive coverage diff --git a/test/farm/html/gold_partial/index.html b/test/farm/html/gold_partial/index.html new file mode 100644 index 0000000..fe94534 --- /dev/null +++ b/test/farm/html/gold_partial/index.html @@ -0,0 +1,82 @@ + + + + + Coverage report + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedbranchespartialcoverage
Total80060100%
partial80060100%
+
+ + + + + diff --git a/test/farm/html/gold_partial/partial.html b/test/farm/html/gold_partial/partial.html new file mode 100644 index 0000000..b9640ce --- /dev/null +++ b/test/farm/html/gold_partial/partial.html @@ -0,0 +1,121 @@ + + + + + + + + Coverage for partial: 100% + + + + + + + + + + + +
+ +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+ +
+ + + + + +
+

1

+

2

+

3

+

4

+

5

+

6

+

7

+

8

+

9

+

10

+

11

+

12

+

13

+

14

+

15

+

16

+

17

+

18

+

19

+ +
+

# partial branches 

+

 

+

a = 3 

+

 

+

while True: 

+

    break 

+

 

+

while 1: 

+

    break 

+

 

+

while a:        # pragma: no branch 

+

    break 

+

 

+

if 0: 

+

    never_happen() 

+

 

+

if 1: 

+

    a = 13 

+

 

+ +
+
+ + + + + diff --git a/test/farm/html/run_partial.py b/test/farm/html/run_partial.py new file mode 100644 index 0000000..3fb621a --- /dev/null +++ b/test/farm/html/run_partial.py @@ -0,0 +1,27 @@ +def html_it(): + """Run coverage and make an HTML report for partial.""" + import coverage + cov = coverage.coverage(branch=True) + cov.start() + import partial + cov.stop() + cov.html_report(partial, directory="../html_partial") + +runfunc(html_it, rundir="src") + +# HTML files will change often. Check that the sizes are reasonable, +# and check that certain key strings are in the output. +compare("gold_partial", "html_partial", size_within=10, file_pattern="*.html") +contains("html_partial/partial.html", + "

", + "

", + "

", + # The "if 0" and "if 1" statements are optimized away. + "

", + ) +contains("html_partial/index.html", + "partial", + "100%" + ) + +clean("html_partial") diff --git a/test/farm/html/src/partial.py b/test/farm/html/src/partial.py new file mode 100644 index 0000000..9126844 --- /dev/null +++ b/test/farm/html/src/partial.py @@ -0,0 +1,19 @@ +# partial branches + +a = 3 + +while True: + break + +while 1: + break + +while a: # pragma: no branch + break + +if 0: + never_happen() + +if 1: + a = 13 + diff --git a/test/test_api.py b/test/test_api.py index 0a0aabf..ae31404 100644 --- a/test/test_api.py +++ b/test/test_api.py @@ -220,10 +220,43 @@ class ApiTest(CoverageTest): self.assertEqual(cov.get_exclude_list(), ["foo"]) cov.exclude("bar") self.assertEqual(cov.get_exclude_list(), ["foo", "bar"]) - self.assertEqual(cov.exclude_re, "(foo)|(bar)") + self.assertEqual(cov._exclude_regex('exclude'), "(foo)|(bar)") cov.clear_exclude() self.assertEqual(cov.get_exclude_list(), []) + def test_exclude_partial_list(self): + cov = coverage.coverage() + cov.clear_exclude(which='partial') + self.assertEqual(cov.get_exclude_list(which='partial'), []) + cov.exclude("foo", which='partial') + self.assertEqual(cov.get_exclude_list(which='partial'), ["foo"]) + cov.exclude("bar", which='partial') + self.assertEqual(cov.get_exclude_list(which='partial'), ["foo", "bar"]) + self.assertEqual(cov._exclude_regex(which='partial'), "(foo)|(bar)") + cov.clear_exclude(which='partial') + self.assertEqual(cov.get_exclude_list(which='partial'), []) + + def test_exclude_and_partial_are_separate_lists(self): + cov = coverage.coverage() + cov.clear_exclude(which='partial') + cov.clear_exclude(which='exclude') + cov.exclude("foo", which='partial') + self.assertEqual(cov.get_exclude_list(which='partial'), ['foo']) + self.assertEqual(cov.get_exclude_list(which='exclude'), []) + cov.exclude("bar", which='exclude') + self.assertEqual(cov.get_exclude_list(which='partial'), ['foo']) + self.assertEqual(cov.get_exclude_list(which='exclude'), ['bar']) + cov.exclude("p2", which='partial') + cov.exclude("e2", which='exclude') + self.assertEqual(cov.get_exclude_list(which='partial'), ['foo', 'p2']) + self.assertEqual(cov.get_exclude_list(which='exclude'), ['bar', 'e2']) + cov.clear_exclude(which='partial') + self.assertEqual(cov.get_exclude_list(which='partial'), []) + self.assertEqual(cov.get_exclude_list(which='exclude'), ['bar', 'e2']) + cov.clear_exclude(which='exclude') + self.assertEqual(cov.get_exclude_list(which='partial'), []) + self.assertEqual(cov.get_exclude_list(which='exclude'), []) + def test_datafile_default(self): # Default data file behavior: it's .coverage self.make_file("datatest1.py", """\ diff --git a/test/test_arcs.py b/test/test_arcs.py index a406b3a..0c16929 100644 --- a/test/test_arcs.py +++ b/test/test_arcs.py @@ -213,15 +213,14 @@ class LoopArcTest(CoverageTest): i += 1 assert a == 4 and i == 3 """, - arcz=".1 12 34 45 36 63 57 7.", - #arcz_missing="27" # while loop never exits naturally. + arcz=".1 12 23 27 34 45 36 63 57 7.", ) # With "while True", 2.x thinks it's computation, 3.x thinks it's # constant. if sys.version_info >= (3, 0): - arcz = ".1 12 34 45 36 63 57 7." + arcz = ".1 12 23 27 34 45 36 63 57 7." else: - arcz = ".1 12 34 45 36 62 57 7." + arcz = ".1 12 23 27 34 45 36 62 57 7." self.check_coverage("""\ a, i = 1, 0 while True: @@ -232,7 +231,6 @@ class LoopArcTest(CoverageTest): assert a == 4 and i == 3 """, arcz=arcz, - #arcz_missing="27" # while loop never exits naturally. ) def test_for_if_else_for(self): @@ -481,3 +479,33 @@ class MiscArcTest(CoverageTest): assert d """, arcz=".1 19 9.") + + +class ExcludeTest(CoverageTest): + """Tests of exclusions to indicate known partial branches.""" + + def test_default(self): + # A number of forms of pragma comment are accepted. + self.check_coverage("""\ + a = 1 + if a: #pragma: no branch + b = 3 + c = 4 + if c: # pragma NOBRANCH + d = 6 + e = 7 + """, + [1,2,3,4,5,6,7], + arcz=".1 12 23 24 34 45 56 57 67 7.", arcz_missing="") + + def test_custom_pragmas(self): + self.check_coverage("""\ + a = 1 + while a: # [only some] + c = 3 + break + assert c == 5-2 + """, + [1,2,3,4,5], + partials=["only some"], + arcz=".1 12 23 34 45 25 5.", arcz_missing="") diff --git a/test/test_coverage.py b/test/test_coverage.py index fe81da7..7c12b3a 100644 --- a/test/test_coverage.py +++ b/test/test_coverage.py @@ -1155,7 +1155,7 @@ class ExcludeTest(CoverageTest): if 0: a = 4 # -cc """, - [1,3], "", ['-cc']) + [1,3], "", excludes=['-cc']) def test_two_excludes(self): self.check_coverage("""\ @@ -1167,7 +1167,7 @@ class ExcludeTest(CoverageTest): c = 6 # -xx assert a == 1 and b == 2 """, - [1,3,5,7], "5", ['-cc', '-xx']) + [1,3,5,7], "5", excludes=['-cc', '-xx']) def test_excluding_if_suite(self): self.check_coverage("""\ @@ -1179,7 +1179,7 @@ class ExcludeTest(CoverageTest): c = 6 assert a == 1 and b == 2 """, - [1,7], "", ['if 0:']) + [1,7], "", excludes=['if 0:']) def test_excluding_if_but_not_else_suite(self): self.check_coverage("""\ @@ -1194,7 +1194,7 @@ class ExcludeTest(CoverageTest): b = 9 assert a == 8 and b == 9 """, - [1,8,9,10], "", ['if 0:']) + [1,8,9,10], "", excludes=['if 0:']) def test_excluding_else_suite(self): self.check_coverage("""\ @@ -1209,7 +1209,7 @@ class ExcludeTest(CoverageTest): b = 9 assert a == 4 and b == 5 and c == 6 """, - [1,3,4,5,6,10], "", ['#pragma: NO COVER']) + [1,3,4,5,6,10], "", excludes=['#pragma: NO COVER']) self.check_coverage("""\ a = 1; b = 2 @@ -1229,7 +1229,7 @@ class ExcludeTest(CoverageTest): b = 9 assert a == 4 and b == 5 and c == 6 """, - [1,3,4,5,6,17], "", ['#pragma: NO COVER']) + [1,3,4,5,6,17], "", excludes=['#pragma: NO COVER']) def test_excluding_elif_suites(self): self.check_coverage("""\ @@ -1247,7 +1247,7 @@ class ExcludeTest(CoverageTest): b = 12 assert a == 4 and b == 5 and c == 6 """, - [1,3,4,5,6,11,12,13], "11-12", ['#pragma: NO COVER']) + [1,3,4,5,6,11,12,13], "11-12", excludes=['#pragma: NO COVER']) def test_excluding_oneline_if(self): self.check_coverage("""\ @@ -1258,7 +1258,7 @@ class ExcludeTest(CoverageTest): foo() """, - [1,2,4,6], "", ["no cover"]) + [1,2,4,6], "", excludes=["no cover"]) def test_excluding_a_colon_not_a_suite(self): self.check_coverage("""\ @@ -1269,7 +1269,7 @@ class ExcludeTest(CoverageTest): foo() """, - [1,2,4,6], "", ["no cover"]) + [1,2,4,6], "", excludes=["no cover"]) def test_excluding_for_suite(self): self.check_coverage("""\ @@ -1278,7 +1278,7 @@ class ExcludeTest(CoverageTest): a += i assert a == 15 """, - [1,4], "", ['#pragma: NO COVER']) + [1,4], "", excludes=['#pragma: NO COVER']) self.check_coverage("""\ a = 0 for i in [1, @@ -1287,7 +1287,7 @@ class ExcludeTest(CoverageTest): a += i assert a == 15 """, - [1,6], "", ['#pragma: NO COVER']) + [1,6], "", excludes=['#pragma: NO COVER']) self.check_coverage("""\ a = 0 for i in [1,2,3,4,5 @@ -1297,7 +1297,7 @@ class ExcludeTest(CoverageTest): a = 99 assert a == 1 """, - [1,7], "", ['#pragma: NO COVER']) + [1,7], "", excludes=['#pragma: NO COVER']) def test_excluding_for_else(self): self.check_coverage("""\ @@ -1310,7 +1310,7 @@ class ExcludeTest(CoverageTest): a = 123 assert a == 1 """, - [1,2,3,4,5,8], "5", ['#pragma: NO COVER']) + [1,2,3,4,5,8], "5", excludes=['#pragma: NO COVER']) def test_excluding_while(self): self.check_coverage("""\ @@ -1321,7 +1321,7 @@ class ExcludeTest(CoverageTest): b = 99 assert a == 3 and b == 0 """, - [1,6], "", ['#pragma: NO COVER']) + [1,6], "", excludes=['#pragma: NO COVER']) self.check_coverage("""\ a = 3; b = 0 while ( @@ -1332,7 +1332,7 @@ class ExcludeTest(CoverageTest): b = 99 assert a == 3 and b == 0 """, - [1,8], "", ['#pragma: NO COVER']) + [1,8], "", excludes=['#pragma: NO COVER']) def test_excluding_while_else(self): self.check_coverage("""\ @@ -1345,7 +1345,7 @@ class ExcludeTest(CoverageTest): b = 123 assert a == 3 and b == 1 """, - [1,2,3,4,5,8], "5", ['#pragma: NO COVER']) + [1,2,3,4,5,8], "5", excludes=['#pragma: NO COVER']) def test_excluding_try_except(self): self.check_coverage("""\ @@ -1356,7 +1356,7 @@ class ExcludeTest(CoverageTest): a = 99 assert a == 1 """, - [1,2,3,6], "", ['#pragma: NO COVER']) + [1,2,3,6], "", excludes=['#pragma: NO COVER']) self.check_coverage("""\ a = 0 try: @@ -1366,7 +1366,7 @@ class ExcludeTest(CoverageTest): a = 99 assert a == 99 """, - [1,2,3,4,5,6,7], "", ['#pragma: NO COVER']) + [1,2,3,4,5,6,7], "", excludes=['#pragma: NO COVER']) self.check_coverage("""\ a = 0 try: @@ -1378,7 +1378,7 @@ class ExcludeTest(CoverageTest): a = 123 assert a == 123 """, - [1,2,3,4,7,8,9], "", ['#pragma: NO COVER']) + [1,2,3,4,7,8,9], "", excludes=['#pragma: NO COVER']) self.check_coverage("""\ a = 0 try: @@ -1389,7 +1389,7 @@ class ExcludeTest(CoverageTest): a = 123 assert a == 123 """, - [1,2,3,7,8], "", ['#pragma: NO COVER']) + [1,2,3,7,8], "", excludes=['#pragma: NO COVER']) self.check_coverage("""\ a = 0 try: @@ -1401,7 +1401,7 @@ class ExcludeTest(CoverageTest): a = 123 assert a == 99 """, - [1,2,3,4,5,6,9], "", ['#pragma: NO COVER']) + [1,2,3,4,5,6,9], "", excludes=['#pragma: NO COVER']) def test_excluding_try_except_pass(self): self.check_coverage("""\ @@ -1412,7 +1412,7 @@ class ExcludeTest(CoverageTest): x = 2 assert a == 1 """, - [1,2,3,6], "", ['#pragma: NO COVER']) + [1,2,3,6], "", excludes=['#pragma: NO COVER']) self.check_coverage("""\ a = 0 try: @@ -1424,7 +1424,7 @@ class ExcludeTest(CoverageTest): a = 123 assert a == 123 """, - [1,2,3,4,7,8,9], "", ['#pragma: NO COVER']) + [1,2,3,4,7,8,9], "", excludes=['#pragma: NO COVER']) self.check_coverage("""\ a = 0 try: @@ -1435,7 +1435,7 @@ class ExcludeTest(CoverageTest): a = 123 assert a == 123 """, - [1,2,3,7,8], "", ['#pragma: NO COVER']) + [1,2,3,7,8], "", excludes=['#pragma: NO COVER']) self.check_coverage("""\ a = 0 try: @@ -1447,7 +1447,7 @@ class ExcludeTest(CoverageTest): x = 2 assert a == 99 """, - [1,2,3,4,5,6,9], "", ['#pragma: NO COVER']) + [1,2,3,4,5,6,9], "", excludes=['#pragma: NO COVER']) def test_excluding_if_pass(self): # From a comment on the coverage page by Michael McNeil Forbes: @@ -1460,7 +1460,7 @@ class ExcludeTest(CoverageTest): f() """, - [1,7], "", ["no cover"]) + [1,7], "", excludes=["no cover"]) def test_excluding_function(self): self.check_coverage("""\ @@ -1472,7 +1472,7 @@ class ExcludeTest(CoverageTest): x = 1 assert x == 1 """, - [6,7], "", ['#pragma: NO COVER']) + [6,7], "", excludes=['#pragma: NO COVER']) def test_excluding_method(self): self.check_coverage("""\ @@ -1486,7 +1486,7 @@ class ExcludeTest(CoverageTest): x = Fooey() assert x.a == 1 """, - [1,2,3,8,9], "", ['#pragma: NO COVER']) + [1,2,3,8,9], "", excludes=['#pragma: NO COVER']) def test_excluding_class(self): self.check_coverage("""\ @@ -1500,7 +1500,7 @@ class ExcludeTest(CoverageTest): x = 1 assert x == 1 """, - [8,9], "", ['#pragma: NO COVER']) + [8,9], "", excludes=['#pragma: NO COVER']) if sys.version_info >= (2, 4): -- cgit v1.2.1