diff options
Diffstat (limited to 'coverage')
-rw-r--r-- | coverage/config.py | 47 | ||||
-rw-r--r-- | coverage/control.py | 4 | ||||
-rw-r--r-- | coverage/misc.py | 8 | ||||
-rw-r--r-- | coverage/parser.py | 24 | ||||
-rw-r--r-- | coverage/results.py | 21 |
5 files changed, 86 insertions, 18 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 |