summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/config.py47
-rw-r--r--coverage/control.py4
-rw-r--r--coverage/misc.py8
-rw-r--r--coverage/parser.py24
-rw-r--r--coverage/results.py21
-rw-r--r--test/test_arcs.py10
-rw-r--r--test/test_config.py12
7 files changed, 103 insertions, 23 deletions
diff --git a/coverage/config.py b/coverage/config.py
index 6643a9cf..4507bc22 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -3,6 +3,26 @@
import os
from coverage.backward import configparser # pylint: disable=W0622
+# The default line exclusion regexes
+DEFAULT_EXCLUDE = [
+ '(?i)# *pragma[: ]*no *cover',
+ ]
+
+# The default partial branch regexes, to be modified by the user.
+DEFAULT_PARTIAL = [
+ '(?i)# *pragma[: ]*no *branch',
+ ]
+
+# The default partial branch regexes, based on Python semantics.
+# 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:',
+ ]
+
class CoverageConfig(object):
"""Coverage.py configuration.
@@ -23,10 +43,12 @@ class CoverageConfig(object):
self.source = None
# Defaults for [report]
- self.exclude_list = ['(?i)# *pragma[: ]*no *cover']
+ self.exclude_list = DEFAULT_EXCLUDE[:]
self.ignore_errors = False
- self.omit = None
self.include = None
+ self.omit = None
+ self.partial_list = DEFAULT_PARTIAL[:]
+ self.partial_always_list = DEFAULT_PARTIAL_ALWAYS[:]
self.precision = 0
# Defaults for [html]
@@ -79,15 +101,17 @@ class CoverageConfig(object):
# [report]
if cp.has_option('report', 'exclude_lines'):
- # exclude_lines is a list of lines, leave out the blank ones.
- exclude_list = cp.get('report', 'exclude_lines')
- self.exclude_list = list(filter(None, exclude_list.split('\n')))
+ self.exclude_list = self.get_line_list(cp, 'report', 'exclude_lines')
if cp.has_option('report', 'ignore_errors'):
self.ignore_errors = cp.getboolean('report', 'ignore_errors')
if cp.has_option('report', 'include'):
self.include = self.get_list(cp, 'report', 'include')
if cp.has_option('report', 'omit'):
self.omit = self.get_list(cp, 'report', 'omit')
+ if cp.has_option('report', 'partial_branches'):
+ self.partial_list = self.get_line_list(cp, 'report', 'partial_branches')
+ if cp.has_option('report', 'partial_branches_always'):
+ self.partial_always_list = self.get_line_list(cp, 'report', 'partial_branches_always')
if cp.has_option('report', 'precision'):
self.precision = cp.getint('report', 'precision')
@@ -116,3 +140,16 @@ class CoverageConfig(object):
if value:
values.append(value)
return values
+
+ def get_line_list(self, cp, section, option):
+ """Read a list of full-line strings from the ConfigParser `cp`.
+
+ The value of `section` and `option` is treated as a newline-separated
+ list of strings. Each value is stripped of whitespace.
+
+ Returns the list of strings.
+
+ """
+ value_list = cp.get(section, option)
+ return list(filter(None, value_list.split('\n')))
+
diff --git a/coverage/control.py b/coverage/control.py
index cee073e4..b71887f7 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -11,7 +11,7 @@ from coverage.data import CoverageData
from coverage.files import FileLocator, TreeMatcher, FnmatchMatcher
from coverage.files import find_python_files
from coverage.html import HtmlReporter
-from coverage.misc import CoverageException, bool_or_none
+from coverage.misc import CoverageException, bool_or_none, join_regex
from coverage.results import Analysis, Numbers
from coverage.summary import SummaryReporter
from coverage.xmlreport import XmlReporter
@@ -414,7 +414,7 @@ class coverage(object):
def _compile_exclude(self):
"""Build the internal usable form of the exclude list."""
- self.exclude_re = "(" + ")|(".join(self.config.exclude_list) + ")"
+ self.exclude_re = join_regex(self.config.exclude_list)
def get_exclude_list(self):
"""Return the list of excluded regex patterns."""
diff --git a/coverage/misc.py b/coverage/misc.py
index 4f3748fe..ec0d0ff7 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -73,6 +73,14 @@ def bool_or_none(b):
return bool(b)
+def join_regex(regexes):
+ """Combine a list of regexes into one that matches any of them."""
+ if len(regexes) > 1:
+ return "(" + ")|(".join(regexes) + ")"
+ else:
+ return regexes[0]
+
+
class Hasher(object):
"""Hashes Python data into md5."""
def __init__(self):
diff --git a/coverage/parser.py b/coverage/parser.py
index d033f6d2..cbbb5a6a 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -5,7 +5,7 @@ import glob, opcode, os, re, sys, token, tokenize
from coverage.backward import set, sorted, StringIO # pylint: disable=W0622
from coverage.backward import open_source
from coverage.bytecode import ByteCodes, CodeObjects
-from coverage.misc import nice_pair, expensive
+from coverage.misc import nice_pair, expensive, join_regex
from coverage.misc import CoverageException, NoSource, NotPython
@@ -15,7 +15,7 @@ class CodeParser(object):
def __init__(self, text=None, filename=None, exclude=None):
"""
Source can be provided as `text`, the text itself, or `filename`, from
- which text will be read. Excluded lines are those that match
+ which the text will be read. Excluded lines are those that match
`exclude`, a regex.
"""
@@ -68,6 +68,21 @@ class CodeParser(object):
return self._byte_parser
byte_parser = property(_get_byte_parser)
+ def lines_matching(self, *regexes):
+ """Find the lines matching one of a list of regexes.
+
+ Returns a set of line numbers, the lines that contain a match for one
+ of the regexes in `regexes`. The entire line needn't match, just a
+ part of it.
+
+ """
+ regex_c = re.compile(join_regex(regexes))
+ matches = set()
+ for i, ltext in enumerate(self.lines):
+ if regex_c.search(ltext):
+ matches.add(i+1)
+ return matches
+
def _raw_parse(self):
"""Parse the source to find the interesting facts about its lines.
@@ -76,10 +91,7 @@ class CodeParser(object):
"""
# Find lines which match an exclusion pattern.
if self.exclude:
- re_exclude = re.compile(self.exclude)
- for i, ltext in enumerate(self.lines):
- if re_exclude.search(ltext):
- self.excluded.add(i+1)
+ self.excluded = self.lines_matching(self.exclude)
# Tokenize, to find excluded suites, to find docstrings, and to find
# multi-line statements.
diff --git a/coverage/results.py b/coverage/results.py
index a7ec0fdc..d92b503c 100644
--- a/coverage/results.py
+++ b/coverage/results.py
@@ -3,7 +3,7 @@
import os
from coverage.backward import set, sorted # pylint: disable=W0622
-from coverage.misc import format_lines, NoSource
+from coverage.misc import format_lines, join_regex, NoSource
from coverage.parser import CodeParser
@@ -35,11 +35,16 @@ class Analysis(object):
self.missing = sorted(set(self.statements) - set(exec1))
if self.coverage.data.has_arcs():
+ self.no_branch = self.parser.lines_matching(
+ join_regex(self.coverage.config.partial_list),
+ join_regex(self.coverage.config.partial_always_list)
+ )
n_branches = self.total_branches()
mba = self.missing_branch_arcs()
n_missing_branches = sum([len(v) for v in mba.values()])
else:
n_branches = n_missing_branches = 0
+ self.no_branch = set()
self.numbers = Numbers(
n_files=1,
@@ -64,7 +69,10 @@ class Analysis(object):
def arc_possibilities(self):
"""Returns a sorted list of the arcs in the code."""
- return self.parser.arcs()
+ 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):
"""Returns a sorted list of the arcs actually executed in the code."""
@@ -89,7 +97,9 @@ class Analysis(object):
# trouble, and here is where it's the least burden to remove them.
unpredicted = [
e for e in executed
- if e not in possible and e[0] != e[1]
+ if e not in possible
+ and e[0] != e[1]
+ and e[0] not in self.no_branch
]
return sorted(unpredicted)
@@ -193,8 +203,9 @@ class Numbers(object):
def _get_pc_covered_str(self):
"""Returns the percent covered, as a string, without a percent sign.
- The important thing here is that "0" only be returned when it's truly
- zero, and "100" only be returned when it's truly 100.
+ Note that "0" is only returned when the value is truly zero, and "100"
+ is only returned when the value is truly 100. Rounding can never
+ result in either "0" or "100".
"""
pc = self.pc_covered
diff --git a/test/test_arcs.py b/test/test_arcs.py
index dafe0fbb..a406b3a9 100644
--- a/test/test_arcs.py
+++ b/test/test_arcs.py
@@ -213,15 +213,15 @@ class LoopArcTest(CoverageTest):
i += 1
assert a == 4 and i == 3
""",
- arcz=".1 12 23 34 45 36 63 57 27 7.",
- arcz_missing="27" # while loop never exits naturally.
+ arcz=".1 12 34 45 36 63 57 7.",
+ #arcz_missing="27" # while loop never exits naturally.
)
# With "while True", 2.x thinks it's computation, 3.x thinks it's
# constant.
if sys.version_info >= (3, 0):
- arcz = ".1 12 23 34 45 36 63 57 27 7."
+ arcz = ".1 12 34 45 36 63 57 7."
else:
- arcz = ".1 12 23 34 45 36 62 57 27 7."
+ arcz = ".1 12 34 45 36 62 57 7."
self.check_coverage("""\
a, i = 1, 0
while True:
@@ -232,7 +232,7 @@ class LoopArcTest(CoverageTest):
assert a == 4 and i == 3
""",
arcz=arcz,
- arcz_missing="27" # while loop never exits naturally.
+ #arcz_missing="27" # while loop never exits naturally.
)
def test_for_if_else_for(self):
diff --git a/test/test_config.py b/test/test_config.py
index d5290584..3c4be9b7 100644
--- a/test/test_config.py
+++ b/test/test_config.py
@@ -127,6 +127,12 @@ class ConfigFileTest(CoverageTest):
yet_more
precision = 3
+ partial_branches =
+ pragma:?\\s+no branch
+ partial_branches_always =
+ if 0:
+ while True:
+
[html]
directory = c:\\tricky\\dir.somewhere
@@ -153,6 +159,12 @@ class ConfigFileTest(CoverageTest):
)
self.assertEqual(cov.config.precision, 3)
+ self.assertEqual(cov.config.partial_list,
+ ["pragma:?\s+no branch"]
+ )
+ self.assertEqual(cov.config.partial_always_list,
+ ["if 0:", "while True:"]
+ )
self.assertEqual(cov.config.html_dir, r"c:\tricky\dir.somewhere")
self.assertEqual(cov.config.xml_output, "mycov.xml")