1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt
3
4"""Results of coverage measurement."""
5
6import collections
7
8from coverage.backward import iitems
9from coverage.misc import format_lines
10
11
12class Analysis(object):
13    """The results of analyzing a FileReporter."""
14
15    def __init__(self, data, file_reporter):
16        self.data = data
17        self.file_reporter = file_reporter
18        self.filename = self.file_reporter.filename
19        self.statements = self.file_reporter.lines()
20        self.excluded = self.file_reporter.excluded_lines()
21
22        # Identify missing statements.
23        executed = self.data.lines(self.filename) or []
24        executed = self.file_reporter.translate_lines(executed)
25        self.missing = self.statements - executed
26
27        if self.data.has_arcs():
28            self._arc_possibilities = sorted(self.file_reporter.arcs())
29            self.exit_counts = self.file_reporter.exit_counts()
30            self.no_branch = self.file_reporter.no_branch_lines()
31            n_branches = self.total_branches()
32            mba = self.missing_branch_arcs()
33            n_partial_branches = sum(
34                len(v) for k,v in iitems(mba) if k not in self.missing
35                )
36            n_missing_branches = sum(len(v) for k,v in iitems(mba))
37        else:
38            self._arc_possibilities = []
39            self.exit_counts = {}
40            self.no_branch = set()
41            n_branches = n_partial_branches = n_missing_branches = 0
42
43        self.numbers = Numbers(
44            n_files=1,
45            n_statements=len(self.statements),
46            n_excluded=len(self.excluded),
47            n_missing=len(self.missing),
48            n_branches=n_branches,
49            n_partial_branches=n_partial_branches,
50            n_missing_branches=n_missing_branches,
51            )
52
53    def missing_formatted(self):
54        """The missing line numbers, formatted nicely.
55
56        Returns a string like "1-2, 5-11, 13-14".
57
58        """
59        return format_lines(self.statements, self.missing)
60
61    def has_arcs(self):
62        """Were arcs measured in this result?"""
63        return self.data.has_arcs()
64
65    def arc_possibilities(self):
66        """Returns a sorted list of the arcs in the code."""
67        return self._arc_possibilities
68
69    def arcs_executed(self):
70        """Returns a sorted list of the arcs actually executed in the code."""
71        executed = self.data.arcs(self.filename) or []
72        executed = self.file_reporter.translate_arcs(executed)
73        return sorted(executed)
74
75    def arcs_missing(self):
76        """Returns a sorted list of the arcs in the code not executed."""
77        possible = self.arc_possibilities()
78        executed = self.arcs_executed()
79        missing = (
80            p for p in possible
81                if p not in executed
82                    and p[0] not in self.no_branch
83        )
84        return sorted(missing)
85
86    def arcs_missing_formatted(self):
87        """ The missing branch arcs, formatted nicely.
88
89        Returns a string like "1->2, 1->3, 16->20". Omits any mention of
90        branches from missing lines, so if line 17 is missing, then 17->18
91        won't be included.
92
93        """
94        arcs = self.missing_branch_arcs()
95        missing = self.missing
96        line_exits = sorted(iitems(arcs))
97        pairs = []
98        for line, exits in line_exits:
99            for ex in sorted(exits):
100                if line not in missing:
101                    pairs.append('%d->%d' % (line, ex))
102        return ', '.join(pairs)
103
104    def arcs_unpredicted(self):
105        """Returns a sorted list of the executed arcs missing from the code."""
106        possible = self.arc_possibilities()
107        executed = self.arcs_executed()
108        # Exclude arcs here which connect a line to itself.  They can occur
109        # in executed data in some cases.  This is where they can cause
110        # trouble, and here is where it's the least burden to remove them.
111        # Also, generators can somehow cause arcs from "enter" to "exit", so
112        # make sure we have at least one positive value.
113        unpredicted = (
114            e for e in executed
115                if e not in possible
116                    and e[0] != e[1]
117                    and (e[0] > 0 or e[1] > 0)
118        )
119        return sorted(unpredicted)
120
121    def branch_lines(self):
122        """Returns a list of line numbers that have more than one exit."""
123        return [l1 for l1,count in iitems(self.exit_counts) if count > 1]
124
125    def total_branches(self):
126        """How many total branches are there?"""
127        return sum(count for count in self.exit_counts.values() if count > 1)
128
129    def missing_branch_arcs(self):
130        """Return arcs that weren't executed from branch lines.
131
132        Returns {l1:[l2a,l2b,...], ...}
133
134        """
135        missing = self.arcs_missing()
136        branch_lines = set(self.branch_lines())
137        mba = collections.defaultdict(list)
138        for l1, l2 in missing:
139            if l1 in branch_lines:
140                mba[l1].append(l2)
141        return mba
142
143    def branch_stats(self):
144        """Get stats about branches.
145
146        Returns a dict mapping line numbers to a tuple:
147        (total_exits, taken_exits).
148        """
149
150        missing_arcs = self.missing_branch_arcs()
151        stats = {}
152        for lnum in self.branch_lines():
153            exits = self.exit_counts[lnum]
154            try:
155                missing = len(missing_arcs[lnum])
156            except KeyError:
157                missing = 0
158            stats[lnum] = (exits, exits - missing)
159        return stats
160
161
162class Numbers(object):
163    """The numerical results of measuring coverage.
164
165    This holds the basic statistics from `Analysis`, and is used to roll
166    up statistics across files.
167
168    """
169    # A global to determine the precision on coverage percentages, the number
170    # of decimal places.
171    _precision = 0
172    _near0 = 1.0              # These will change when _precision is changed.
173    _near100 = 99.0
174
175    def __init__(self, n_files=0, n_statements=0, n_excluded=0, n_missing=0,
176                    n_branches=0, n_partial_branches=0, n_missing_branches=0
177                    ):
178        self.n_files = n_files
179        self.n_statements = n_statements
180        self.n_excluded = n_excluded
181        self.n_missing = n_missing
182        self.n_branches = n_branches
183        self.n_partial_branches = n_partial_branches
184        self.n_missing_branches = n_missing_branches
185
186    def init_args(self):
187        """Return a list for __init__(*args) to recreate this object."""
188        return [
189            self.n_files, self.n_statements, self.n_excluded, self.n_missing,
190            self.n_branches, self.n_partial_branches, self.n_missing_branches,
191        ]
192
193    @classmethod
194    def set_precision(cls, precision):
195        """Set the number of decimal places used to report percentages."""
196        assert 0 <= precision < 10
197        cls._precision = precision
198        cls._near0 = 1.0 / 10**precision
199        cls._near100 = 100.0 - cls._near0
200
201    @property
202    def n_executed(self):
203        """Returns the number of executed statements."""
204        return self.n_statements - self.n_missing
205
206    @property
207    def n_executed_branches(self):
208        """Returns the number of executed branches."""
209        return self.n_branches - self.n_missing_branches
210
211    @property
212    def pc_covered(self):
213        """Returns a single percentage value for coverage."""
214        if self.n_statements > 0:
215            numerator, denominator = self.ratio_covered
216            pc_cov = (100.0 * numerator) / denominator
217        else:
218            pc_cov = 100.0
219        return pc_cov
220
221    @property
222    def pc_covered_str(self):
223        """Returns the percent covered, as a string, without a percent sign.
224
225        Note that "0" is only returned when the value is truly zero, and "100"
226        is only returned when the value is truly 100.  Rounding can never
227        result in either "0" or "100".
228
229        """
230        pc = self.pc_covered
231        if 0 < pc < self._near0:
232            pc = self._near0
233        elif self._near100 < pc < 100:
234            pc = self._near100
235        else:
236            pc = round(pc, self._precision)
237        return "%.*f" % (self._precision, pc)
238
239    @classmethod
240    def pc_str_width(cls):
241        """How many characters wide can pc_covered_str be?"""
242        width = 3   # "100"
243        if cls._precision > 0:
244            width += 1 + cls._precision
245        return width
246
247    @property
248    def ratio_covered(self):
249        """Return a numerator and denominator for the coverage ratio."""
250        numerator = self.n_executed + self.n_executed_branches
251        denominator = self.n_statements + self.n_branches
252        return numerator, denominator
253
254    def __add__(self, other):
255        nums = Numbers()
256        nums.n_files = self.n_files + other.n_files
257        nums.n_statements = self.n_statements + other.n_statements
258        nums.n_excluded = self.n_excluded + other.n_excluded
259        nums.n_missing = self.n_missing + other.n_missing
260        nums.n_branches = self.n_branches + other.n_branches
261        nums.n_partial_branches = (
262            self.n_partial_branches + other.n_partial_branches
263            )
264        nums.n_missing_branches = (
265            self.n_missing_branches + other.n_missing_branches
266            )
267        return nums
268
269    def __radd__(self, other):
270        # Implementing 0+Numbers allows us to sum() a list of Numbers.
271        if other == 0:
272            return self
273        return NotImplemented
274