docs(coverage): regenerate the measured table from the command that measures it
This commit is contained in:
1 parent
b7d0f901cc
commit
19c8d4368d
3 files changed
+147
-50
No files matched your search
+12
-2
@@ -6,8 +6,10 @@
|
||||
# capture src/** with lcov into build/cov/coverage.info, render the HTML report
|
||||
# under build/cov/html/, print the per-file table, and exit non-zero when
|
||||
# tools/coverage_floor.py finds a file below 80% lines or the src/ aggregate
|
||||
# below 90%. The numbers and the floors are the contract docs/coverage.md
|
||||
# states; `docs/coverage.md` also records how to re-read the table from here.
|
||||
# below 90%. The floors are tools/coverage_floor.py's constants; the measured
|
||||
# table and the floor verdict are written back into docs/coverage.md's generated
|
||||
# region by `coverage_floor.py --write-doc`, from the very report this run
|
||||
# captured, so that document cannot drift from the command that measures it.
|
||||
#
|
||||
# Idempotent by construction: gcov counters accumulate in .gcda files, so a
|
||||
# second run that inherited the first run's counters would report a superset of
|
||||
@@ -31,6 +33,7 @@ readonly FLOOR="$ROOT/tools/coverage_floor.py"
|
||||
readonly RAW="$BUILD/coverage.raw.info"
|
||||
readonly INFO="$BUILD/coverage.info"
|
||||
readonly HTML="$BUILD/html"
|
||||
readonly DOC="$ROOT/docs/coverage.md"
|
||||
|
||||
log() { printf '==> %s\n' "$*"; }
|
||||
|
||||
@@ -87,6 +90,13 @@ python3 "$FLOOR" --table "$INFO"
|
||||
readonly FLOOR_STATUS=$?
|
||||
set -e
|
||||
|
||||
# The measured table is written into the document that states it, from the very
|
||||
# report this run captured, so docs/coverage.md cannot drift from the command
|
||||
# that produces it. This happens before the floor gate below, so a failing tree
|
||||
# still gets an honest table (and its failure recorded) in the document.
|
||||
log "regenerating the measured table in $DOC"
|
||||
python3 "$FLOOR" --write-doc "$DOC" "$INFO"
|
||||
|
||||
if (( FLOOR_STATUS != 0 )); then
|
||||
printf '\ncoverage: FLOOR FAILED (exit %d) -- report %s, HTML %s/index.html\n' \
|
||||
"$FLOOR_STATUS" "$INFO" "$HTML" >&2
|
||||
|
||||
@@ -184,6 +184,75 @@ def render_table(records: list[Record], markdown: bool) -> str:
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# The generated region docs/coverage.md carries. tools/coverage.sh calls this
|
||||
# tool with --write-doc on every run, so the table in that document is the one
|
||||
# the command that measured it just captured, never a number copied by hand.
|
||||
DOC_BEGIN = "<!-- BEGIN GENERATED: tools/coverage.sh -->"
|
||||
DOC_END = "<!-- END GENERATED: tools/coverage.sh -->"
|
||||
|
||||
|
||||
def render_doc_block(records: list[Record], per_file: float, aggregate: float) -> str:
|
||||
"""The region spliced into docs/coverage.md: the floor verdict and the table."""
|
||||
failures = [
|
||||
record
|
||||
for record in records
|
||||
if record.lines_found > 0
|
||||
and record.lines_hit * 100.0 < per_file * record.lines_found
|
||||
]
|
||||
aggregate_hit = sum(record.lines_hit for record in records)
|
||||
aggregate_found = sum(record.lines_found for record in records)
|
||||
aggregate_rate = (
|
||||
100.0 * aggregate_hit / aggregate_found if aggregate_found else 0.0
|
||||
)
|
||||
aggregate_below = aggregate_hit * 100.0 < aggregate * aggregate_found
|
||||
|
||||
lines = [DOC_BEGIN, ""]
|
||||
lines.append("```")
|
||||
if failures or aggregate_below:
|
||||
lines.append(
|
||||
f"coverage_floor: FAILED -- {len(failures) + (1 if aggregate_below else 0)} "
|
||||
"floor(s) violated in the report this run captured:"
|
||||
)
|
||||
for record in failures:
|
||||
lines.append(
|
||||
f" {record.display}: {record.rate:.1f}% lines "
|
||||
f"({record.lines_hit}/{record.lines_found}) is below the "
|
||||
f"{per_file:g}% per-file floor"
|
||||
)
|
||||
if aggregate_below:
|
||||
lines.append(
|
||||
f" src/ aggregate: {aggregate_rate:.1f}% lines "
|
||||
f"({aggregate_hit}/{aggregate_found}) is below the "
|
||||
f"{aggregate:g}% aggregate floor"
|
||||
)
|
||||
else:
|
||||
lines.append(
|
||||
f"coverage_floor: ok -- {len(records)} files under src/, aggregate "
|
||||
f"{aggregate_rate:.1f}% lines ({aggregate_hit}/{aggregate_found}), every file "
|
||||
f"at or above {per_file:g}%, aggregate target {aggregate:g}%"
|
||||
)
|
||||
lines.append("```")
|
||||
lines.append("")
|
||||
lines.append(render_table(records, markdown=True))
|
||||
lines.append("")
|
||||
lines.append(DOC_END)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def update_doc(path: Path, block: str) -> None:
|
||||
"""Replace the region between the markers in `path` with `block`."""
|
||||
lines = path.read_text(encoding="utf-8").splitlines()
|
||||
try:
|
||||
begin = next(i for i, line in enumerate(lines) if line.startswith(DOC_BEGIN))
|
||||
end = next(i for i, line in enumerate(lines) if line.startswith(DOC_END))
|
||||
except StopIteration:
|
||||
raise ValueError(f"no {DOC_BEGIN} / {DOC_END} markers") from None
|
||||
if end < begin:
|
||||
raise ValueError("the end marker precedes the begin marker")
|
||||
merged = lines[:begin] + block.splitlines() + lines[end + 1 :]
|
||||
path.write_text("\n".join(merged) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Fail when src/** coverage drops below the project's floors.",
|
||||
@@ -210,6 +279,14 @@ def main(argv: list[str]) -> int:
|
||||
default="table",
|
||||
help="table layout; markdown is what docs/coverage.md carries",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--write-doc",
|
||||
metavar="PATH",
|
||||
help=(
|
||||
"rewrite the generated region of the markdown file at PATH from the "
|
||||
"report, and exit 0; the floor verdict is written inside that region"
|
||||
),
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
report = Path(args.report)
|
||||
@@ -235,6 +312,21 @@ def main(argv: list[str]) -> int:
|
||||
)
|
||||
return 2
|
||||
|
||||
if args.write_doc:
|
||||
try:
|
||||
update_doc(
|
||||
Path(args.write_doc),
|
||||
render_doc_block(records, args.per_file, args.aggregate),
|
||||
)
|
||||
except (OSError, ValueError) as error:
|
||||
print(
|
||||
f"coverage_floor: cannot update {args.write_doc}: {error}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
print(f"coverage_floor: wrote the measured table to {args.write_doc}")
|
||||
return 0
|
||||
|
||||
if args.table:
|
||||
print(render_table(records, args.format == "markdown"))
|
||||
print()
|
||||
|
||||
Reference in new issue
Block a user