eric7/DebugClients/Python/coverage/results.py

branch
eric7
changeset 8312
800c432b34c8
parent 7427
362cd1b6f81a
child 8527
2bd1325d727e
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric7/DebugClients/Python/coverage/results.py	Sat May 15 18:45:04 2021 +0200
@@ -0,0 +1,346 @@
+# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
+# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
+
+"""Results of coverage measurement."""
+
+import collections
+
+from coverage.backward import iitems
+from coverage.debug import SimpleReprMixin
+from coverage.misc import contract, CoverageException, nice_pair
+
+
+class Analysis(object):
+    """The results of analyzing a FileReporter."""
+
+    def __init__(self, data, file_reporter, file_mapper):
+        self.data = data
+        self.file_reporter = file_reporter
+        self.filename = file_mapper(self.file_reporter.filename)
+        self.statements = self.file_reporter.lines()
+        self.excluded = self.file_reporter.excluded_lines()
+
+        # Identify missing statements.
+        executed = self.data.lines(self.filename) or []
+        executed = self.file_reporter.translate_lines(executed)
+        self.executed = executed
+        self.missing = self.statements - self.executed
+
+        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)
+            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.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 missing_formatted(self, branches=False):
+        """The missing line numbers, formatted nicely.
+
+        Returns a string like "1-2, 5-11, 13-14".
+
+        If `branches` is true, includes the missing branch arcs also.
+
+        """
+        if branches and self.has_arcs():
+            arcs = iitems(self.missing_branch_arcs())
+        else:
+            arcs = None
+
+        return format_lines(self.statements, self.missing, arcs=arcs)
+
+    def has_arcs(self):
+        """Were arcs measured in this result?"""
+        return self.data.has_arcs()
+
+    @contract(returns='list(tuple(int, int))')
+    def arc_possibilities(self):
+        """Returns a sorted list of the arcs in the code."""
+        return self._arc_possibilities
+
+    @contract(returns='list(tuple(int, int))')
+    def arcs_executed(self):
+        """Returns a sorted list of the arcs actually executed in the code."""
+        executed = self.data.arcs(self.filename) or []
+        executed = self.file_reporter.translate_arcs(executed)
+        return sorted(executed)
+
+    @contract(returns='list(tuple(int, int))')
+    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)
+
+    @contract(returns='list(tuple(int, int))')
+    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.
+        # 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."""
+        return [l1 for l1,count in iitems(self.exit_counts) if count > 1]
+
+    def _total_branches(self):
+        """How many total branches are there?"""
+        return sum(count for count in self.exit_counts.values() if count > 1)
+
+    @contract(returns='dict(int: list(int))')
+    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 = collections.defaultdict(list)
+        for l1, l2 in missing:
+            if l1 in branch_lines:
+                mba[l1].append(l2)
+        return mba
+
+    @contract(returns='dict(int: tuple(int, int))')
+    def branch_stats(self):
+        """Get stats about branches.
+
+        Returns a dict mapping line numbers to a tuple:
+        (total_exits, taken_exits).
+        """
+
+        missing_arcs = self.missing_branch_arcs()
+        stats = {}
+        for lnum in self._branch_lines():
+            exits = self.exit_counts[lnum]
+            try:
+                missing = len(missing_arcs[lnum])
+            except KeyError:
+                missing = 0
+            stats[lnum] = (exits, exits - missing)
+        return stats
+
+
+class Numbers(SimpleReprMixin):
+    """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 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
+
+    @property
+    def n_executed(self):
+        """Returns the number of executed statements."""
+        return self.n_statements - self.n_missing
+
+    @property
+    def n_executed_branches(self):
+        """Returns the number of executed branches."""
+        return self.n_branches - self.n_missing_branches
+
+    @property
+    def pc_covered(self):
+        """Returns a single percentage value for coverage."""
+        if self.n_statements > 0:
+            numerator, denominator = self.ratio_covered
+            pc_cov = (100.0 * numerator) / denominator
+        else:
+            pc_cov = 100.0
+        return pc_cov
+
+    @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"
+        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)
+
+    @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
+
+    @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()
+        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
+
+
+def _line_ranges(statements, lines):
+    """Produce a list of ranges for `format_lines`."""
+    statements = sorted(statements)
+    lines = sorted(lines)
+
+    pairs = []
+    start = None
+    lidx = 0
+    for stmt in statements:
+        if lidx >= len(lines):
+            break
+        if stmt == lines[lidx]:
+            lidx += 1
+            if not start:
+                start = stmt
+            end = stmt
+        elif start:
+            pairs.append((start, end))
+            start = None
+    if start:
+        pairs.append((start, end))
+    return pairs
+
+
+def format_lines(statements, lines, arcs=None):
+    """Nicely format a list of line numbers.
+
+    Format a list of line numbers for printing by coalescing groups of lines as
+    long as the lines represent consecutive statements.  This will coalesce
+    even if there are gaps between statements.
+
+    For example, if `statements` is [1,2,3,4,5,10,11,12,13,14] and
+    `lines` is [1,2,5,10,11,13,14] then the result will be "1-2, 5-11, 13-14".
+
+    Both `lines` and `statements` can be any iterable. All of the elements of
+    `lines` must be in `statements`, and all of the values must be positive
+    integers.
+
+    If `arcs` is provided, they are (start,[end,end,end]) pairs that will be
+    included in the output as long as start isn't in `lines`.
+
+    """
+    line_items = [(pair[0], nice_pair(pair)) for pair in _line_ranges(statements, lines)]
+    if arcs:
+        line_exits = sorted(arcs)
+        for line, exits in line_exits:
+            for ex in sorted(exits):
+                if line not in lines:
+                    dest = (ex if ex > 0 else "exit")
+                    line_items.append((line, "%d->%s" % (line, dest)))
+
+    ret = ', '.join(t[-1] for t in sorted(line_items))
+    return ret
+
+
+@contract(total='number', fail_under='number', precision=int, returns=bool)
+def should_fail_under(total, fail_under, precision):
+    """Determine if a total should fail due to fail-under.
+
+    `total` is a float, the coverage measurement total. `fail_under` is the
+    fail_under setting to compare with. `precision` is the number of digits
+    to consider after the decimal point.
+
+    Returns True if the total should fail.
+
+    """
+    # We can never achieve higher than 100% coverage, or less than zero.
+    if not (0 <= fail_under <= 100.0):
+        msg = "fail_under={} is invalid. Must be between 0 and 100.".format(fail_under)
+        raise CoverageException(msg)
+
+    # Special case for fail_under=100, it must really be 100.
+    if fail_under == 100.0 and total != 100.0:
+        return True
+
+    return round(total, precision) < fail_under

eric ide

mercurial