--- a/DebugClients/Python/coverage/results.py Fri Apr 04 22:57:07 2014 +0200 +++ b/DebugClients/Python/coverage/results.py Thu Apr 10 23:02:20 2014 +0200 @@ -2,8 +2,8 @@ import os -from .backward import set, sorted # pylint: disable-msg=W0622 -from .misc import format_lines, NoSource +from .backward import iitems, set, sorted # pylint: disable=W0622 +from .misc import format_lines, join_regex, NoSource from .parser import CodeParser @@ -15,31 +15,33 @@ self.code_unit = code_unit self.filename = self.code_unit.filename - ext = os.path.splitext(self.filename)[1] - source = None - if ext == '.py': - if not os.path.exists(self.filename): - source = self.coverage.file_locator.get_zip_data(self.filename) - if not source: - raise NoSource("No source for code: %r" % self.filename) + actual_filename, source = self.find_source(self.filename) self.parser = CodeParser( - text=source, filename=self.filename, - exclude=self.coverage.exclude_re + 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)) + self.missing = self.statements - 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_missing_branches = sum([len(v) for v in mba.values()]) + 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_missing_branches = 0 + n_branches = n_partial_branches = n_missing_branches = 0 + self.no_branch = set() self.numbers = Numbers( n_files=1, @@ -47,9 +49,48 @@ 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. @@ -64,7 +105,8 @@ def arc_possibilities(self): """Returns a sorted list of the arcs in the code.""" - return self.parser.arcs() + arcs = self.parser.arcs() + return arcs def arcs_executed(self): """Returns a sorted list of the arcs actually executed in the code.""" @@ -77,7 +119,11 @@ """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] + 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): @@ -89,14 +135,15 @@ # 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] + if e not in possible + and e[0] != e[1] ] return sorted(unpredicted) def branch_lines(self): - """Returns lines that have more than one exit.""" + """Returns a list of line numbers that have more than one exit.""" exit_counts = self.parser.exit_counts() - return [l1 for l1,count in exit_counts.items() if count > 1] + return [l1 for l1,count in iitems(exit_counts) if count > 1] def total_branches(self): """How many total branches are there?""" @@ -119,6 +166,25 @@ 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. @@ -127,16 +193,31 @@ 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_missing_branches=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 @@ -157,6 +238,32 @@ 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 @@ -164,15 +271,16 @@ 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_missing_branches = (self.n_missing_branches + - other.n_missing_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 - raise NotImplemented - -# -# eflag: FileType = Python2 + return NotImplemented