| Index: third_party/pycoverage/coverage/results.py
|
| diff --git a/third_party/pycoverage/coverage/results.py b/third_party/pycoverage/coverage/results.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..db6df0d30b7e389319de83ebaaf11fa4c7806e22
|
| --- /dev/null
|
| +++ b/third_party/pycoverage/coverage/results.py
|
| @@ -0,0 +1,286 @@
|
| +"""Results of coverage measurement."""
|
| +
|
| +import os
|
| +
|
| +from coverage.backward import iitems, set, sorted # pylint: disable=W0622
|
| +from coverage.misc import format_lines, join_regex, NoSource
|
| +from coverage.parser import CodeParser
|
| +
|
| +
|
| +class Analysis(object):
|
| + """The results of analyzing a code unit."""
|
| +
|
| + def __init__(self, cov, code_unit):
|
| + self.coverage = cov
|
| + self.code_unit = code_unit
|
| +
|
| + self.filename = self.code_unit.filename
|
| + actual_filename, source = self.find_source(self.filename)
|
| +
|
| + self.parser = CodeParser(
|
| + text=source, filename=actual_filename,
|
| + exclude=self.coverage._exclude_regex('exclude')
|
| + )
|
| + self.statements, self.excluded = self.parser.parse_source()
|
| +
|
| + # Identify missing statements.
|
| + executed = self.coverage.data.executed_lines(self.filename)
|
| + exec1 = self.parser.first_lines(executed)
|
| + self.missing = self.statements - 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_partial_branches = sum(
|
| + [len(v) for k,v in iitems(mba) if k not in self.missing]
|
| + )
|
| + n_missing_branches = sum([len(v) for k,v in iitems(mba)])
|
| + else:
|
| + n_branches = n_partial_branches = n_missing_branches = 0
|
| + self.no_branch = set()
|
| +
|
| + self.numbers = Numbers(
|
| + n_files=1,
|
| + n_statements=len(self.statements),
|
| + n_excluded=len(self.excluded),
|
| + n_missing=len(self.missing),
|
| + n_branches=n_branches,
|
| + n_partial_branches=n_partial_branches,
|
| + n_missing_branches=n_missing_branches,
|
| + )
|
| +
|
| + def find_source(self, filename):
|
| + """Find the source for `filename`.
|
| +
|
| + Returns two values: the actual filename, and the source.
|
| +
|
| + The source returned depends on which of these cases holds:
|
| +
|
| + * The filename seems to be a non-source file: returns None
|
| +
|
| + * The filename is a source file, and actually exists: returns None.
|
| +
|
| + * The filename is a source file, and is in a zip file or egg:
|
| + returns the source.
|
| +
|
| + * The filename is a source file, but couldn't be found: raises
|
| + `NoSource`.
|
| +
|
| + """
|
| + source = None
|
| +
|
| + base, ext = os.path.splitext(filename)
|
| + TRY_EXTS = {
|
| + '.py': ['.py', '.pyw'],
|
| + '.pyw': ['.pyw'],
|
| + }
|
| + try_exts = TRY_EXTS.get(ext)
|
| + if not try_exts:
|
| + return filename, None
|
| +
|
| + for try_ext in try_exts:
|
| + try_filename = base + try_ext
|
| + if os.path.exists(try_filename):
|
| + return try_filename, None
|
| + source = self.coverage.file_locator.get_zip_data(try_filename)
|
| + if source:
|
| + return try_filename, source
|
| + raise NoSource("No source for code: '%s'" % filename)
|
| +
|
| + def missing_formatted(self):
|
| + """The missing line numbers, formatted nicely.
|
| +
|
| + Returns a string like "1-2, 5-11, 13-14".
|
| +
|
| + """
|
| + return format_lines(self.statements, self.missing)
|
| +
|
| + def has_arcs(self):
|
| + """Were arcs measured in this result?"""
|
| + return self.coverage.data.has_arcs()
|
| +
|
| + def arc_possibilities(self):
|
| + """Returns a sorted list of the arcs in the code."""
|
| + arcs = self.parser.arcs()
|
| + return arcs
|
| +
|
| + def arcs_executed(self):
|
| + """Returns a sorted list of the arcs actually executed in the code."""
|
| + executed = self.coverage.data.executed_arcs(self.filename)
|
| + m2fl = self.parser.first_line
|
| + executed = [(m2fl(l1), m2fl(l2)) for (l1,l2) in executed]
|
| + return sorted(executed)
|
| +
|
| + def arcs_missing(self):
|
| + """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
|
| + and p[0] not in self.no_branch
|
| + ]
|
| + return sorted(missing)
|
| +
|
| + def arcs_unpredicted(self):
|
| + """Returns a sorted list of the executed arcs missing from the code."""
|
| + possible = self.arc_possibilities()
|
| + executed = self.arcs_executed()
|
| + # Exclude arcs here which connect a line to itself. They can occur
|
| + # in executed data in some cases. This is where they can cause
|
| + # 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]
|
| + ]
|
| + return sorted(unpredicted)
|
| +
|
| + def branch_lines(self):
|
| + """Returns a list of line numbers that have more than one exit."""
|
| + exit_counts = self.parser.exit_counts()
|
| + return [l1 for l1,count in iitems(exit_counts) if count > 1]
|
| +
|
| + def total_branches(self):
|
| + """How many total branches are there?"""
|
| + exit_counts = self.parser.exit_counts()
|
| + return sum([count for count in exit_counts.values() if count > 1])
|
| +
|
| + def missing_branch_arcs(self):
|
| + """Return arcs that weren't executed from branch lines.
|
| +
|
| + Returns {l1:[l2a,l2b,...], ...}
|
| +
|
| + """
|
| + missing = self.arcs_missing()
|
| + branch_lines = set(self.branch_lines())
|
| + mba = {}
|
| + for l1, l2 in missing:
|
| + if l1 in branch_lines:
|
| + if l1 not in mba:
|
| + mba[l1] = []
|
| + mba[l1].append(l2)
|
| + return mba
|
| +
|
| + def branch_stats(self):
|
| + """Get stats about branches.
|
| +
|
| + Returns a dict mapping line numbers to a tuple:
|
| + (total_exits, taken_exits).
|
| + """
|
| +
|
| + exit_counts = self.parser.exit_counts()
|
| + missing_arcs = self.missing_branch_arcs()
|
| + stats = {}
|
| + for lnum in self.branch_lines():
|
| + exits = exit_counts[lnum]
|
| + try:
|
| + missing = len(missing_arcs[lnum])
|
| + except KeyError:
|
| + missing = 0
|
| + stats[lnum] = (exits, exits - missing)
|
| + return stats
|
| +
|
| +
|
| +class Numbers(object):
|
| + """The numerical results of measuring coverage.
|
| +
|
| + This holds the basic statistics from `Analysis`, and is used to roll
|
| + up statistics across files.
|
| +
|
| + """
|
| + # A global to determine the precision on coverage percentages, the number
|
| + # of decimal places.
|
| + _precision = 0
|
| + _near0 = 1.0 # These will change when _precision is changed.
|
| + _near100 = 99.0
|
| +
|
| + def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0,
|
| + n_branches=0, n_partial_branches=0, n_missing_branches=0
|
| + ):
|
| + self.n_files = n_files
|
| + self.n_statements = n_statements
|
| + self.n_excluded = n_excluded
|
| + self.n_missing = n_missing
|
| + self.n_branches = n_branches
|
| + self.n_partial_branches = n_partial_branches
|
| + self.n_missing_branches = n_missing_branches
|
| +
|
| + def set_precision(cls, precision):
|
| + """Set the number of decimal places used to report percentages."""
|
| + assert 0 <= precision < 10
|
| + cls._precision = precision
|
| + cls._near0 = 1.0 / 10**precision
|
| + cls._near100 = 100.0 - cls._near0
|
| + set_precision = classmethod(set_precision)
|
| +
|
| + def _get_n_executed(self):
|
| + """Returns the number of executed statements."""
|
| + return self.n_statements - self.n_missing
|
| + n_executed = property(_get_n_executed)
|
| +
|
| + def _get_n_executed_branches(self):
|
| + """Returns the number of executed branches."""
|
| + return self.n_branches - self.n_missing_branches
|
| + n_executed_branches = property(_get_n_executed_branches)
|
| +
|
| + def _get_pc_covered(self):
|
| + """Returns a single percentage value for coverage."""
|
| + if self.n_statements > 0:
|
| + pc_cov = (100.0 * (self.n_executed + self.n_executed_branches) /
|
| + (self.n_statements + self.n_branches))
|
| + else:
|
| + pc_cov = 100.0
|
| + return pc_cov
|
| + pc_covered = property(_get_pc_covered)
|
| +
|
| + def _get_pc_covered_str(self):
|
| + """Returns the percent covered, as a string, without a percent sign.
|
| +
|
| + 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
|
| + if 0 < pc < self._near0:
|
| + pc = self._near0
|
| + elif self._near100 < pc < 100:
|
| + pc = self._near100
|
| + else:
|
| + pc = round(pc, self._precision)
|
| + return "%.*f" % (self._precision, pc)
|
| + pc_covered_str = property(_get_pc_covered_str)
|
| +
|
| + def pc_str_width(cls):
|
| + """How many characters wide can pc_covered_str be?"""
|
| + width = 3 # "100"
|
| + if cls._precision > 0:
|
| + width += 1 + cls._precision
|
| + return width
|
| + pc_str_width = classmethod(pc_str_width)
|
| +
|
| + def __add__(self, other):
|
| + nums = Numbers()
|
| + nums.n_files = self.n_files + other.n_files
|
| + nums.n_statements = self.n_statements + other.n_statements
|
| + nums.n_excluded = self.n_excluded + other.n_excluded
|
| + nums.n_missing = self.n_missing + other.n_missing
|
| + nums.n_branches = self.n_branches + other.n_branches
|
| + nums.n_partial_branches = (
|
| + self.n_partial_branches + other.n_partial_branches
|
| + )
|
| + nums.n_missing_branches = (
|
| + self.n_missing_branches + other.n_missing_branches
|
| + )
|
| + return nums
|
| +
|
| + def __radd__(self, other):
|
| + # Implementing 0+Numbers allows us to sum() a list of Numbers.
|
| + if other == 0:
|
| + return self
|
| + return NotImplemented
|
|
|