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:
219
venv/lib/python3.12/site-packages/coverage/lcovreport.py
Normal file
219
venv/lib/python3.12/site-packages/coverage/lcovreport.py
Normal file
@@ -0,0 +1,219 @@
|
||||
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
|
||||
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
|
||||
|
||||
"""LCOV reporting for coverage.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from typing import IO, TYPE_CHECKING
|
||||
|
||||
from coverage.plugin import FileReporter
|
||||
from coverage.report_core import get_analysis_to_report
|
||||
from coverage.results import Analysis, AnalysisNarrower, Numbers
|
||||
from coverage.types import TMorf
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from coverage import Coverage
|
||||
|
||||
|
||||
def line_hash(line: str) -> str:
|
||||
"""Produce a hash of a source line for use in the LCOV file."""
|
||||
# The LCOV file format optionally allows each line to be MD5ed as a
|
||||
# fingerprint of the file. This is not a security use. Some security
|
||||
# scanners raise alarms about the use of MD5 here, but it is a false
|
||||
# positive. This is not a security concern.
|
||||
# The unusual encoding of the MD5 hash, as a base64 sequence with the
|
||||
# trailing = signs stripped, is specified by the LCOV file format.
|
||||
hashed = hashlib.md5(line.encode("utf-8"), usedforsecurity=False).digest()
|
||||
return base64.b64encode(hashed).decode("ascii").rstrip("=")
|
||||
|
||||
|
||||
def lcov_lines(
|
||||
analysis: Analysis,
|
||||
lines: list[int],
|
||||
source_lines: list[str],
|
||||
outfile: IO[str],
|
||||
) -> None:
|
||||
"""Emit line coverage records for an analyzed file."""
|
||||
hash_suffix = ""
|
||||
for line in lines:
|
||||
if source_lines:
|
||||
hash_suffix = "," + line_hash(source_lines[line - 1])
|
||||
# Q: can we get info about the number of times a statement is
|
||||
# executed? If so, that should be recorded here.
|
||||
hit = int(line not in analysis.missing)
|
||||
outfile.write(f"DA:{line},{hit}{hash_suffix}\n")
|
||||
|
||||
if analysis.numbers.n_statements > 0:
|
||||
outfile.write(f"LF:{analysis.numbers.n_statements}\n")
|
||||
outfile.write(f"LH:{analysis.numbers.n_executed}\n")
|
||||
|
||||
|
||||
def lcov_functions(
|
||||
fr: FileReporter,
|
||||
file_analysis: Analysis,
|
||||
outfile: IO[str],
|
||||
) -> None:
|
||||
"""Emit function coverage records for an analyzed file."""
|
||||
# lcov 2.2 introduces a new format for function coverage records.
|
||||
# We continue to generate the old format because we don't know what
|
||||
# version of the lcov tools will be used to read this report.
|
||||
|
||||
# "and region.lines" below avoids a crash due to a bug in PyPy 3.8
|
||||
# where, for whatever reason, when collecting data in --branch mode,
|
||||
# top-level functions have an empty lines array. Instead we just don't
|
||||
# emit function records for those.
|
||||
|
||||
# suppressions because of https://github.com/pylint-dev/pylint/issues/9923
|
||||
functions = [
|
||||
(
|
||||
min(region.start, min(region.lines)), # pylint: disable=nested-min-max
|
||||
max(region.start, max(region.lines)), # pylint: disable=nested-min-max
|
||||
region,
|
||||
)
|
||||
for region in fr.code_regions()
|
||||
if region.kind == "function" and region.lines
|
||||
]
|
||||
if not functions:
|
||||
return
|
||||
|
||||
narrower = AnalysisNarrower(file_analysis)
|
||||
narrower.add_regions(r.lines for _, _, r in functions)
|
||||
|
||||
functions.sort()
|
||||
functions_hit = 0
|
||||
for first_line, last_line, region in functions:
|
||||
# A function counts as having been executed if any of it has been
|
||||
# executed.
|
||||
analysis = narrower.narrow(region.lines)
|
||||
hit = int(analysis.numbers.n_executed > 0)
|
||||
functions_hit += hit
|
||||
|
||||
outfile.write(f"FN:{first_line},{last_line},{region.name}\n")
|
||||
outfile.write(f"FNDA:{hit},{region.name}\n")
|
||||
|
||||
outfile.write(f"FNF:{len(functions)}\n")
|
||||
outfile.write(f"FNH:{functions_hit}\n")
|
||||
|
||||
|
||||
def lcov_arcs(
|
||||
fr: FileReporter,
|
||||
analysis: Analysis,
|
||||
lines: list[int],
|
||||
outfile: IO[str],
|
||||
) -> None:
|
||||
"""Emit branch coverage records for an analyzed file."""
|
||||
branch_stats = analysis.branch_stats()
|
||||
executed_arcs = analysis.executed_branch_arcs()
|
||||
missing_arcs = analysis.missing_branch_arcs()
|
||||
|
||||
for line in lines:
|
||||
if line not in branch_stats:
|
||||
continue
|
||||
|
||||
# This is only one of several possible ways to map our sets of executed
|
||||
# and not-executed arcs to BRDA codes. It seems to produce reasonable
|
||||
# results when fed through genhtml.
|
||||
_, taken = branch_stats[line]
|
||||
|
||||
if taken == 0:
|
||||
# When _none_ of the out arcs from 'line' were executed,
|
||||
# it can mean the line always raised an exception.
|
||||
assert len(executed_arcs[line]) == 0
|
||||
destinations = [(dst, "-") for dst in missing_arcs[line]]
|
||||
else:
|
||||
# Q: can we get counts of the number of times each arc was executed?
|
||||
# branch_stats has "total" and "taken" counts for each branch,
|
||||
# but it doesn't have "taken" broken down by destination.
|
||||
destinations = [(dst, "1") for dst in executed_arcs[line]]
|
||||
destinations.extend((dst, "0") for dst in missing_arcs[line])
|
||||
|
||||
# Sort exit arcs after normal arcs. Exit arcs typically come from
|
||||
# an if statement, at the end of a function, with no else clause.
|
||||
# This structure reads like you're jumping to the end of the function
|
||||
# when the conditional expression is false, so it should be presented
|
||||
# as the second alternative for the branch, after the alternative that
|
||||
# enters the if clause.
|
||||
destinations.sort(key=lambda d: (d[0] < 0, d))
|
||||
|
||||
for dst, hit in destinations:
|
||||
branch = fr.arc_description(line, dst)
|
||||
outfile.write(f"BRDA:{line},0,{branch},{hit}\n")
|
||||
|
||||
# Summary of the branch coverage.
|
||||
brf = sum(t for t, k in branch_stats.values())
|
||||
brh = brf - sum(t - k for t, k in branch_stats.values())
|
||||
if brf > 0:
|
||||
outfile.write(f"BRF:{brf}\n")
|
||||
outfile.write(f"BRH:{brh}\n")
|
||||
|
||||
|
||||
class LcovReporter:
|
||||
"""A reporter for writing LCOV coverage reports."""
|
||||
|
||||
report_type = "LCOV report"
|
||||
|
||||
def __init__(self, coverage: Coverage) -> None:
|
||||
self.coverage = coverage
|
||||
self.config = coverage.config
|
||||
self.total = Numbers(self.coverage.config.precision)
|
||||
|
||||
def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str]) -> float:
|
||||
"""Renders the full lcov report.
|
||||
|
||||
`morfs` is a list of modules or filenames
|
||||
|
||||
outfile is the file object to write the file into.
|
||||
"""
|
||||
|
||||
self.coverage.get_data()
|
||||
outfile = outfile or sys.stdout
|
||||
|
||||
# ensure file records are sorted by the _relative_ filename, not the full path
|
||||
to_report = [
|
||||
(fr.relative_filename(), fr, analysis)
|
||||
for fr, analysis in get_analysis_to_report(self.coverage, morfs)
|
||||
]
|
||||
to_report.sort()
|
||||
|
||||
for fname, fr, analysis in to_report:
|
||||
self.total += analysis.numbers
|
||||
self.lcov_file(fname, fr, analysis, outfile)
|
||||
|
||||
return self.total.n_statements and self.total.pc_covered
|
||||
|
||||
def lcov_file(
|
||||
self,
|
||||
rel_fname: str,
|
||||
fr: FileReporter,
|
||||
analysis: Analysis,
|
||||
outfile: IO[str],
|
||||
) -> None:
|
||||
"""Produces the lcov data for a single file.
|
||||
|
||||
This currently supports both line and branch coverage,
|
||||
however function coverage is not supported.
|
||||
"""
|
||||
|
||||
if analysis.numbers.n_statements == 0:
|
||||
if self.config.skip_empty:
|
||||
return
|
||||
|
||||
outfile.write(f"SF:{rel_fname}\n")
|
||||
|
||||
lines = sorted(analysis.statements)
|
||||
if self.config.lcov_line_checksums:
|
||||
source_lines = fr.source().splitlines()
|
||||
else:
|
||||
source_lines = []
|
||||
|
||||
lcov_lines(analysis, lines, source_lines, outfile)
|
||||
lcov_functions(fr, analysis, outfile)
|
||||
if analysis.has_arcs:
|
||||
lcov_arcs(fr, analysis, lines, outfile)
|
||||
|
||||
outfile.write("end_of_record\n")
|
||||
Reference in New Issue
Block a user