commit 87a90cf6d71cbb27dfe2d64e4c2f093c2510b776
parent da316347842757d2f7868597042175842e737772
Author: triesap <tyson@radroots.org>
Date: Tue, 31 Mar 2026 17:14:27 +0000
build: remove local coverage contract surface
Diffstat:
6 files changed, 57 insertions(+), 259 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,20 +0,0 @@
-COVERAGE_OUTPUT_DIR := target/coverage
-COVERAGE_SUMMARY := $(COVERAGE_OUTPUT_DIR)/summary.json
-COVERAGE_LCOV := $(COVERAGE_OUTPUT_DIR)/lcov.info
-COVERAGE_THRESHOLDS := contract/coverage/thresholds.toml
-COVERAGE_INCLUDE := contract/coverage/include.txt
-
-.PHONY: coverage-report coverage-gate
-
-coverage-report:
- mkdir -p $(COVERAGE_OUTPUT_DIR)
- cargo +nightly llvm-cov clean --workspace
- cargo +nightly llvm-cov --workspace --all-features --branch --no-report
- cargo +nightly llvm-cov report --json --summary-only --output-path $(COVERAGE_SUMMARY)
- cargo +nightly llvm-cov report --lcov --output-path $(COVERAGE_LCOV)
- cargo +nightly llvm-cov report --summary-only
- @echo "coverage summary: $(COVERAGE_SUMMARY)"
- @echo "coverage lcov: $(COVERAGE_LCOV)"
-
-coverage-gate: coverage-report
- python3 scripts/ci/verify_coverage.py --thresholds $(COVERAGE_THRESHOLDS) --summary $(COVERAGE_SUMMARY) --lcov $(COVERAGE_LCOV) --include $(COVERAGE_INCLUDE)
diff --git a/contract/coverage/POLICY.md b/contract/coverage/POLICY.md
@@ -1,32 +0,0 @@
-# radrootsd coverage policy
-
-this policy defines the required rust coverage gate for this repository.
-
-## gate contract
-
-- executable lines coverage: 100.0
-- function coverage: 100.0
-- branch coverage: 100.0
-- region coverage: 100.0
-- branch records must be present in coverage data
-
-all thresholds are merge-blocking and release-blocking.
-
-## toolchain contract
-
-- use nightly rust for coverage runs
-- use `cargo llvm-cov` with `--branch`
-- export summary json and lcov artifacts per run
-- evaluate gates from `contract/coverage/thresholds.toml`
-
-## enforcement contract
-
-- evaluate coverage for the repository crate, not only aggregated workspace totals
-- fail hard when any required metric is below threshold, including regions
-- fail hard when required branch records are missing
-- fail hard when required covered files in `contract/coverage/include.txt` are absent from summary output
-
-## local and ci contract
-
-- local development may run report-only commands
-- ci must run strict gate commands with default thresholds
diff --git a/contract/coverage/include.txt b/contract/coverage/include.txt
@@ -1,7 +0,0 @@
-# required source files that must appear in coverage summary output
-src/app/config.rs
-src/app/runtime.rs
-src/core/nip46/session.rs
-src/core/state.rs
-src/lib.rs
-src/main.rs
diff --git a/contract/coverage/thresholds.toml b/contract/coverage/thresholds.toml
@@ -1,6 +0,0 @@
-[coverage]
-line_percent = 100.0
-function_percent = 100.0
-branch_percent = 100.0
-region_percent = 100.0
-require_branch_records = true
diff --git a/flake.nix b/flake.nix
@@ -44,14 +44,22 @@
];
libraryPath = pkgs.lib.makeLibraryPath basePackages;
includePath = pkgs.lib.makeSearchPathOutput "dev" "include" basePackages;
+ llvmToolsBin = "${pkgs.llvmPackages.llvm}/bin";
darwinLdFlags = pkgs.lib.optionalString pkgs.stdenv.isDarwin "-L${pkgs.darwin.libiconv}/lib";
darwinRustFlags = pkgs.lib.optionalString pkgs.stdenv.isDarwin "-L native=${pkgs.darwin.libiconv}/lib";
+ coveragePackages = basePackages ++ [
+ pkgs.llvmPackages.llvm
+ ];
mkApp =
- name: text:
+ name:
+ {
+ runtimeInputs ? basePackages,
+ text,
+ }:
let
script = pkgs.writeShellApplication {
inherit name;
- runtimeInputs = basePackages;
+ inherit runtimeInputs;
text = ''
set -euo pipefail
repo_root="$(git rev-parse --show-toplevel)"
@@ -75,10 +83,12 @@
f {
inherit
basePackages
+ coveragePackages
darwinLdFlags
darwinRustFlags
includePath
libraryPath
+ llvmToolsBin
mkApp
pkgs
rustToolchain
@@ -88,19 +98,53 @@
in
{
apps = forAllSystems (
- { mkApp, ... }:
+ {
+ coveragePackages,
+ llvmToolsBin,
+ mkApp,
+ ...
+ }:
rec {
default = check;
- check = mkApp "check" ''
- cargo metadata --format-version 1 --no-deps
- cargo check
- '';
- fmt = mkApp "fmt" ''
- cargo fmt --all --check
- '';
- test = mkApp "test" ''
- cargo test
- '';
+ check = mkApp "check" {
+ text = ''
+ cargo metadata --format-version 1 --no-deps
+ cargo check
+ '';
+ };
+ coverage-report = mkApp "coverage-report" {
+ runtimeInputs = coveragePackages;
+ text = ''
+ export PATH="$HOME/.cargo/bin:$PATH"
+ cargo +nightly llvm-cov --version >/dev/null 2>&1 || {
+ echo "cargo +nightly llvm-cov must be available to run coverage-report" >&2
+ exit 1
+ }
+ export LLVM_COV="${llvmToolsBin}/llvm-cov"
+ export LLVM_PROFDATA="${llvmToolsBin}/llvm-profdata"
+ coverage_target_dir="$(mktemp -d "''${TMPDIR:-/tmp}/radrootsd-llvm-cov.XXXXXX")"
+ trap 'rm -rf "$coverage_target_dir"' EXIT
+ export CARGO_TARGET_DIR="$coverage_target_dir"
+ mkdir -p target/coverage
+ cargo +nightly llvm-cov clean --workspace
+ cargo +nightly llvm-cov --workspace --all-features --branch --no-report
+ cargo +nightly llvm-cov report --json --summary-only --output-path target/coverage/summary.json
+ cargo +nightly llvm-cov report --lcov --output-path target/coverage/lcov.info
+ cargo +nightly llvm-cov report --summary-only
+ echo "coverage summary: target/coverage/summary.json"
+ echo "coverage lcov: target/coverage/lcov.info"
+ '';
+ };
+ fmt = mkApp "fmt" {
+ text = ''
+ cargo fmt --all --check
+ '';
+ };
+ test = mkApp "test" {
+ text = ''
+ cargo test
+ '';
+ };
}
);
diff --git a/scripts/ci/verify_coverage.py b/scripts/ci/verify_coverage.py
@@ -1,181 +0,0 @@
-#!/usr/bin/env python3
-
-from __future__ import annotations
-
-import argparse
-import json
-import pathlib
-import sys
-import tomllib
-
-
-def parse_args() -> argparse.Namespace:
- parser = argparse.ArgumentParser(description="verify coverage thresholds")
- parser.add_argument("--thresholds", required=True, type=pathlib.Path)
- parser.add_argument("--summary", required=True, type=pathlib.Path)
- parser.add_argument("--lcov", required=True, type=pathlib.Path)
- parser.add_argument("--include", required=False, type=pathlib.Path)
- return parser.parse_args()
-
-
-def read_thresholds(path: pathlib.Path) -> dict[str, float | bool]:
- payload = tomllib.loads(path.read_text(encoding="utf-8"))
- coverage = payload.get("coverage")
- if not isinstance(coverage, dict):
- raise ValueError("missing [coverage] table")
- return {
- "line_percent": float(coverage["line_percent"]),
- "function_percent": float(coverage["function_percent"]),
- "branch_percent": float(coverage["branch_percent"]),
- "region_percent": float(coverage["region_percent"]),
- "require_branch_records": bool(coverage["require_branch_records"]),
- }
-
-
-def read_totals(path: pathlib.Path) -> dict[str, object]:
- payload = json.loads(path.read_text(encoding="utf-8"))
- data = payload.get("data")
- if not isinstance(data, list) or not data:
- raise ValueError("coverage summary missing data entries")
- totals = data[0].get("totals")
- if not isinstance(totals, dict):
- raise ValueError("coverage summary missing totals")
- return totals
-
-
-def read_covered_files(path: pathlib.Path) -> set[str]:
- payload = json.loads(path.read_text(encoding="utf-8"))
- data = payload.get("data")
- if not isinstance(data, list) or not data:
- raise ValueError("coverage summary missing data entries")
- files = data[0].get("files")
- if not isinstance(files, list):
- raise ValueError("coverage summary missing files")
-
- repo_root = pathlib.Path.cwd().resolve()
- covered: set[str] = set()
- for entry in files:
- if not isinstance(entry, dict):
- continue
- raw_name = entry.get("filename")
- if not isinstance(raw_name, str):
- continue
- filename = pathlib.Path(raw_name)
- if filename.is_absolute():
- try:
- rel = filename.resolve().relative_to(repo_root)
- covered.add(rel.as_posix())
- continue
- except ValueError:
- pass
- covered.add(filename.as_posix())
- return covered
-
-
-def read_required_files(path: pathlib.Path) -> set[str]:
- required: set[str] = set()
- for raw_line in path.read_text(encoding="utf-8").splitlines():
- line = raw_line.strip()
- if not line or line.startswith("#"):
- continue
- required.add(pathlib.Path(line).as_posix())
- return required
-
-
-def metric_percent(totals: dict[str, object], metric: str) -> float:
- metric_obj = totals.get(metric)
- if not isinstance(metric_obj, dict):
- raise ValueError(f"missing totals metric: {metric}")
- percent = metric_obj.get("percent")
- if not isinstance(percent, (int, float)):
- raise ValueError(f"missing percent for metric: {metric}")
- return float(percent)
-
-
-def metric_count(totals: dict[str, object], metric: str) -> int:
- metric_obj = totals.get(metric)
- if not isinstance(metric_obj, dict):
- raise ValueError(f"missing totals metric: {metric}")
- count = metric_obj.get("count")
- if not isinstance(count, int):
- raise ValueError(f"missing count for metric: {metric}")
- return count
-
-
-def lcov_branch_record_count(path: pathlib.Path) -> int:
- total = 0
- for raw_line in path.read_text(encoding="utf-8").splitlines():
- if not raw_line.startswith("BRF:"):
- continue
- value = raw_line[4:].strip()
- if not value:
- continue
- total += int(value)
- return total
-
-
-def main() -> int:
- args = parse_args()
- thresholds = read_thresholds(args.thresholds)
- totals = read_totals(args.summary)
- covered_files = read_covered_files(args.summary)
-
- checks = [
- ("lines", metric_percent(totals, "lines"), thresholds["line_percent"]),
- (
- "functions",
- metric_percent(totals, "functions"),
- thresholds["function_percent"],
- ),
- (
- "branches",
- metric_percent(totals, "branches"),
- thresholds["branch_percent"],
- ),
- (
- "regions",
- metric_percent(totals, "regions"),
- thresholds["region_percent"],
- ),
- ]
-
- errors: list[str] = []
- for name, actual, required in checks:
- if actual < float(required):
- errors.append(f"{name} coverage {actual:.4f}% is below {required:.4f}%")
-
- if thresholds["require_branch_records"]:
- branch_total = metric_count(totals, "branches")
- if branch_total <= 0:
- errors.append("summary has no branch records")
- lcov_branches = lcov_branch_record_count(args.lcov)
- if lcov_branches <= 0:
- errors.append("lcov report has no branch records")
-
- if args.include is not None:
- required_files = read_required_files(args.include)
- missing = sorted(required_files - covered_files)
- if missing:
- errors.append(
- "summary is missing required covered files: "
- + ", ".join(missing)
- )
-
- print(
- "coverage totals: "
- f"lines={metric_percent(totals, 'lines'):.4f}% "
- f"functions={metric_percent(totals, 'functions'):.4f}% "
- f"branches={metric_percent(totals, 'branches'):.4f}% "
- f"regions={metric_percent(totals, 'regions'):.4f}%"
- )
- print(f"coverage files counted: {len(covered_files)}")
- if errors:
- for error in errors:
- print(f"coverage gate error: {error}", file=sys.stderr)
- return 1
- print("coverage gate passed")
- return 0
-
-
-if __name__ == "__main__":
- raise SystemExit(main())