Index: third_party/coverage/results.py |
diff --git a/third_party/coverage/results.py b/third_party/coverage/results.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..2d13e81878a1947d44c9b8fdf8737617346d9e88 |
--- /dev/null |
+++ b/third_party/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 = sorted(set(self.statements) - set(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 |