Home | History | Annotate | Download | only in coverage
      1 """Results of coverage measurement."""
      2 
      3 import os
      4 
      5 from coverage.backward import set, sorted           # pylint: disable=W0622
      6 from coverage.misc import format_lines, join_regex, NoSource
      7 from coverage.parser import CodeParser
      8 
      9 
     10 class Analysis(object):
     11     """The results of analyzing a code unit."""
     12 
     13     def __init__(self, cov, code_unit):
     14         self.coverage = cov
     15         self.code_unit = code_unit
     16 
     17         self.filename = self.code_unit.filename
     18         ext = os.path.splitext(self.filename)[1]
     19         source = None
     20         if ext == '.py':
     21             if not os.path.exists(self.filename):
     22                 source = self.coverage.file_locator.get_zip_data(self.filename)
     23                 if not source:
     24                     raise NoSource("No source for code: %r" % self.filename)
     25 
     26         self.parser = CodeParser(
     27             text=source, filename=self.filename,
     28             exclude=self.coverage._exclude_regex('exclude')
     29             )
     30         self.statements, self.excluded = self.parser.parse_source()
     31 
     32         # Identify missing statements.
     33         executed = self.coverage.data.executed_lines(self.filename)
     34         exec1 = self.parser.first_lines(executed)
     35         self.missing = sorted(set(self.statements) - set(exec1))
     36 
     37         if self.coverage.data.has_arcs():
     38             self.no_branch = self.parser.lines_matching(
     39                 join_regex(self.coverage.config.partial_list),
     40                 join_regex(self.coverage.config.partial_always_list)
     41                 )
     42             n_branches = self.total_branches()
     43             mba = self.missing_branch_arcs()
     44             n_missing_branches = sum(
     45                 [len(v) for k,v in mba.items() if k not in self.missing]
     46                 )
     47         else:
     48             n_branches = n_missing_branches = 0
     49             self.no_branch = set()
     50 
     51         self.numbers = Numbers(
     52             n_files=1,
     53             n_statements=len(self.statements),
     54             n_excluded=len(self.excluded),
     55             n_missing=len(self.missing),
     56             n_branches=n_branches,
     57             n_missing_branches=n_missing_branches,
     58             )
     59 
     60     def missing_formatted(self):
     61         """The missing line numbers, formatted nicely.
     62 
     63         Returns a string like "1-2, 5-11, 13-14".
     64 
     65         """
     66         return format_lines(self.statements, self.missing)
     67 
     68     def has_arcs(self):
     69         """Were arcs measured in this result?"""
     70         return self.coverage.data.has_arcs()
     71 
     72     def arc_possibilities(self):
     73         """Returns a sorted list of the arcs in the code."""
     74         arcs = self.parser.arcs()
     75         return arcs
     76 
     77     def arcs_executed(self):
     78         """Returns a sorted list of the arcs actually executed in the code."""
     79         executed = self.coverage.data.executed_arcs(self.filename)
     80         m2fl = self.parser.first_line
     81         executed = [(m2fl(l1), m2fl(l2)) for (l1,l2) in executed]
     82         return sorted(executed)
     83 
     84     def arcs_missing(self):
     85         """Returns a sorted list of the arcs in the code not executed."""
     86         possible = self.arc_possibilities()
     87         executed = self.arcs_executed()
     88         missing = [
     89             p for p in possible
     90                 if p not in executed
     91                     and p[0] not in self.no_branch
     92             ]
     93         return sorted(missing)
     94 
     95     def arcs_unpredicted(self):
     96         """Returns a sorted list of the executed arcs missing from the code."""
     97         possible = self.arc_possibilities()
     98         executed = self.arcs_executed()
     99         # Exclude arcs here which connect a line to itself.  They can occur
    100         # in executed data in some cases.  This is where they can cause
    101         # trouble, and here is where it's the least burden to remove them.
    102         unpredicted = [
    103             e for e in executed
    104                 if e not in possible
    105                     and e[0] != e[1]
    106             ]
    107         return sorted(unpredicted)
    108 
    109     def branch_lines(self):
    110         """Returns a list of line numbers that have more than one exit."""
    111         exit_counts = self.parser.exit_counts()
    112         return [l1 for l1,count in exit_counts.items() if count > 1]
    113 
    114     def total_branches(self):
    115         """How many total branches are there?"""
    116         exit_counts = self.parser.exit_counts()
    117         return sum([count for count in exit_counts.values() if count > 1])
    118 
    119     def missing_branch_arcs(self):
    120         """Return arcs that weren't executed from branch lines.
    121 
    122         Returns {l1:[l2a,l2b,...], ...}
    123 
    124         """
    125         missing = self.arcs_missing()
    126         branch_lines = set(self.branch_lines())
    127         mba = {}
    128         for l1, l2 in missing:
    129             if l1 in branch_lines:
    130                 if l1 not in mba:
    131                     mba[l1] = []
    132                 mba[l1].append(l2)
    133         return mba
    134 
    135     def branch_stats(self):
    136         """Get stats about branches.
    137 
    138         Returns a dict mapping line numbers to a tuple:
    139         (total_exits, taken_exits).
    140         """
    141 
    142         exit_counts = self.parser.exit_counts()
    143         missing_arcs = self.missing_branch_arcs()
    144         stats = {}
    145         for lnum in self.branch_lines():
    146             exits = exit_counts[lnum]
    147             try:
    148                 missing = len(missing_arcs[lnum])
    149             except KeyError:
    150                 missing = 0
    151             stats[lnum] = (exits, exits - missing)
    152         return stats
    153 
    154 
    155 class Numbers(object):
    156     """The numerical results of measuring coverage.
    157 
    158     This holds the basic statistics from `Analysis`, and is used to roll
    159     up statistics across files.
    160 
    161     """
    162     # A global to determine the precision on coverage percentages, the number
    163     # of decimal places.
    164     _precision = 0
    165     _near0 = 1.0              # These will change when _precision is changed.
    166     _near100 = 99.0
    167 
    168     def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0,
    169                     n_branches=0, n_missing_branches=0
    170                     ):
    171         self.n_files = n_files
    172         self.n_statements = n_statements
    173         self.n_excluded = n_excluded
    174         self.n_missing = n_missing
    175         self.n_branches = n_branches
    176         self.n_missing_branches = n_missing_branches
    177 
    178     def set_precision(cls, precision):
    179         """Set the number of decimal places used to report percentages."""
    180         assert 0 <= precision < 10
    181         cls._precision = precision
    182         cls._near0 = 1.0 / 10**precision
    183         cls._near100 = 100.0 - cls._near0
    184     set_precision = classmethod(set_precision)
    185 
    186     def _get_n_executed(self):
    187         """Returns the number of executed statements."""
    188         return self.n_statements - self.n_missing
    189     n_executed = property(_get_n_executed)
    190 
    191     def _get_n_executed_branches(self):
    192         """Returns the number of executed branches."""
    193         return self.n_branches - self.n_missing_branches
    194     n_executed_branches = property(_get_n_executed_branches)
    195 
    196     def _get_pc_covered(self):
    197         """Returns a single percentage value for coverage."""
    198         if self.n_statements > 0:
    199             pc_cov = (100.0 * (self.n_executed + self.n_executed_branches) /
    200                         (self.n_statements + self.n_branches))
    201         else:
    202             pc_cov = 100.0
    203         return pc_cov
    204     pc_covered = property(_get_pc_covered)
    205 
    206     def _get_pc_covered_str(self):
    207         """Returns the percent covered, as a string, without a percent sign.
    208 
    209         Note that "0" is only returned when the value is truly zero, and "100"
    210         is only returned when the value is truly 100.  Rounding can never
    211         result in either "0" or "100".
    212 
    213         """
    214         pc = self.pc_covered
    215         if 0 < pc < self._near0:
    216             pc = self._near0
    217         elif self._near100 < pc < 100:
    218             pc = self._near100
    219         else:
    220             pc = round(pc, self._precision)
    221         return "%.*f" % (self._precision, pc)
    222     pc_covered_str = property(_get_pc_covered_str)
    223 
    224     def pc_str_width(cls):
    225         """How many characters wide can pc_covered_str be?"""
    226         width = 3   # "100"
    227         if cls._precision > 0:
    228             width += 1 + cls._precision
    229         return width
    230     pc_str_width = classmethod(pc_str_width)
    231 
    232     def __add__(self, other):
    233         nums = Numbers()
    234         nums.n_files = self.n_files + other.n_files
    235         nums.n_statements = self.n_statements + other.n_statements
    236         nums.n_excluded = self.n_excluded + other.n_excluded
    237         nums.n_missing = self.n_missing + other.n_missing
    238         nums.n_branches = self.n_branches + other.n_branches
    239         nums.n_missing_branches = (self.n_missing_branches +
    240                                                     other.n_missing_branches)
    241         return nums
    242 
    243     def __radd__(self, other):
    244         # Implementing 0+Numbers allows us to sum() a list of Numbers.
    245         if other == 0:
    246             return self
    247         return NotImplemented
    248