--- a/DebugClients/Python/coverage/results.py Sun Oct 04 13:35:09 2015 +0200 +++ b/DebugClients/Python/coverage/results.py Sun Oct 04 22:37:56 2015 +0200 @@ -1,47 +1,44 @@ +# 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 os +import collections -from .backward import iitems, set, sorted # pylint: disable=W0622 -from .misc import format_lines, join_regex, NoSource -from .parser import CodeParser +from coverage.backward import iitems +from coverage.misc import format_lines class Analysis(object): - """The results of analyzing a code unit.""" - - def __init__(self, cov, code_unit): - self.coverage = cov - self.code_unit = code_unit + """The results of analyzing a FileReporter.""" - 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() + 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.coverage.data.executed_lines(self.filename) - exec1 = self.parser.first_lines(executed) - self.missing = self.statements - exec1 + executed = self.data.lines(self.filename) or [] + executed = self.file_reporter.translate_lines(executed) + self.missing = self.statements - executed - 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) - ) + 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] + 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)]) + 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.no_branch = set() self.numbers = Numbers( n_files=1, @@ -53,44 +50,6 @@ 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. @@ -101,31 +60,47 @@ def has_arcs(self): """Were arcs measured in this result?""" - return self.coverage.data.has_arcs() + return self.data.has_arcs() def arc_possibilities(self): """Returns a sorted list of the arcs in the code.""" - arcs = self.parser.arcs() - return arcs + return self._arc_possibilities 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] + 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 = [ + 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->%d' % (line, ex)) + return ', '.join(pairs) + def arcs_unpredicted(self): """Returns a sorted list of the executed arcs missing from the code.""" possible = self.arc_possibilities() @@ -133,22 +108,23 @@ # 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 = [ + # 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.""" - exit_counts = self.parser.exit_counts() - return [l1 for l1,count in iitems(exit_counts) if count > 1] + return [l1 for l1,count in iitems(self.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]) + 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. @@ -158,11 +134,9 @@ """ missing = self.arcs_missing() branch_lines = set(self.branch_lines()) - mba = {} + mba = collections.defaultdict(list) for l1, l2 in missing: if l1 in branch_lines: - if l1 not in mba: - mba[l1] = [] mba[l1].append(l2) return mba @@ -173,11 +147,10 @@ (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] + exits = self.exit_counts[lnum] try: missing = len(missing_arcs[lnum]) except KeyError: @@ -210,35 +183,43 @@ 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 - set_precision = classmethod(set_precision) - def _get_n_executed(self): + @property + def 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): + @property + def 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): + @property + def 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)) + numerator, denominator = self.ratio_covered + pc_cov = (100.0 * numerator) / denominator else: pc_cov = 100.0 return pc_cov - pc_covered = property(_get_pc_covered) - def _get_pc_covered_str(self): + @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" @@ -254,15 +235,21 @@ else: pc = round(pc, self._precision) return "%.*f" % (self._precision, pc) - pc_covered_str = property(_get_pc_covered_str) + @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 - pc_str_width = classmethod(pc_str_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() @@ -284,6 +271,3 @@ if other == 0: return self return NotImplemented - -# -# eflag: FileType = Python2