Major fixes and new features
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
471
venv/lib/python3.12/site-packages/coverage/results.py
Normal file
471
venv/lib/python3.12/site-packages/coverage/results.py
Normal file
@@ -0,0 +1,471 @@
|
||||
# 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."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import dataclasses
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from coverage.exceptions import ConfigError
|
||||
from coverage.misc import nice_pair
|
||||
from coverage.types import TArc, TLineNo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from coverage.data import CoverageData
|
||||
from coverage.plugin import FileReporter
|
||||
|
||||
|
||||
def analysis_from_file_reporter(
|
||||
data: CoverageData,
|
||||
precision: int,
|
||||
file_reporter: FileReporter,
|
||||
filename: str,
|
||||
) -> Analysis:
|
||||
"""Create an Analysis from a FileReporter."""
|
||||
has_arcs = data.has_arcs()
|
||||
statements = file_reporter.lines()
|
||||
excluded = file_reporter.excluded_lines()
|
||||
executed = file_reporter.translate_lines(data.lines(filename) or [])
|
||||
|
||||
if has_arcs:
|
||||
arc_possibilities_set = file_reporter.arcs()
|
||||
arcs: Iterable[TArc] = data.arcs(filename) or []
|
||||
arcs = file_reporter.translate_arcs(arcs)
|
||||
|
||||
# Reduce the set of arcs to the ones that could be branches.
|
||||
dests = collections.defaultdict(set)
|
||||
for fromno, tono in arc_possibilities_set:
|
||||
dests[fromno].add(tono)
|
||||
single_dests = {
|
||||
fromno: list(tonos)[0] for fromno, tonos in dests.items() if len(tonos) == 1
|
||||
}
|
||||
new_arcs = set()
|
||||
for fromno, tono in arcs:
|
||||
if fromno != tono:
|
||||
new_arcs.add((fromno, tono))
|
||||
else:
|
||||
if fromno in single_dests:
|
||||
new_arcs.add((fromno, single_dests[fromno]))
|
||||
|
||||
arcs_executed_set = file_reporter.translate_arcs(new_arcs)
|
||||
exit_counts = file_reporter.exit_counts()
|
||||
no_branch = file_reporter.no_branch_lines()
|
||||
else:
|
||||
arc_possibilities_set = set()
|
||||
arcs_executed_set = set()
|
||||
exit_counts = {}
|
||||
no_branch = set()
|
||||
|
||||
return Analysis(
|
||||
precision=precision,
|
||||
filename=filename,
|
||||
has_arcs=has_arcs,
|
||||
statements=statements,
|
||||
excluded=excluded,
|
||||
executed=executed,
|
||||
arc_possibilities_set=arc_possibilities_set,
|
||||
arcs_executed_set=arcs_executed_set,
|
||||
exit_counts=exit_counts,
|
||||
no_branch=no_branch,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Analysis:
|
||||
"""The results of analyzing a FileReporter."""
|
||||
|
||||
precision: int
|
||||
filename: str
|
||||
has_arcs: bool
|
||||
statements: set[TLineNo]
|
||||
excluded: set[TLineNo]
|
||||
executed: set[TLineNo]
|
||||
arc_possibilities_set: set[TArc]
|
||||
arcs_executed_set: set[TArc]
|
||||
exit_counts: dict[TLineNo, int]
|
||||
no_branch: set[TLineNo]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.arc_possibilities = sorted(self.arc_possibilities_set)
|
||||
self.arcs_executed = sorted(self.arcs_executed_set)
|
||||
self.missing = self.statements - self.executed
|
||||
|
||||
if self.has_arcs:
|
||||
n_branches = self._total_branches()
|
||||
mba = self.missing_branch_arcs()
|
||||
n_partial_branches = sum(len(v) for k, v in mba.items() if k not in self.missing)
|
||||
n_missing_branches = sum(len(v) for k, v in mba.items())
|
||||
else:
|
||||
n_branches = n_partial_branches = n_missing_branches = 0
|
||||
|
||||
self.numbers = Numbers(
|
||||
precision=self.precision,
|
||||
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: bool = False) -> str:
|
||||
"""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 = self.missing_branch_arcs().items()
|
||||
else:
|
||||
arcs = None
|
||||
|
||||
return format_lines(self.statements, self.missing, arcs=arcs)
|
||||
|
||||
def arcs_missing(self) -> list[TArc]:
|
||||
"""Returns a sorted list of the un-executed arcs in the code."""
|
||||
missing = (
|
||||
p
|
||||
for p in self.arc_possibilities
|
||||
if p not in self.arcs_executed_set
|
||||
and p[0] not in self.no_branch
|
||||
and p[1] not in self.excluded
|
||||
)
|
||||
return sorted(missing)
|
||||
|
||||
def _branch_lines(self) -> list[TLineNo]:
|
||||
"""Returns a list of line numbers that have more than one exit."""
|
||||
return [l1 for l1, count in self.exit_counts.items() if count > 1]
|
||||
|
||||
def _total_branches(self) -> int:
|
||||
"""How many total branches are there?"""
|
||||
return sum(count for count in self.exit_counts.values() if count > 1)
|
||||
|
||||
def missing_branch_arcs(self) -> dict[TLineNo, list[TLineNo]]:
|
||||
"""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:
|
||||
assert l1 != l2, f"In {self.filename}, didn't expect {l1} == {l2}"
|
||||
if l1 in branch_lines:
|
||||
mba[l1].append(l2)
|
||||
return mba
|
||||
|
||||
def executed_branch_arcs(self) -> dict[TLineNo, list[TLineNo]]:
|
||||
"""Return arcs that were executed from branch lines.
|
||||
|
||||
Only include ones that we considered possible.
|
||||
|
||||
Returns {l1:[l2a,l2b,...], ...}
|
||||
|
||||
"""
|
||||
branch_lines = set(self._branch_lines())
|
||||
eba = collections.defaultdict(list)
|
||||
for l1, l2 in self.arcs_executed:
|
||||
assert l1 != l2, f"Oops: Didn't think this could happen: {l1 = }, {l2 = }"
|
||||
if (l1, l2) not in self.arc_possibilities_set:
|
||||
continue
|
||||
if l1 in branch_lines:
|
||||
eba[l1].append(l2)
|
||||
return eba
|
||||
|
||||
def branch_stats(self) -> dict[TLineNo, tuple[int, int]]:
|
||||
"""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]
|
||||
missing = len(missing_arcs[lnum])
|
||||
stats[lnum] = (exits, exits - missing)
|
||||
return stats
|
||||
|
||||
|
||||
TRegionLines = frozenset[TLineNo]
|
||||
|
||||
|
||||
class AnalysisNarrower:
|
||||
"""
|
||||
For reducing an `Analysis` to a subset of its lines.
|
||||
|
||||
Originally this was a simpler method on Analysis, but that led to quadratic
|
||||
behavior. This class does the bulk of the work up-front to provide the
|
||||
same results in linear time.
|
||||
|
||||
Create an AnalysisNarrower from an Analysis, bulk-add region lines to it
|
||||
with `add_regions`, then individually request new narrowed Analysis objects
|
||||
for each region with `narrow`. Doing most of the work in limited calls to
|
||||
`add_regions` lets us avoid poor performance.
|
||||
"""
|
||||
|
||||
# In this class, regions are represented by a frozenset of their lines.
|
||||
|
||||
def __init__(self, analysis: Analysis) -> None:
|
||||
self.analysis = analysis
|
||||
self.region2arc_possibilities: dict[TRegionLines, set[TArc]] = collections.defaultdict(set)
|
||||
self.region2arc_executed: dict[TRegionLines, set[TArc]] = collections.defaultdict(set)
|
||||
self.region2exit_counts: dict[TRegionLines, dict[TLineNo, int]] = collections.defaultdict(
|
||||
dict
|
||||
)
|
||||
|
||||
def add_regions(self, liness: Iterable[set[TLineNo]]) -> None:
|
||||
"""
|
||||
Pre-process a number of sets of line numbers. Later calls to `narrow`
|
||||
with one of these sets will provide a narrowed Analysis.
|
||||
"""
|
||||
if self.analysis.has_arcs:
|
||||
line2region: dict[TLineNo, TRegionLines] = {}
|
||||
|
||||
for lines in liness:
|
||||
fzlines = frozenset(lines)
|
||||
for line in lines:
|
||||
line2region[line] = fzlines
|
||||
|
||||
def collect_arcs(
|
||||
arc_set: set[TArc],
|
||||
region2arcs: dict[TRegionLines, set[TArc]],
|
||||
) -> None:
|
||||
for a, b in arc_set:
|
||||
if r := line2region.get(a):
|
||||
region2arcs[r].add((a, b))
|
||||
if r := line2region.get(b):
|
||||
region2arcs[r].add((a, b))
|
||||
|
||||
collect_arcs(self.analysis.arc_possibilities_set, self.region2arc_possibilities)
|
||||
collect_arcs(self.analysis.arcs_executed_set, self.region2arc_executed)
|
||||
|
||||
for lno, num in self.analysis.exit_counts.items():
|
||||
if r := line2region.get(lno):
|
||||
self.region2exit_counts[r][lno] = num
|
||||
|
||||
def narrow(self, lines: set[TLineNo]) -> Analysis:
|
||||
"""Create a narrowed Analysis.
|
||||
|
||||
The current analysis is copied to make a new one that only considers
|
||||
the lines in `lines`.
|
||||
"""
|
||||
|
||||
# Technically, the set intersections in this method are still O(N**2)
|
||||
# since this method is called N times, but they're very fast and moving
|
||||
# them to `add_regions` won't avoid the quadratic time.
|
||||
|
||||
statements = self.analysis.statements & lines
|
||||
excluded = self.analysis.excluded & lines
|
||||
executed = self.analysis.executed & lines
|
||||
|
||||
if self.analysis.has_arcs:
|
||||
fzlines = frozenset(lines)
|
||||
arc_possibilities_set = self.region2arc_possibilities[fzlines]
|
||||
arcs_executed_set = self.region2arc_executed[fzlines]
|
||||
exit_counts = self.region2exit_counts[fzlines]
|
||||
no_branch = self.analysis.no_branch & lines
|
||||
else:
|
||||
arc_possibilities_set = set()
|
||||
arcs_executed_set = set()
|
||||
exit_counts = {}
|
||||
no_branch = set()
|
||||
|
||||
return Analysis(
|
||||
precision=self.analysis.precision,
|
||||
filename=self.analysis.filename,
|
||||
has_arcs=self.analysis.has_arcs,
|
||||
statements=statements,
|
||||
excluded=excluded,
|
||||
executed=executed,
|
||||
arc_possibilities_set=arc_possibilities_set,
|
||||
arcs_executed_set=arcs_executed_set,
|
||||
exit_counts=exit_counts,
|
||||
no_branch=no_branch,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Numbers:
|
||||
"""The numerical results of measuring coverage.
|
||||
|
||||
This holds the basic statistics from `Analysis`, and is used to roll
|
||||
up statistics across files.
|
||||
|
||||
"""
|
||||
|
||||
precision: int = 0
|
||||
n_files: int = 0
|
||||
n_statements: int = 0
|
||||
n_excluded: int = 0
|
||||
n_missing: int = 0
|
||||
n_branches: int = 0
|
||||
n_partial_branches: int = 0
|
||||
n_missing_branches: int = 0
|
||||
|
||||
@property
|
||||
def n_executed(self) -> int:
|
||||
"""Returns the number of executed statements."""
|
||||
return self.n_statements - self.n_missing
|
||||
|
||||
@property
|
||||
def n_executed_branches(self) -> int:
|
||||
"""Returns the number of executed branches."""
|
||||
return self.n_branches - self.n_missing_branches
|
||||
|
||||
@property
|
||||
def pc_covered(self) -> float:
|
||||
"""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) -> str:
|
||||
"""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".
|
||||
|
||||
"""
|
||||
return display_covered(self.pc_covered, self.precision)
|
||||
|
||||
@property
|
||||
def ratio_covered(self) -> tuple[int, int]:
|
||||
"""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: Numbers) -> Numbers:
|
||||
return Numbers(
|
||||
self.precision,
|
||||
self.n_files + other.n_files,
|
||||
self.n_statements + other.n_statements,
|
||||
self.n_excluded + other.n_excluded,
|
||||
self.n_missing + other.n_missing,
|
||||
self.n_branches + other.n_branches,
|
||||
self.n_partial_branches + other.n_partial_branches,
|
||||
self.n_missing_branches + other.n_missing_branches,
|
||||
)
|
||||
|
||||
def __radd__(self, other: int) -> Numbers:
|
||||
# Implementing 0+Numbers allows us to sum() a list of Numbers.
|
||||
assert other == 0 # we only ever call it this way.
|
||||
return self
|
||||
|
||||
|
||||
def display_covered(pc: float, precision: int) -> str:
|
||||
"""Return a displayable total percentage, as a string.
|
||||
|
||||
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".
|
||||
|
||||
"""
|
||||
near0 = 1.0 / 10**precision
|
||||
if 0 < pc < near0:
|
||||
pc = near0
|
||||
elif (100.0 - near0) < pc < 100:
|
||||
pc = 100.0 - near0
|
||||
else:
|
||||
pc = round(pc, precision)
|
||||
return f"{pc:.{precision}f}"
|
||||
|
||||
|
||||
def _line_ranges(
|
||||
statements: Iterable[TLineNo],
|
||||
lines: Iterable[TLineNo],
|
||||
) -> list[tuple[TLineNo, TLineNo]]:
|
||||
"""Produce a list of ranges for `format_lines`."""
|
||||
statements = sorted(statements)
|
||||
lines = sorted(lines)
|
||||
|
||||
pairs = []
|
||||
start: TLineNo | None = 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: Iterable[TLineNo],
|
||||
lines: Iterable[TLineNo],
|
||||
arcs: Iterable[tuple[TLineNo, list[TLineNo]]] | None = None,
|
||||
) -> str:
|
||||
"""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 is not None:
|
||||
line_exits = sorted(arcs)
|
||||
for line, exits in line_exits:
|
||||
for ex in sorted(exits):
|
||||
if line not in lines and ex not in lines:
|
||||
dest = ex if ex > 0 else "exit"
|
||||
line_items.append((line, f"{line}->{dest}"))
|
||||
|
||||
ret = ", ".join(t[-1] for t in sorted(line_items))
|
||||
return ret
|
||||
|
||||
|
||||
def should_fail_under(total: float, fail_under: float, precision: int) -> bool:
|
||||
"""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 = f"fail_under={fail_under} is invalid. Must be between 0 and 100."
|
||||
raise ConfigError(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
|
||||
Reference in New Issue
Block a user