1"""Results of coverage measurement."""
2
3import os
4
5from coverage.backward import set, sorted           # pylint: disable=W0622
6from coverage.misc import format_lines, join_regex, NoSource
7from coverage.parser import CodeParser
8
9
10class 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
155class 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