123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
- # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
- """Results of coverage measurement."""
- import collections
- from coverage.backward import iitems
- from coverage.misc import format_lines, SimpleRepr
- class Analysis(object):
- """The results of analyzing a FileReporter."""
- def __init__(self, data, file_reporter):
- self.data = data
- self.file_reporter = file_reporter
- self.filename = self.file_reporter.filename
- self.statements = self.file_reporter.lines()
- self.excluded = self.file_reporter.excluded_lines()
- # Identify missing statements.
- executed = self.data.lines(self.filename) or []
- executed = self.file_reporter.translate_lines(executed)
- self.missing = self.statements - executed
- if self.data.has_arcs():
- self._arc_possibilities = sorted(self.file_reporter.arcs())
- self.exit_counts = self.file_reporter.exit_counts()
- self.no_branch = self.file_reporter.no_branch_lines()
- 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:
- self._arc_possibilities = []
- self.exit_counts = {}
- self.no_branch = set()
- n_branches = n_partial_branches = n_missing_branches = 0
- 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 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.data.has_arcs()
- def arc_possibilities(self):
- """Returns a sorted list of the arcs in the code."""
- return self._arc_possibilities
- def arcs_executed(self):
- """Returns a sorted list of the arcs actually executed in the code."""
- executed = self.data.arcs(self.filename) or []
- executed = self.file_reporter.translate_arcs(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_missing_formatted(self):
- """The missing branch arcs, formatted nicely.
- Returns a string like "1->2, 1->3, 16->20". Omits any mention of
- branches from missing lines, so if line 17 is missing, then 17->18
- won't be included.
- """
- arcs = self.missing_branch_arcs()
- missing = self.missing
- line_exits = sorted(iitems(arcs))
- pairs = []
- for line, exits in line_exits:
- for ex in sorted(exits):
- if line not in missing:
- pairs.append("%d->%s" % (line, (ex if ex > 0 else "exit")))
- return ', '.join(pairs)
- 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.
- # Also, generators can somehow cause arcs from "enter" to "exit", so
- # make sure we have at least one positive value.
- unpredicted = (
- e for e in executed
- if e not in possible
- and e[0] != e[1]
- and (e[0] > 0 or e[1] > 0)
- )
- return sorted(unpredicted)
- def branch_lines(self):
- """Returns a list of line numbers that have more than one exit."""
- return [l1 for l1,count in iitems(self.exit_counts) if count > 1]
- def total_branches(self):
- """How many total branches are there?"""
- return sum(count for count in self.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 = collections.defaultdict(list)
- for l1, l2 in missing:
- if l1 in branch_lines:
- 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).
- """
- missing_arcs = self.missing_branch_arcs()
- stats = {}
- for lnum in self.branch_lines():
- exits = self.exit_counts[lnum]
- try:
- missing = len(missing_arcs[lnum])
- except KeyError:
- missing = 0
- stats[lnum] = (exits, exits - missing)
- return stats
- class Numbers(SimpleRepr):
- """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 init_args(self):
- """Return a list for __init__(*args) to recreate this object."""
- return [
- self.n_files, self.n_statements, self.n_excluded, self.n_missing,
- self.n_branches, self.n_partial_branches, self.n_missing_branches,
- ]
- @classmethod
- 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
- @property
- def n_executed(self):
- """Returns the number of executed statements."""
- return self.n_statements - self.n_missing
- @property
- def n_executed_branches(self):
- """Returns the number of executed branches."""
- return self.n_branches - self.n_missing_branches
- @property
- def pc_covered(self):
- """Returns a single percentage value for coverage."""
- if self.n_statements > 0:
- numerator, denominator = self.ratio_covered
- pc_cov = (100.0 * numerator) / denominator
- else:
- pc_cov = 100.0
- return pc_cov
- @property
- def 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)
- @classmethod
- 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
- @property
- def ratio_covered(self):
- """Return a numerator and denominator for the coverage ratio."""
- numerator = self.n_executed + self.n_executed_branches
- denominator = self.n_statements + self.n_branches
- return numerator, denominator
- 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
|