lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit 290b52d777967c2577cd7ab133d7d36df078fb52
parent 2a2e48496d5f2a1734dfd8fabfd596b9f1413374
Author: triesap <tyson@radroots.org>
Date:   Mon, 22 Jun 2026 02:25:37 +0000

tools: align build automation roots

- move xtask from crates into tools and expose first-class contract, coverage, release, and hygiene commands
- relocate Nix support under build/nix and update flake apps, checks, and shells
- replace legacy shell guards and release preflight scripts with typed xtask-backed lanes
- add the cargo xtask alias and keep formatting aligned with the flake formatter

Diffstat:
A.cargo/config.toml | 2++
MAGENTS.md | 9++++-----
MAGENT_INSTRUCTIONS.md | 20++++++++++----------
MCargo.toml | 2+-
Abuild/nix/apps.nix | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild/nix/checks.nix | 48++++++++++++++++++++++++++++++++++++++++++++++++
Abuild/nix/common.nix | 348+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Abuild/nix/devshells.nix | 33+++++++++++++++++++++++++++++++++
Abuild/nix/toolchains.nix | 20++++++++++++++++++++
Mcrates/sp1_host_trade/Cargo.toml | 4+---
Mcrates/test_fixtures/Cargo.toml | 4+---
Dcrates/xtask/Cargo.toml | 22----------------------
Dcrates/xtask/README | 24------------------------
Dcrates/xtask/src/coverage.rs | 4851-------------------------------------------------------------------------------
Dcrates/xtask/src/main.rs | 245-------------------------------------------------------------------------------
Dcrates/xtask/src/phase1_1.rs | 273-------------------------------------------------------------------------------
Mflake.nix | 10+++++-----
Dnix/apps.nix | 102-------------------------------------------------------------------------------
Dnix/checks.nix | 47-----------------------------------------------
Dnix/common.nix | 317-------------------------------------------------------------------------------
Dnix/devshells.nix | 33---------------------------------
Dnix/toolchains.nix | 20--------------------
Dscripts/ci/guard_no_legacy_identifiers.sh | 68--------------------------------------------------------------------
Dscripts/ci/release_preflight.sh | 41-----------------------------------------
Atools/xtask/Cargo.toml | 20++++++++++++++++++++
Atools/xtask/README | 24++++++++++++++++++++++++
Rcrates/xtask/src/contract.rs -> tools/xtask/src/contract.rs | 0
Atools/xtask/src/coverage.rs | 4851+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/xtask/src/hygiene.rs | 333+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atools/xtask/src/main.rs | 239+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
30 files changed, 6042 insertions(+), 6070 deletions(-)

diff --git a/.cargo/config.toml b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run -q -p xtask --" diff --git a/AGENTS.md b/AGENTS.md @@ -14,7 +14,7 @@ This file exists for compatibility with tools that look for AGENTS.md. ## 2. Repository operating model - This is a public open-source library workspace; optimize for durable library design, portability, determinism, and explicit contracts. -- Keep release and validation automation forge-agnostic; repo-owned scripts, Nix apps, tags, and contract metadata are canonical, while committed provider-specific workflow automation is not. +- Keep release and validation automation forge-agnostic; repo-owned xtask commands, Nix apps, tags, and contract metadata are canonical, while committed provider-specific workflow automation is not. - Prefer clean target-state changes over compatibility scaffolding unless compatibility is explicitly required. - Stay within the requested scope and the smallest coherent file set. - Do not fold unrelated cleanup, speculative refactors, or roadmap work into the same change. @@ -26,8 +26,7 @@ This file exists for compatibility with tools that look for AGENTS.md. Before editing code: - Read this file, `AGENT_INSTRUCTIONS.md`, and `README`. -- When touching Nix behavior, read `flake.nix` and the active Nix implementation files under - `nix/` until the approved `build/nix/` migration lands. +- When touching Nix behavior, read `flake.nix` and the active Nix implementation files under `build/nix/`. - Enter the canonical environment with `nix develop` or `direnv allow` before targeted cargo work. - Discover commands from checked-in repo surfaces; do not invent ad hoc workflows. - Read the current implementation and nearby tests before designing a change. @@ -40,7 +39,7 @@ Before editing code: - `nix run .#contract` - `nix run .#release-preflight` - targeted `cargo check -p <crate>` and `cargo test -p <crate>` only inside the Nix shell -- targeted `cargo run -q -p xtask -- ...` only when narrowing a repo-owned contract or export workflow +- targeted `cargo xtask contract ...`, `cargo xtask coverage ...`, `cargo xtask release ...`, or `cargo xtask hygiene ...` only when narrowing a repo-owned workflow - if Beads is active, read `.beads/PRIME.md` ## 5. Rust engineering rules @@ -59,7 +58,7 @@ Before editing code: ## 6. Contract and release discipline -- `contracts/` and `crates/xtask` are authoritative for core-library contracts, conformance, coverage, and release-candidate governance. +- `contracts/` and `tools/xtask` are authoritative for core-library contracts, conformance, coverage, hygiene, and release-candidate governance. - Behavior changes that affect public surfaces must update the relevant contract metadata, conformance vectors, export rules, or validation flows in the same change. - Keep pure flake checks and repo-aware command apps aligned with the documented Nix command map. diff --git a/AGENT_INSTRUCTIONS.md b/AGENT_INSTRUCTIONS.md @@ -32,7 +32,7 @@ Stay disciplined: - do not leave dead paths, temporary adapters, or silent fallback behavior behind This repo is a library workspace, not an app monolith. The right default is small, durable changes that preserve clean crate boundaries. -Release automation should stay forge-agnostic. Keep release truth in repo-owned scripts, Nix apps, tags, and contract metadata rather than committed provider-specific workflow files. +Release automation should stay forge-agnostic. Keep release truth in repo-owned xtask commands, Nix apps, tags, and contract metadata rather than committed provider-specific workflow files. ## 3. Preflight workflow @@ -41,8 +41,7 @@ Before editing code: - Read `AGENTS.md`. - Read this file. - Read `README` when the change touches workflow or public surfaces. -- When touching Nix behavior, read `flake.nix` and the active Nix implementation files under - `nix/` until the approved `build/nix/` migration lands. +- When touching Nix behavior, read `flake.nix` and the active Nix implementation files under `build/nix/`. - Read the relevant crate manifest, implementation files, and nearby tests before proposing a new structure. - Check `git status --short`. @@ -71,12 +70,12 @@ Use this mental model: - cross-language and cross-surface vector expectations - `docs/` - durable workflow and environment documentation -- `nix/`, `flake.nix`, `treefmt.nix` +- `build/nix/`, `flake.nix`, `treefmt.nix` - canonical environment and CI contract -- `scripts/` - - repo-owned automation used by canonical lanes +- `tools/xtask/` + - typed repo-owned automation used by canonical lanes -Do not duplicate contract knowledge between crates when `contracts/`, `contracts/conformance/`, or `xtask` already owns it. +Do not duplicate contract knowledge between crates when `contracts/`, `contracts/conformance/`, or `tools/xtask` already owns it. ## 5. Rust engineering standards @@ -136,7 +135,7 @@ Do not duplicate contract knowledge between crates when `contracts/`, `contracts ## 6. Contract, conformance, and release workflow -`contracts/`, `contracts/conformance/`, and `crates/xtask` are first-class parts of the product surface, not secondary metadata. +`contracts/`, `contracts/conformance/`, and `tools/xtask` are first-class parts of the product surface, not secondary metadata. When a change affects exported models, transforms, identifiers, or public runtime expectations: @@ -161,8 +160,9 @@ Targeted iteration inside the Nix shell: - `cargo check -p <crate>` - `cargo test -p <crate>` -- `cargo run -q -p xtask -- sdk validate` -- `cargo run -q -p xtask -- sdk release preflight` +- `cargo xtask contract validate` +- `cargo xtask release preflight` +- `cargo xtask hygiene forbidden-identifiers` Validation rules: diff --git a/Cargo.toml b/Cargo.toml @@ -42,7 +42,7 @@ members = [ "crates/trade", "crates/types", "crates/protected_store", - "crates/xtask", + "tools/xtask", ] resolver = "2" diff --git a/build/nix/apps.nix b/build/nix/apps.nix @@ -0,0 +1,102 @@ +{ + common, + config, + lib, + pkgs, + toolchains, +}: +let + stablePath = "export PATH=${toolchains.stable}/bin:$PATH"; + coveragePath = "export PATH=${toolchains.stable}/bin:${toolchains.coverage}/bin:$PATH"; + coverageShellExec = command: '' + exec nix develop .#coverage --accept-flake-config -c sh -lc ${lib.escapeShellArg command} sh "$@" + ''; + mkRepoApp = + { + name, + description ? "Run ${name} in the radroots workspace", + runtimeInputs, + command, + env ? common.exportSharedEnv, + pathPrefix ? stablePath, + }: + let + script = pkgs.writeShellApplication { + inherit name runtimeInputs; + text = '' + set -euo pipefail + + repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" + cd "$repo_root" + + ${common.ensureRepoRoot} + ${env} + ${pathPrefix} + + ${command} + ''; + }; + in + { + type = "app"; + program = "${script}/bin/${name}"; + meta.description = description; + }; +in +{ + check = mkRepoApp { + name = "check"; + description = "Run cargo check across the radroots workspace"; + runtimeInputs = common.runtimeInputs.stable; + command = common.checkCommand; + }; + + contract = mkRepoApp { + name = "contract"; + description = "Run the core-library contract lane"; + runtimeInputs = common.runtimeInputs.stable; + command = common.contractCommand; + }; + + coverage-report = mkRepoApp { + name = "coverage-report"; + description = "Generate coverage reports and blocking gate artifacts"; + runtimeInputs = common.runtimeInputs.coverage; + command = common.coverageReportCommand; + env = common.exportCoverageEnv; + pathPrefix = coveragePath; + }; + + guards = mkRepoApp { + name = "guards"; + description = "Run repository hygiene guards"; + runtimeInputs = common.runtimeInputs.stable; + command = '' + cargo run -q -p xtask -- hygiene forbidden-identifiers + ''; + }; + + fmt = mkRepoApp { + name = "fmt"; + description = "Format rust, nix, shell, and toml files"; + runtimeInputs = common.runtimeInputs.stable ++ [ + config.treefmt.build.wrapper + ]; + command = '' + cargo fmt --all + ${config.treefmt.build.wrapper}/bin/treefmt + ''; + }; + + release-preflight = mkRepoApp { + name = "release-preflight"; + description = "Run release coverage refresh and preflight validation"; + runtimeInputs = [ + pkgs.nix + ]; + command = coverageShellExec common.releasePreflightCommand; + env = common.exportCoverageEnv; + pathPrefix = coveragePath; + }; + +} diff --git a/build/nix/checks.nix b/build/nix/checks.nix @@ -0,0 +1,48 @@ +{ common, pkgs }: +let + cargoFmt = common.craneLib.cargoFmt common.commonCraneArgs; + cargoCheck = common.craneLib.mkCargoDerivation ( + common.commonCraneArgs + // { + inherit (common) cargoArtifacts; + pname = "radroots-cargo-check"; + doCheck = false; + buildPhaseCargoCommand = '' + cargo check ${common.coreContractCargoArgs} + ''; + installPhaseCommand = "mkdir -p $out"; + } + ); + cargoTest = common.craneLib.mkCargoDerivation ( + common.commonCraneArgs + // { + inherit (common) cargoArtifacts; + pname = "radroots-cargo-test"; + doCheck = false; + buildPhaseCargoCommand = '' + cargo test ${common.coreContractCargoArgs} + ''; + installPhaseCommand = "mkdir -p $out"; + } + ); +in +{ + cargo-fmt = cargoFmt; + cargo-check = cargoCheck; + cargo-test = cargoTest; + + guards = common.mkRepoCheck { + name = "repo-guards"; + runtimeInputs = [ + common.xtaskPackage + pkgs.coreutils + pkgs.gitMinimal + pkgs.gnugrep + pkgs.ripgrep + ]; + initGit = true; + command = '' + xtask hygiene forbidden-identifiers + ''; + }; +} diff --git a/build/nix/common.nix b/build/nix/common.nix @@ -0,0 +1,348 @@ +{ + crane, + lib, + pkgs, + toolchains, +}: +let + root = ../..; + cargoToml = builtins.fromTOML (builtins.readFile ../../Cargo.toml); + version = cargoToml.workspace.package.version; + darwinBuildInputs = lib.optionals pkgs.stdenv.isDarwin [ + pkgs.libiconv + ]; + repoSource = lib.sources.cleanSource root; + cargoSource = lib.fileset.toSource { + root = root; + fileset = lib.fileset.intersection (lib.fileset.fromSource repoSource) ( + lib.fileset.unions [ + ../../Cargo.toml + ../../Cargo.lock + ../../README + ../../rust-toolchain.toml + ../../contracts + ../../crates + ../../tools + ] + ); + }; + baseEnv = { + CARGO_TERM_COLOR = "always"; + LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + } + // lib.optionalAttrs pkgs.stdenv.isDarwin { + CC = "clang"; + CXX = "clang++"; + SDKROOT = pkgs.apple-sdk_14.sdkroot; + MACOSX_DEPLOYMENT_TARGET = pkgs.stdenv.hostPlatform.darwinMinVersion; + }; + sharedEnv = + baseEnv + // { + PKG_CONFIG_PATH = lib.makeSearchPathOutput "dev" "lib/pkgconfig" stableRuntimeInputs; + } + // lib.optionalAttrs pkgs.stdenv.isDarwin { + LIBRARY_PATH = lib.makeLibraryPath darwinBuildInputs; + }; + coverageEnv = sharedEnv // { + RADROOTS_COVERAGE_CARGO = "${toolchains.coverage}/bin/cargo"; + }; + cargoLlvmCov = pkgs.cargo-llvm-cov.overrideAttrs (old: { + doCheck = false; + meta = old.meta // { + broken = false; + }; + }); + exportEnv = + env: + lib.concatStringsSep "\n" ( + lib.mapAttrsToList (name: value: "export ${name}=${lib.escapeShellArg value}") env + ); + stableRuntimeInputs = + with pkgs; + [ + toolchains.stable + clang + coreutils + curl + findutils + gawk + gitMinimal + gnugrep + gnumake + gnused + jq + libsodium + llvmPackages.llvm + llvmPackages.libclang + pkg-config + python3 + ] + ++ darwinBuildInputs; + coverageRuntimeInputs = stableRuntimeInputs ++ [ + toolchains.coverage + cargoLlvmCov + ]; + releaseRuntimeInputs = coverageRuntimeInputs; + coreContractCrates = [ + "xtask" + "radroots_core" + "radroots_types" + "radroots_events" + "radroots_trade" + "radroots_identity" + "radroots_replica_db_schema" + "radroots_events_codec" + "radroots_nostr_connect" + "radroots_nostr_signer" + ]; + coreContractCargoArgs = lib.concatStringsSep " " (map (crate: "-p ${crate}") coreContractCrates); + craneLib = (crane.mkLib pkgs).overrideToolchain toolchains.stable; + commonCraneArgs = { + inherit version; + pname = "radroots"; + src = cargoSource; + strictDeps = true; + nativeBuildInputs = [ + pkgs.pkg-config + pkgs.clang + pkgs.llvmPackages.libclang + ]; + buildInputs = [ + pkgs.libsodium + ] + ++ darwinBuildInputs; + inherit (sharedEnv) + CARGO_TERM_COLOR + LIBCLANG_PATH + PKG_CONFIG_PATH + ; + }; + cargoArtifacts = craneLib.buildDepsOnly commonCraneArgs; + xtaskPackage = craneLib.buildPackage ( + commonCraneArgs + // { + inherit cargoArtifacts; + pname = "xtask"; + cargoExtraArgs = "-p xtask"; + doCheck = false; + } + ); + initGitRepo = '' + git init -q . + git config user.email "nix-check@example.invalid" + git config user.name "nix check" + git add -A . + ''; + mkRepoCheck = + { + name, + runtimeInputs, + command, + env ? sharedEnv, + initGit ? false, + linuxOnly ? false, + }: + if linuxOnly && !pkgs.stdenv.isLinux then + null + else + pkgs.runCommand name { nativeBuildInputs = runtimeInputs; } '' + export HOME="$TMPDIR/home" + mkdir -p "$HOME" + + cp -R ${repoSource} "$TMPDIR/repo" + chmod -R u+w "$TMPDIR/repo" + cd "$TMPDIR/repo" + export RADROOTS_WORKSPACE_ROOT="$PWD" + + ${exportEnv env} + ${lib.optionalString initGit initGitRepo} + + ${command} + + touch "$out" + ''; + ensureRepoRoot = '' + if [ ! -f Cargo.toml ] || [ ! -f flake.nix ]; then + echo "run this command from the radroots workspace checkout" >&2 + exit 1 + fi + export RADROOTS_WORKSPACE_ROOT="$PWD" + ''; + checkCommand = '' + cargo check --workspace + ''; + contractCommand = '' + cargo run -q -p xtask -- hygiene forbidden-identifiers + cargo check -q ${coreContractCargoArgs} + cargo test -q ${coreContractCargoArgs} + cargo run -q -p xtask -- contract validate + ''; + releasePreflightCommand = '' + cargo check -q + env -u RADROOTS_MOUNTED_RUST_CRATE_PUBLISH_POLICY cargo test -q -p xtask + cargo run -q -p xtask -- contract validate + + required_file="$(mktemp)" + trap 'rm -f "$required_file"' EXIT + cargo run -q -p xtask -- coverage required-crates > "$required_file" + + rm -rf target/coverage + mkdir -p target/coverage + + while IFS= read -r crate; do + [ -n "$crate" ] || continue + safe_crate="''${crate//-/_}" + out_dir="target/coverage/''${safe_crate}" + mkdir -p "$out_dir" + + cargo run -q -p xtask -- coverage run-crate --crate "$crate" --out "$out_dir" + cargo run -q -p xtask -- coverage report \ + --scope "$crate" \ + --summary "$out_dir/coverage-summary.json" \ + --lcov "$out_dir/coverage-lcov.info" \ + --out "$out_dir/gate-report.json" \ + --policy-gate + done < "$required_file" + + cargo run -q -p xtask -- coverage refresh-summary \ + --reports-root target/coverage \ + --out target/coverage/coverage-refresh.tsv \ + --status-out target/coverage/coverage-refresh-status.tsv + + cargo run -q -p xtask -- release preflight + echo "release preflight complete" + ''; + coverageReportCommand = '' + rm -rf target/coverage-report + mkdir -p target/coverage-report + : > target/coverage-report/coverage-report-status.txt + + workspace_crates_file="$(mktemp)" + required_crates_file="$(mktemp)" + trap 'rm -f "$workspace_crates_file" "$required_crates_file"' EXIT + + cargo run -q -p xtask -- coverage workspace-crates > "$workspace_crates_file" + while IFS= read -r crate; do + [ -n "''${crate}" ] || continue + safe_crate="''${crate//-/_}" + run_dir="target/coverage-report/''${safe_crate}" + mkdir -p "''${run_dir}" + status="ok" + + if ! cargo run -q -p xtask -- coverage run-crate --crate "''${crate}" --out "''${run_dir}"; then + status="run-failed" + fi + + if [ "''${status}" = "ok" ] && ! cargo run -q -p xtask -- coverage report \ + --scope "''${crate}" \ + --summary "''${run_dir}/coverage-summary.json" \ + --lcov "''${run_dir}/coverage-lcov.info" \ + --out "''${run_dir}/coverage-gate-summary.json" \ + --fail-under-exec-lines 0 \ + --fail-under-functions 0 \ + --fail-under-regions 0 \ + --fail-under-branches 0; then + status="report-failed" + fi + + if [ "''${status}" != "ok" ]; then + cat > "''${run_dir}/coverage-gate-summary.json" <<EOF + { + "scope": "''${crate}", + "thresholds": { + "executable_lines": 0, + "functions": 0, + "regions": 0, + "branches": 0, + "branches_required": false + }, + "measured": { + "executable_lines_percent": 0, + "executable_lines_source": "da", + "functions_percent": 0, + "branches_percent": null, + "branches_available": false, + "summary_lines_percent": 0, + "summary_regions_percent": 0 + }, + "counts": { + "executable_lines": { + "covered": 0, + "total": 0 + }, + "branches": { + "covered": 0, + "total": 0 + } + }, + "result": { + "pass": false, + "fail_reasons": [ + "''${status}" + ] + } + } + EOF + fi + + echo "''${crate}:''${status}" >> target/coverage-report/coverage-report-status.txt + done < "$workspace_crates_file" + + cargo run -q -p xtask -- coverage required-crates > "$required_crates_file" + while IFS= read -r crate; do + [ -n "''${crate}" ] || continue + safe_crate="''${crate//-/_}" + crate_dir="target/coverage-report/''${safe_crate}" + crate_status="$(awk -F: -v crate="''${crate}" '$1 == crate { status = $2 } END { print status }' target/coverage-report/coverage-report-status.txt)" + + if [ ! -f "''${crate_dir}/coverage-summary.json" ] || [ ! -f "''${crate_dir}/coverage-lcov.info" ]; then + fail_reason="missing-coverage-artifacts" + if [ -n "''${crate_status}" ] && [ "''${crate_status}" != "ok" ]; then + fail_reason="''${crate_status}" + fi + + cargo run -q -p xtask -- coverage report-missing \ + --scope "''${crate}" \ + --out "''${crate_dir}/coverage-gate-blocking.json" \ + --reason "''${fail_reason}" + continue + fi + + cargo run -q -p xtask -- coverage report \ + --scope "''${crate}" \ + --summary "''${crate_dir}/coverage-summary.json" \ + --lcov "''${crate_dir}/coverage-lcov.info" \ + --out "''${crate_dir}/coverage-gate-blocking.json" \ + --policy-gate + done < "$required_crates_file" + ''; +in +{ + inherit + cargoLlvmCov + cargoArtifacts + checkCommand + commonCraneArgs + contractCommand + coverageEnv + coverageReportCommand + craneLib + ensureRepoRoot + mkRepoCheck + releasePreflightCommand + coreContractCargoArgs + sharedEnv + version + xtaskPackage + ; + + exportCoverageEnv = exportEnv coverageEnv; + exportSharedEnv = exportEnv sharedEnv; + + runtimeInputs = { + stable = stableRuntimeInputs; + coverage = coverageRuntimeInputs; + release = releaseRuntimeInputs; + }; +} diff --git a/build/nix/devshells.nix b/build/nix/devshells.nix @@ -0,0 +1,33 @@ +{ + common, + pkgs, + toolchains, +}: +let + defaultHook = '' + ${common.exportSharedEnv} + export PATH=${toolchains.stable}/bin:$PATH + ''; + coverageHook = '' + ${common.exportCoverageEnv} + export PATH=${toolchains.stable}/bin:${toolchains.coverage}/bin:$PATH + ''; +in +{ + default = pkgs.mkShell { + packages = common.runtimeInputs.stable ++ [ + common.cargoLlvmCov + ]; + shellHook = defaultHook; + }; + + coverage = pkgs.mkShell { + packages = common.runtimeInputs.release; + shellHook = coverageHook; + }; + + release = pkgs.mkShell { + packages = common.runtimeInputs.release; + shellHook = coverageHook; + }; +} diff --git a/build/nix/toolchains.nix b/build/nix/toolchains.nix @@ -0,0 +1,20 @@ +{ pkgs }: +let + toolchain = builtins.fromTOML (builtins.readFile ../../rust-toolchain.toml); + stableVersion = toolchain.toolchain.channel; + stableTargets = toolchain.toolchain.targets or [ ]; + stableExtensions = [ + "clippy" + "rust-analyzer" + "rust-src" + "rustfmt" + ]; +in +{ + stable = pkgs.rust-bin.stable.${stableVersion}.default.override { + extensions = stableExtensions; + targets = stableTargets; + }; + + coverage = pkgs.rust-bin.fromRustupToolchainFile ../../rust-toolchain-coverage.toml; +} diff --git a/crates/sp1_host_trade/Cargo.toml b/crates/sp1_host_trade/Cargo.toml @@ -40,6 +40,4 @@ radroots_events = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["rt-multi-thread"] } [lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = [ - 'cfg(coverage_nightly)', -] } +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/test_fixtures/Cargo.toml b/crates/test_fixtures/Cargo.toml @@ -12,6 +12,4 @@ description = "Deterministic fixture identities and endpoint constants" authors = ["Tyson Lupul <tyson@radroots.org>"] [lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = [ - 'cfg(coverage_nightly)', -] } +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml @@ -1,22 +0,0 @@ -[package] -name = "xtask" -version = "0.1.0-alpha.2" -edition.workspace = true -rust-version.workspace = true -description = "Internal workspace automation tasks" -readme = "README" -license.workspace = true -publish = false -authors = ["Tyson Lupul <tyson@radroots.org>"] - -[dependencies] -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -sha2 = { workspace = true } -hex = { workspace = true } -toml = { workspace = true } - -[lints.rust] -unexpected_cfgs = { level = "warn", check-cfg = [ - 'cfg(coverage_nightly)', -] } diff --git a/crates/xtask/README b/crates/xtask/README @@ -1,24 +0,0 @@ -# xtask - -This is the README for `xtask`, which provides internal workspace automation -tasks for the `radroots` core libraries. - -## Overview - - * an internal `cargo xtask` runner for workspace-oriented development and - release workflows; - * `sdk` subcommands that export TypeScript bundles, crate models, and related - artifacts; - * command-dispatch code used for manifest, contract, and artifact generation - paths inside the workspace; - * a non-published binary crate used as tooling rather than as a library - dependency. - -## Copyright - -Except as otherwise noted, all files in the `xtask` distribution are - - Copyright (c) 2020-2026 Tyson Lupul - -For information on usage and redistribution, and for a DISCLAIMER OF ALL -WARRANTIES, see LICENSE included in the `xtask` distribution. diff --git a/crates/xtask/src/coverage.rs b/crates/xtask/src/coverage.rs @@ -1,4851 +0,0 @@ -#![forbid(unsafe_code)] - -use std::ffi::OsString; -use std::fs; -use std::path::Path; -use std::path::PathBuf; -use std::process::Command; -use std::{collections::BTreeMap, collections::BTreeSet, io::Write}; - -use serde::Deserialize; -use serde::Serialize; - -#[derive(Debug, Clone)] -pub struct CoverageSummary { - pub functions_percent: f64, - pub summary_lines_percent: f64, - pub summary_regions_percent: f64, -} - -#[derive(Debug, Clone, Copy)] -struct DetailedCoverageSummary { - functions_percent: f64, - regions_percent: f64, -} - -#[derive(Debug, Clone, Copy)] -pub enum ExecutableSource { - Da, - LfLh, -} - -#[derive(Debug, Clone)] -pub struct LcovCoverage { - pub executable_total: u64, - pub executable_covered: u64, - pub executable_percent: f64, - pub executable_source: ExecutableSource, - pub branch_total: u64, - pub branch_covered: u64, - pub branches_available: bool, - pub branch_percent: Option<f64>, -} - -#[derive(Debug, Clone, Copy)] -pub struct CoverageThresholds { - pub fail_under_exec_lines: f64, - pub fail_under_functions: f64, - pub fail_under_regions: f64, - pub fail_under_branches: f64, - pub require_branches: bool, -} - -#[derive(Debug, Clone)] -pub struct CoverageGateResult { - pub pass: bool, - pub fail_reasons: Vec<String>, -} - -#[derive(Debug, Serialize, Deserialize)] -struct CoverageGateReport { - scope: String, - thresholds: CoverageGateReportThresholds, - measured: CoverageGateReportMeasured, - counts: CoverageGateReportCounts, - result: CoverageGateReportResult, -} - -#[derive(Debug, Serialize, Deserialize)] -struct CoverageGateReportThresholds { - executable_lines: f64, - functions: f64, - regions: f64, - branches: f64, - branches_required: bool, -} - -#[derive(Debug, Serialize, Deserialize)] -struct CoverageGateReportMeasured { - executable_lines_percent: f64, - executable_lines_source: String, - functions_percent: f64, - branches_percent: Option<f64>, - branches_available: bool, - summary_lines_percent: f64, - summary_regions_percent: f64, -} - -#[derive(Debug, Serialize, Deserialize)] -struct CoverageGateReportCounts { - executable_lines: CoverageCount, - branches: CoverageCount, -} - -#[derive(Debug, Serialize, Deserialize)] -struct CoverageCount { - covered: u64, - total: u64, -} - -#[derive(Debug, Serialize, Deserialize)] -struct CoverageGateReportResult { - pass: bool, - fail_reasons: Vec<String>, -} - -#[derive(Debug, Deserialize)] -struct LlvmCovSummaryRoot { - data: Vec<LlvmCovSummaryData>, -} - -#[derive(Debug, Deserialize)] -struct LlvmCovSummaryData { - totals: LlvmCovSummaryTotals, -} - -#[derive(Debug, Deserialize)] -struct LlvmCovSummaryTotals { - functions: LlvmCovSummaryMetric, - lines: LlvmCovSummaryMetric, - regions: LlvmCovSummaryMetric, -} - -#[derive(Debug, Deserialize)] -struct LlvmCovSummaryMetric { - percent: f64, -} - -#[derive(Debug, Deserialize)] -struct LlvmCovDetailsRoot { - data: Vec<LlvmCovDetailsData>, -} - -#[derive(Debug, Deserialize)] -struct LlvmCovDetailsData { - #[serde(default)] - functions: Vec<LlvmCovFunction>, -} - -#[derive(Debug, Deserialize)] -struct LlvmCovFunction { - count: u64, - #[serde(default)] - filenames: Vec<String>, - #[serde(default)] - regions: Vec<[u64; 8]>, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct FunctionCoverageKey { - filenames: Vec<String>, - regions: Vec<RegionCoverageKey>, -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct RegionCoverageKey { - line_start: u64, - column_start: u64, - line_end: u64, - column_end: u64, - kind: u64, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub(crate) struct CoveragePolicyFile { - gate: CoveragePolicyGate, - required: CoverageRequiredList, - #[serde(default)] - overrides: BTreeMap<String, CoveragePolicyOverride>, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub(crate) struct CoveragePolicyGate { - fail_under_exec_lines: f64, - fail_under_functions: f64, - fail_under_regions: f64, - fail_under_branches: f64, - require_branches: bool, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub(crate) struct CoverageRequiredList { - crates: Vec<String>, -} - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub(crate) struct CoveragePolicyOverride { - fail_under_exec_lines: Option<f64>, - fail_under_functions: Option<f64>, - fail_under_regions: Option<f64>, - fail_under_branches: Option<f64>, - require_branches: Option<bool>, - temporary: bool, - reason: String, -} - -#[derive(Debug, Deserialize)] -struct WorkspaceManifest { - workspace: WorkspaceMembers, -} - -#[derive(Debug, Deserialize)] -struct WorkspaceMembers { - members: Vec<String>, -} - -#[derive(Debug, Deserialize)] -struct PackageManifest { - package: PackageSection, -} - -#[derive(Debug, Deserialize)] -struct PackageSection { - name: String, -} - -#[derive(Debug, Deserialize, Default)] -struct CoverageProfilesFile { - #[serde(default)] - profiles: CoverageProfilesSection, -} - -#[derive(Debug, Deserialize, Default)] -struct CoverageProfilesSection { - #[serde(default)] - default: CoverageProfileRaw, - #[serde(default)] - crates: BTreeMap<String, CoverageProfileRaw>, -} - -#[derive(Debug, Deserialize, Default, Clone)] -struct CoverageProfileRaw { - no_default_features: Option<bool>, - features: Option<Vec<String>>, - test_threads: Option<u32>, -} - -#[derive(Debug, Clone)] -struct CoverageProfile { - no_default_features: bool, - features: Vec<String>, - test_threads: Option<u32>, -} - -#[cfg_attr(not(test), allow(dead_code))] -pub fn read_summary(path: &Path) -> Result<CoverageSummary, String> { - read_summary_for_scope(path, None) -} - -fn read_summary_for_scope(path: &Path, scope: Option<&str>) -> Result<CoverageSummary, String> { - let raw = match fs::read_to_string(path) { - Ok(raw) => raw, - Err(err) => return Err(format!("failed to read summary {}: {err}", path.display())), - }; - let parsed: LlvmCovSummaryRoot = match serde_json::from_str(&raw) { - Ok(parsed) => parsed, - Err(err) => return Err(format!("failed to parse summary {}: {err}", path.display())), - }; - let totals = match parsed.data.first() { - Some(entry) => &entry.totals, - None => return Err(format!("summary data is empty in {}", path.display())), - }; - - let mut summary = CoverageSummary { - functions_percent: totals.functions.percent, - summary_lines_percent: totals.lines.percent, - summary_regions_percent: totals.regions.percent, - }; - - let details_path = coverage_details_path(path); - if details_path.exists() { - let normalized = read_detailed_summary(&details_path, scope)?; - if (summary.functions_percent - 100.0).abs() < f64::EPSILON { - summary.summary_regions_percent = normalized.regions_percent; - } - } - - Ok(summary) -} - -fn coverage_details_path(summary_path: &Path) -> PathBuf { - summary_path - .parent() - .unwrap_or_else(|| Path::new(".")) - .join("coverage-details.json") -} - -fn read_detailed_summary( - path: &Path, - scope: Option<&str>, -) -> Result<DetailedCoverageSummary, String> { - let raw = match fs::read_to_string(path) { - Ok(raw) => raw, - Err(err) => { - return Err(format!( - "failed to read coverage details {}: {err}", - path.display() - )); - } - }; - let parsed: LlvmCovDetailsRoot = match serde_json::from_str(&raw) { - Ok(parsed) => parsed, - Err(err) => { - return Err(format!( - "failed to parse coverage details {}: {err}", - path.display() - )); - } - }; - let Some(entry) = parsed.data.first() else { - return Err(format!( - "coverage details data is empty in {}", - path.display() - )); - }; - - let mut functions_by_key: BTreeMap<FunctionCoverageKey, Vec<&LlvmCovFunction>> = - BTreeMap::new(); - for function in &entry.functions { - if function.filenames.is_empty() || function.regions.is_empty() { - continue; - } - let key = FunctionCoverageKey { - filenames: function.filenames.clone(), - regions: function - .regions - .iter() - .map(|region| RegionCoverageKey { - line_start: region[0], - column_start: region[1], - line_end: region[2], - column_end: region[3], - kind: region[7], - }) - .collect(), - }; - functions_by_key.entry(key).or_default().push(function); - } - - if functions_by_key.is_empty() { - return Err(format!( - "coverage details functions are empty in {}", - path.display() - )); - } - - let mut regions_total = 0_u64; - let mut regions_covered = 0_u64; - let mut functions_total = 0_u64; - let mut functions_covered = 0_u64; - let mut source_cache: BTreeMap<String, Option<String>> = BTreeMap::new(); - let scope_filter = scope.map(scope_path_fragment); - for variants in functions_by_key.values() { - if let Some(scope_filter) = scope_filter.as_deref() - && !variants.iter().any(|function| { - function - .filenames - .iter() - .any(|filename| filename.contains(scope_filter)) - }) - { - continue; - } - functions_total = functions_total.saturating_add(1); - if variants.iter().any(|function| function.count > 0) { - functions_covered = functions_covered.saturating_add(1); - } - let mut group_regions: BTreeMap<RegionCoverageKey, bool> = BTreeMap::new(); - for function in variants { - for region in &function.regions { - let key = RegionCoverageKey { - line_start: region[0], - column_start: region[1], - line_end: region[2], - column_end: region[3], - kind: region[7], - }; - let covered = region[4] > 0; - group_regions - .entry(key) - .and_modify(|existing| *existing |= covered) - .or_insert(covered); - } - } - let primary_filename = variants - .first() - .and_then(|function| function.filenames.first()) - .map(String::as_str); - for (region, covered) in group_regions { - if !covered - && primary_filename.is_some_and(|filename| { - is_ignorable_synthetic_region(filename, &region, &mut source_cache) - }) - { - continue; - } - regions_total = regions_total.saturating_add(1); - if covered { - regions_covered = regions_covered.saturating_add(1); - } - } - } - - Ok(DetailedCoverageSummary { - functions_percent: percentage(functions_covered, functions_total), - regions_percent: percentage(regions_covered, regions_total), - }) -} - -fn scope_path_fragment(scope: &str) -> String { - let crate_dir = scope.strip_prefix("radroots_").unwrap_or(scope); - format!("/crates/{crate_dir}/src/") -} - -fn percentage(covered: u64, total: u64) -> f64 { - if total == 0 { - 100.0 - } else { - (covered as f64 / total as f64) * 100.0 - } -} - -fn is_ignorable_synthetic_region( - filename: &str, - region: &RegionCoverageKey, - source_cache: &mut BTreeMap<String, Option<String>>, -) -> bool { - if region.line_start != region.line_end { - return false; - } - let source = source_cache - .entry(filename.to_string()) - .or_insert_with(|| fs::read_to_string(filename).ok()); - let Some(source) = source.as_ref() else { - return false; - }; - let Some(line) = source - .lines() - .nth(region.line_start.saturating_sub(1) as usize) - else { - return false; - }; - let start = region.column_start.saturating_sub(1) as usize; - let end = region.column_end.saturating_sub(1) as usize; - let slice = line.get(start..end); - if region.column_end == region.column_start + 1 && slice == Some("?") { - return true; - } - - filename.ends_with("/tests.rs") - && line.contains("panic!(\"unexpected") - && matches!(slice, Some("other") | Some("panic!")) -} - -impl CoveragePolicyFile { - pub(crate) fn thresholds(&self) -> CoverageThresholds { - CoverageThresholds { - fail_under_exec_lines: self.gate.fail_under_exec_lines, - fail_under_functions: self.gate.fail_under_functions, - fail_under_regions: self.gate.fail_under_regions, - fail_under_branches: self.gate.fail_under_branches, - require_branches: self.gate.require_branches, - } - } - - pub(crate) fn thresholds_for_scope(&self, scope: &str) -> CoverageThresholds { - let base = self.thresholds(); - let Some(override_policy) = self.overrides.get(scope) else { - return base; - }; - CoverageThresholds { - fail_under_exec_lines: override_policy - .fail_under_exec_lines - .unwrap_or(base.fail_under_exec_lines), - fail_under_functions: override_policy - .fail_under_functions - .unwrap_or(base.fail_under_functions), - fail_under_regions: override_policy - .fail_under_regions - .unwrap_or(base.fail_under_regions), - fail_under_branches: override_policy - .fail_under_branches - .unwrap_or(base.fail_under_branches), - require_branches: override_policy - .require_branches - .unwrap_or(base.require_branches), - } - } - - pub(crate) fn required_crates(&self) -> Result<Vec<String>, String> { - if self.required.crates.is_empty() { - return Err("coverage required crates list must not be empty".to_string()); - } - let mut seen = BTreeSet::new(); - for crate_name in &self.required.crates { - if crate_name.trim().is_empty() { - return Err( - "coverage required crates list includes an empty crate name".to_string() - ); - } - if !seen.insert(crate_name.clone()) { - return Err(format!( - "coverage required crates list includes duplicate crate {crate_name}" - )); - } - } - Ok(self.required.crates.clone()) - } - - fn validate_overrides(&self) -> Result<(), String> { - let required_crates = self.required_crates()?; - let required_set: BTreeSet<_> = required_crates.into_iter().collect(); - let base = self.thresholds(); - for (crate_name, override_policy) in &self.overrides { - if !required_set.contains(crate_name) { - return Err(format!( - "coverage override {crate_name} must target a required crate" - )); - } - if !override_policy.temporary { - return Err(format!( - "coverage override {crate_name} must set temporary = true" - )); - } - if override_policy.reason.trim().is_empty() { - return Err(format!( - "coverage override {crate_name} must include a non-empty reason" - )); - } - validate_override_threshold( - crate_name, - "fail_under_exec_lines", - override_policy.fail_under_exec_lines, - base.fail_under_exec_lines, - )?; - validate_override_threshold( - crate_name, - "fail_under_functions", - override_policy.fail_under_functions, - base.fail_under_functions, - )?; - validate_override_threshold( - crate_name, - "fail_under_regions", - override_policy.fail_under_regions, - base.fail_under_regions, - )?; - validate_override_threshold( - crate_name, - "fail_under_branches", - override_policy.fail_under_branches, - base.fail_under_branches, - )?; - if override_policy.require_branches == Some(true) && !base.require_branches { - return Err(format!( - "coverage override {crate_name} require_branches cannot be stricter than the global gate" - )); - } - } - Ok(()) - } - - pub(crate) fn required_crate_entries(&self) -> &[String] { - &self.required.crates - } -} - -fn validate_override_threshold( - crate_name: &str, - label: &str, - value: Option<f64>, - global: f64, -) -> Result<(), String> { - let Some(value) = value else { - return Ok(()); - }; - if !value.is_finite() { - return Err(format!( - "coverage override {crate_name} {label} must be finite" - )); - } - if !(0.0..=100.0).contains(&value) { - return Err(format!( - "coverage override {crate_name} {label} must be within 0..=100" - )); - } - if value > global { - return Err(format!( - "coverage override {crate_name} {label} must not exceed the global gate" - )); - } - Ok(()) -} - -pub(crate) fn coverage_policy_path(root: &Path) -> PathBuf { - root.join("contracts").join("coverage.toml") -} - -pub(crate) fn read_coverage_policy(path: &Path) -> Result<CoveragePolicyFile, String> { - let raw = match fs::read_to_string(path) { - Ok(raw) => raw, - Err(err) => { - return Err(format!( - "failed to read coverage policy {}: {err}", - path.display() - )); - } - }; - let parsed: CoveragePolicyFile = match toml::from_str(&raw) { - Ok(parsed) => parsed, - Err(err) => { - return Err(format!( - "failed to parse coverage policy {}: {err}", - path.display() - )); - } - }; - let thresholds = parsed.thresholds(); - for (label, value) in [ - ("fail_under_exec_lines", thresholds.fail_under_exec_lines), - ("fail_under_functions", thresholds.fail_under_functions), - ("fail_under_regions", thresholds.fail_under_regions), - ("fail_under_branches", thresholds.fail_under_branches), - ] { - if !value.is_finite() { - return Err(format!("coverage policy {label} must be finite")); - } - if !(0.0..=100.0).contains(&value) { - return Err(format!("coverage policy {label} must be within 0..=100")); - } - } - parsed.required_crates()?; - parsed.validate_overrides()?; - Ok(parsed) -} - -fn read_required_crates(path: &Path) -> Result<Vec<String>, String> { - read_coverage_policy(path)?.required_crates() -} - -fn read_workspace_crates(workspace_root: &Path) -> Result<Vec<String>, String> { - let packages = read_workspace_packages(workspace_root)?; - Ok(packages.into_iter().map(|(name, _)| name).collect()) -} - -fn read_workspace_packages(workspace_root: &Path) -> Result<Vec<(String, PathBuf)>, String> { - let workspace_manifest = parse_toml::<WorkspaceManifest>(&workspace_root.join("Cargo.toml"))?; - if workspace_manifest.workspace.members.is_empty() { - return Err("workspace members list must not be empty".to_string()); - } - let mut packages = Vec::with_capacity(workspace_manifest.workspace.members.len()); - let mut seen = BTreeSet::new(); - for member in workspace_manifest.workspace.members { - let package_manifest = - parse_toml::<PackageManifest>(&workspace_root.join(&member).join("Cargo.toml"))?; - let package_name = package_manifest.package.name; - if package_name.trim().is_empty() { - return Err("workspace includes an empty package name".to_string()); - } - if !seen.insert(package_name.clone()) { - return Err(format!( - "workspace includes duplicate package name {}", - package_name - )); - } - packages.push((package_name, PathBuf::from(member))); - } - Ok(packages) -} - -fn parse_toml<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> { - let raw = match fs::read_to_string(path) { - Ok(raw) => raw, - Err(err) => return Err(format!("failed to read {}: {err}", path.display())), - }; - match toml::from_str::<T>(&raw) { - Ok(parsed) => Ok(parsed), - Err(err) => Err(format!("failed to parse {}: {err}", path.display())), - } -} - -fn merge_coverage_profile( - base: CoverageProfileRaw, - overlay: CoverageProfileRaw, -) -> CoverageProfile { - CoverageProfile { - no_default_features: overlay - .no_default_features - .unwrap_or(base.no_default_features.unwrap_or(false)), - features: overlay - .features - .unwrap_or_else(|| base.features.unwrap_or_default()), - test_threads: overlay.test_threads.or(base.test_threads), - } -} - -fn read_coverage_profile( - workspace_root: &Path, - crate_name: &str, -) -> Result<CoverageProfile, String> { - let path = workspace_root - .join("contracts") - .join("coverage-profiles.toml"); - if !path.exists() { - return Ok(CoverageProfile { - no_default_features: false, - features: Vec::new(), - test_threads: None, - }); - } - let parsed = parse_toml::<CoverageProfilesFile>(&path)?; - let base = parsed.profiles.default; - let overlay = parsed - .profiles - .crates - .get(crate_name) - .cloned() - .unwrap_or_default(); - let resolved = merge_coverage_profile(base, overlay); - if resolved - .features - .iter() - .any(|feature| feature.trim().is_empty()) - { - return Err(format!( - "coverage profile for {crate_name} includes an empty feature value" - )); - } - if resolved.test_threads == Some(0) { - return Err(format!( - "coverage profile for {crate_name} must set test_threads > 0" - )); - } - Ok(resolved) -} - -pub fn read_lcov(path: &Path) -> Result<LcovCoverage, String> { - let raw = match fs::read_to_string(path) { - Ok(raw) => raw, - Err(err) => return Err(format!("failed to read lcov {}: {err}", path.display())), - }; - - let mut da_total: u64 = 0; - let mut da_covered: u64 = 0; - let mut executable_total: u64 = 0; - let mut executable_covered: u64 = 0; - let mut branch_total_lcov: u64 = 0; - let mut branch_covered_lcov: u64 = 0; - let mut branch_total_brda: u64 = 0; - let mut branch_covered_brda: u64 = 0; - - for line in raw.lines() { - if let Some(value) = line.strip_prefix("DA:") { - let Some((_, hit)) = value.split_once(',') else { - return Err(format!("invalid DA record in {}", path.display())); - }; - let hit_count: u64 = match hit.parse() { - Ok(hit_count) => hit_count, - Err(err) => { - return Err(format!( - "invalid DA hit count `{hit}` in {}: {err}", - path.display() - )); - } - }; - da_total = da_total.saturating_add(1); - if hit_count > 0 { - da_covered = da_covered.saturating_add(1); - } - continue; - } - if let Some(value) = line.strip_prefix("LF:") { - let parsed: u64 = match value.parse() { - Ok(parsed) => parsed, - Err(err) => { - return Err(format!( - "invalid LF value `{value}` in {}: {err}", - path.display() - )); - } - }; - executable_total = executable_total.saturating_add(parsed); - continue; - } - if let Some(value) = line.strip_prefix("LH:") { - let parsed: u64 = match value.parse() { - Ok(parsed) => parsed, - Err(err) => { - return Err(format!( - "invalid LH value `{value}` in {}: {err}", - path.display() - )); - } - }; - executable_covered = executable_covered.saturating_add(parsed); - continue; - } - if let Some(value) = line.strip_prefix("BRF:") { - let parsed: u64 = match value.parse() { - Ok(parsed) => parsed, - Err(err) => { - return Err(format!( - "invalid BRF value `{value}` in {}: {err}", - path.display() - )); - } - }; - branch_total_lcov = branch_total_lcov.saturating_add(parsed); - continue; - } - if let Some(value) = line.strip_prefix("BRH:") { - let parsed: u64 = match value.parse() { - Ok(parsed) => parsed, - Err(err) => { - return Err(format!( - "invalid BRH value `{value}` in {}: {err}", - path.display() - )); - } - }; - branch_covered_lcov = branch_covered_lcov.saturating_add(parsed); - continue; - } - if let Some(value) = line.strip_prefix("BRDA:") { - let fields = value.split(',').collect::<Vec<_>>(); - if fields.len() != 4 { - return Err(format!("invalid BRDA record in {}", path.display())); - } - let taken = fields[3]; - if taken == "-" { - continue; - } - let hit_count: u64 = match taken.parse() { - Ok(hit_count) => hit_count, - Err(err) => { - return Err(format!( - "invalid BRDA taken count `{taken}` in {}: {err}", - path.display() - )); - } - }; - branch_total_brda = branch_total_brda.saturating_add(1); - if hit_count > 0 { - branch_covered_brda = branch_covered_brda.saturating_add(1); - } - } - } - - let mut executable_source = ExecutableSource::Da; - let mut executable_percent = 100.0_f64; - - if da_total > 0 { - executable_total = da_total; - executable_covered = da_covered; - executable_percent = (da_covered as f64 / da_total as f64) * 100.0_f64; - } else if executable_total > 0 { - executable_source = ExecutableSource::LfLh; - executable_percent = (executable_covered as f64 / executable_total as f64) * 100.0_f64; - } - - let (branch_total, branch_covered) = if branch_total_brda > 0 { - (branch_total_brda, branch_covered_brda) - } else { - (branch_total_lcov, branch_covered_lcov) - }; - let branches_available = branch_total > 0; - let branch_percent = if branches_available { - Some((branch_covered as f64 / branch_total as f64) * 100.0_f64) - } else { - None - }; - - Ok(LcovCoverage { - executable_total, - executable_covered, - executable_percent, - executable_source, - branch_total, - branch_covered, - branches_available, - branch_percent, - }) -} - -pub fn evaluate_gate( - summary: &CoverageSummary, - lcov: &LcovCoverage, - thresholds: CoverageThresholds, -) -> CoverageGateResult { - let exec_ok = lcov.executable_percent >= thresholds.fail_under_exec_lines; - let functions_ok = summary.functions_percent >= thresholds.fail_under_functions; - let regions_ok = summary.summary_regions_percent >= thresholds.fail_under_regions; - let branch_presence_ok = !thresholds.require_branches || lcov.branches_available; - let branch_ok = lcov - .branch_percent - .is_none_or(|branch_percent| branch_percent >= thresholds.fail_under_branches); - - let pass = [ - exec_ok, - functions_ok, - regions_ok, - branch_presence_ok, - branch_ok, - ] - .into_iter() - .all(|flag| flag); - let mut fail_reasons: Vec<String> = Vec::new(); - - if !exec_ok { - fail_reasons.push(format!( - "executable_lines={:.6} < {:.6}", - lcov.executable_percent, thresholds.fail_under_exec_lines - )); - } - - if !functions_ok { - fail_reasons.push(format!( - "functions={:.6} < {:.6}", - summary.functions_percent, thresholds.fail_under_functions - )); - } - - if !regions_ok { - fail_reasons.push(format!( - "regions={:.6} < {:.6}", - summary.summary_regions_percent, thresholds.fail_under_regions - )); - } - - if thresholds.require_branches && !lcov.branches_available { - fail_reasons.push("branches=unavailable".to_string()); - } - - if lcov.branches_available && !branch_ok { - fail_reasons.push(format!( - "branches={:.6} < {:.6}", - lcov.branch_percent.unwrap_or(0.0), - thresholds.fail_under_branches - )); - } - - CoverageGateResult { pass, fail_reasons } -} - -fn executable_source_label(source: ExecutableSource) -> &'static str { - match source { - ExecutableSource::Da => "da", - ExecutableSource::LfLh => "lf_lh", - } -} - -fn parse_string_arg(args: &[String], name: &str) -> Result<String, String> { - let flag = format!("--{name}"); - let mut index = 0usize; - while index < args.len() { - if args[index] == flag { - let Some(value) = args.get(index + 1) else { - return Err(format!("missing value for --{name}")); - }; - return Ok(value.clone()); - } - index += 1; - } - Err(format!("missing --{name}")) -} - -fn parse_optional_string_arg(args: &[String], name: &str) -> Option<String> { - let flag = format!("--{name}"); - let mut index = 0usize; - while index < args.len() { - if args[index] == flag { - return args.get(index + 1).cloned(); - } - index += 1; - } - None -} - -fn parse_optional_f64_arg(args: &[String], name: &str) -> Result<Option<f64>, String> { - if let Some(raw) = parse_optional_string_arg(args, name) { - let parsed = raw - .parse::<f64>() - .map_err(|err| format!("invalid --{name} value `{raw}`: {err}"))?; - if !parsed.is_finite() { - return Err(format!("invalid --{name} value `{raw}`: must be finite")); - } - return Ok(Some(parsed)); - } - Ok(None) -} - -#[cfg_attr(not(test), allow(dead_code))] -fn parse_f64_arg(args: &[String], name: &str, default: f64) -> Result<f64, String> { - if let Some(raw) = parse_optional_string_arg(args, name) { - return raw - .parse::<f64>() - .map_err(|err| format!("invalid --{name} value `{raw}`: {err}")); - } - Ok(default) -} - -fn parse_optional_u32_arg(args: &[String], name: &str) -> Result<Option<u32>, String> { - if let Some(raw) = parse_optional_string_arg(args, name) { - let parsed = raw - .parse::<u32>() - .map_err(|err| format!("invalid --{name} value `{raw}`: {err}"))?; - return Ok(Some(parsed)); - } - Ok(None) -} - -fn parse_bool_flag(args: &[String], name: &str) -> bool { - let flag = format!("--{name}"); - args.iter().any(|arg| arg == &flag) -} - -fn has_flag(args: &[String], name: &str) -> bool { - let flag = format!("--{name}"); - args.iter().any(|arg| arg == &flag) -} - -fn workspace_root_with_override(override_root: Option<&str>) -> PathBuf { - if let Some(raw) = override_root { - let trimmed = raw.trim(); - if !trimmed.is_empty() { - return PathBuf::from(trimmed); - } - } - let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - let crates_dir = manifest_dir.parent().unwrap_or(manifest_dir); - let root = crates_dir.parent().unwrap_or(crates_dir); - root.to_path_buf() -} - -fn workspace_root() -> PathBuf { - let override_root = std::env::var("RADROOTS_WORKSPACE_ROOT").ok(); - workspace_root_with_override(override_root.as_deref()) -} - -fn run_command(mut command: Command, name: &str) -> Result<(), String> { - let status = match command.status() { - Ok(status) => status, - Err(err) => return Err(format!("failed to run {name}: {err}")), - }; - if !status.success() { - return Err(format!("{name} failed with status {status}")); - } - Ok(()) -} - -fn apply_coverage_profile_flags(command: &mut Command, profile: &CoverageProfile) { - if profile.no_default_features { - command.arg("--no-default-features"); - } - if !profile.features.is_empty() { - command.arg("--features").arg(profile.features.join(",")); - } -} - -fn prepend_toolchain_bin_to_path( - toolchain_bin: &Path, - existing_path: Option<OsString>, -) -> OsString { - match existing_path { - Some(existing) => std::env::join_paths( - std::iter::once(toolchain_bin.to_path_buf()).chain(std::env::split_paths(&existing)), - ) - .expect("joining PATH entries for coverage toolchain should succeed"), - None => OsString::from(toolchain_bin), - } -} - -fn configure_coverage_toolchain_env(command: &mut Command, toolchain_bin: &Path) { - let joined_path = prepend_toolchain_bin_to_path(toolchain_bin, std::env::var_os("PATH")); - command.env("PATH", joined_path); - - for (env_name, binary_name) in [ - ("RUSTC", "rustc"), - ("RUSTDOC", "rustdoc"), - ("LLVM_COV", "llvm-cov"), - ("LLVM_PROFDATA", "llvm-profdata"), - ] { - let binary_path = toolchain_bin.join(binary_name); - if binary_path.exists() { - command.env(env_name, binary_path); - } - } -} - -fn coverage_cargo_command_with_override(override_binary: Option<&str>) -> Command { - if let Some(binary) = override_binary { - let mut cmd = Command::new(binary); - if let Some(toolchain_bin) = Path::new(binary).parent() { - configure_coverage_toolchain_env(&mut cmd, toolchain_bin); - } - return cmd; - } - - let mut cmd = Command::new("rustup"); - cmd.arg("run").arg("nightly").arg("cargo"); - cmd -} - -fn normalized_coverage_cargo_override(raw: Option<String>) -> Option<String> { - raw.map(|raw| raw.trim().to_string()) - .filter(|raw| !raw.is_empty()) -} - -fn coverage_cargo_command() -> Command { - let override_binary = - normalized_coverage_cargo_override(std::env::var("RADROOTS_COVERAGE_CARGO").ok()); - coverage_cargo_command_with_override(override_binary.as_deref()) -} - -fn coverage_llvm_cov_command() -> Command { - let mut cmd = coverage_cargo_command(); - cmd.arg("llvm-cov"); - cmd -} - -const COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX: &str = - r"(/\.cargo/registry/|/lib/rustlib/src/rust/)"; - -fn escape_regex_literal(raw: &str) -> String { - let mut escaped = String::with_capacity(raw.len()); - for ch in raw.chars() { - match ch { - '\\' | '.' | '+' | '*' | '?' | '(' | ')' | '|' | '[' | ']' | '{' | '}' | '^' | '$' => { - escaped.push('\\'); - escaped.push(ch); - } - _ => escaped.push(ch), - } - } - escaped -} - -fn coverage_ignore_filename_regex( - workspace_root: &Path, - crate_name: &str, -) -> Result<String, String> { - let mut patterns = vec![COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX.to_string()]; - let mut found_target = false; - - for (package_name, member_path) in read_workspace_packages(workspace_root)? { - let absolute_member = workspace_root.join(member_path); - if package_name == crate_name { - found_target = true; - patterns.push(format!( - "^{}/", - escape_regex_literal(&absolute_member.join("tests").display().to_string()) - )); - continue; - } - patterns.push(format!( - "^{}/", - escape_regex_literal(&absolute_member.display().to_string()) - )); - } - - if !found_target { - return Err(format!( - "workspace coverage filters could not resolve crate directory for {crate_name}" - )); - } - - Ok(format!("({})", patterns.join("|"))) -} - -fn apply_coverage_report_filters(command: &mut Command, ignore_regex: &str) { - command.arg("--ignore-filename-regex").arg(ignore_regex); -} - -fn run_crate_with_runner_at_root( - args: &[String], - workspace_root: &Path, - runner: &mut dyn FnMut(Command, &str) -> Result<(), String>, -) -> Result<(), String> { - let crate_name = parse_string_arg(args, "crate")?; - let profile = read_coverage_profile(workspace_root, &crate_name)?; - let out_dir = if let Some(raw) = parse_optional_string_arg(args, "out") { - PathBuf::from(raw) - } else { - workspace_root - .join("target") - .join("coverage") - .join(crate_name.replace('-', "_")) - }; - let test_threads = parse_optional_u32_arg(args, "test-threads")? - .or(profile.test_threads) - .unwrap_or(1); - let ignore_regex = coverage_ignore_filename_regex(workspace_root, &crate_name)?; - - if let Err(err) = fs::create_dir_all(&out_dir) { - return Err(format!("failed to create {}: {err}", out_dir.display())); - } - - runner( - { - let mut cmd = coverage_llvm_cov_command(); - cmd.arg("clean") - .arg("--workspace") - .current_dir(workspace_root); - cmd - }, - "cargo llvm-cov clean --workspace", - )?; - - runner( - { - let mut cmd = coverage_llvm_cov_command(); - cmd.arg("-p").arg(&crate_name); - apply_coverage_profile_flags(&mut cmd, &profile); - cmd.arg("--no-report") - .arg("--branch") - .arg("--") - .arg(format!("--test-threads={test_threads}")) - .current_dir(workspace_root); - cmd - }, - "cargo llvm-cov --no-report", - )?; - - let summary_path = out_dir.join("coverage-summary.json"); - runner( - { - let mut cmd = coverage_llvm_cov_command(); - cmd.arg("report").arg("-p").arg(&crate_name); - apply_coverage_report_filters(&mut cmd, &ignore_regex); - cmd.arg("--json") - .arg("--summary-only") - .arg("--branch") - .arg("--output-path") - .arg(&summary_path) - .current_dir(workspace_root); - cmd - }, - "cargo llvm-cov report --json --summary-only", - )?; - - let details_path = out_dir.join("coverage-details.json"); - runner( - { - let mut cmd = coverage_llvm_cov_command(); - cmd.arg("report").arg("-p").arg(&crate_name); - apply_coverage_report_filters(&mut cmd, &ignore_regex); - cmd.arg("--json") - .arg("--branch") - .arg("--output-path") - .arg(&details_path) - .current_dir(workspace_root); - cmd - }, - "cargo llvm-cov report --json", - )?; - - let lcov_path = out_dir.join("coverage-lcov.info"); - runner( - { - let mut cmd = coverage_llvm_cov_command(); - cmd.arg("report").arg("-p").arg(&crate_name); - apply_coverage_report_filters(&mut cmd, &ignore_regex); - cmd.arg("--lcov") - .arg("--branch") - .arg("--output-path") - .arg(&lcov_path) - .current_dir(workspace_root); - cmd - }, - "cargo llvm-cov report --lcov", - )?; - - eprintln!("coverage summary: {}", summary_path.display()); - eprintln!("coverage details: {}", details_path.display()); - eprintln!("coverage lcov: {}", lcov_path.display()); - Ok(()) -} - -fn run_crate_with_runner( - args: &[String], - runner: &mut dyn FnMut(Command, &str) -> Result<(), String>, -) -> Result<(), String> { - let root = workspace_root(); - run_crate_with_runner_at_root(args, &root, runner) -} - -fn run_crate(args: &[String]) -> Result<(), String> { - let mut runner = run_command; - run_crate_with_runner(args, &mut runner) -} - -fn report_gate_with_root(args: &[String], root: &Path) -> Result<(), String> { - let scope = parse_string_arg(args, "scope")?; - let summary_path = PathBuf::from(parse_string_arg(args, "summary")?); - let lcov_path = PathBuf::from(parse_string_arg(args, "lcov")?); - let out_path = PathBuf::from(parse_string_arg(args, "out")?); - let policy_gate = parse_bool_flag(args, "policy-gate"); - let explicit_exec = parse_optional_f64_arg(args, "fail-under-exec-lines")?; - let explicit_functions = parse_optional_f64_arg(args, "fail-under-functions")?; - let explicit_regions = parse_optional_f64_arg(args, "fail-under-regions")?; - let explicit_branches = parse_optional_f64_arg(args, "fail-under-branches")?; - let explicit_require_branches = has_flag(args, "require-branches"); - let any_explicit_threshold = explicit_exec.is_some() - || explicit_functions.is_some() - || explicit_regions.is_some() - || explicit_branches.is_some(); - let thresholds = if policy_gate { - if any_explicit_threshold || explicit_require_branches { - return Err( - "--policy-gate cannot be combined with explicit threshold or branch flags" - .to_string(), - ); - } - let policy = read_coverage_policy(&coverage_policy_path(root))?; - policy.thresholds_for_scope(&scope) - } else { - let Some(fail_under_exec_lines) = explicit_exec else { - return Err( - "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values" - .to_string(), - ); - }; - let Some(fail_under_functions) = explicit_functions else { - return Err( - "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values" - .to_string(), - ); - }; - let Some(fail_under_regions) = explicit_regions else { - return Err( - "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values" - .to_string(), - ); - }; - let Some(fail_under_branches) = explicit_branches else { - return Err( - "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values" - .to_string(), - ); - }; - CoverageThresholds { - fail_under_exec_lines, - fail_under_functions, - fail_under_regions, - fail_under_branches, - require_branches: explicit_require_branches, - } - }; - - let mut summary = read_summary_for_scope(&summary_path, Some(&scope))?; - let lcov = read_lcov(&lcov_path)?; - normalize_summary_for_gate(&scope, &summary_path, &lcov, &mut summary)?; - let gate = evaluate_gate(&summary, &lcov, thresholds); - - let report = CoverageGateReport { - scope: scope.clone(), - thresholds: CoverageGateReportThresholds { - executable_lines: thresholds.fail_under_exec_lines, - functions: thresholds.fail_under_functions, - regions: thresholds.fail_under_regions, - branches: thresholds.fail_under_branches, - branches_required: thresholds.require_branches, - }, - measured: CoverageGateReportMeasured { - executable_lines_percent: lcov.executable_percent, - executable_lines_source: executable_source_label(lcov.executable_source).to_string(), - functions_percent: summary.functions_percent, - branches_percent: lcov.branch_percent, - branches_available: lcov.branches_available, - summary_lines_percent: summary.summary_lines_percent, - summary_regions_percent: summary.summary_regions_percent, - }, - counts: CoverageGateReportCounts { - executable_lines: CoverageCount { - covered: lcov.executable_covered, - total: lcov.executable_total, - }, - branches: CoverageCount { - covered: lcov.branch_covered, - total: lcov.branch_total, - }, - }, - result: CoverageGateReportResult { - pass: gate.pass, - fail_reasons: gate.fail_reasons.clone(), - }, - }; - write_gate_report(&out_path, &report)?; - - if lcov.branches_available { - eprintln!( - "{} coverage: executable_lines={:.6} functions={:.6} regions={:.6} branches={:.6}", - scope, - lcov.executable_percent, - summary.functions_percent, - summary.summary_regions_percent, - lcov.branch_percent.unwrap_or(0.0) - ); - } else { - eprintln!( - "{} coverage: executable_lines={:.6} functions={:.6} regions={:.6} branches=unavailable", - scope, - lcov.executable_percent, - summary.functions_percent, - summary.summary_regions_percent - ); - } - - eprintln!( - "{} summary (informational): lines={:.6} regions={:.6}", - scope, summary.summary_lines_percent, summary.summary_regions_percent - ); - - if !gate.pass { - for reason in &gate.fail_reasons { - eprintln!("{scope} gate fail: {reason}"); - } - return Err("coverage gate failed".to_string()); - } - - Ok(()) -} - -#[cfg_attr(coverage_nightly, coverage(off))] -fn normalize_summary_for_gate( - scope: &str, - summary_path: &Path, - _lcov: &LcovCoverage, - summary: &mut CoverageSummary, -) -> Result<(), String> { - let details_path = coverage_details_path(summary_path); - if !details_path.exists() { - return Ok(()); - } - - let normalized = read_detailed_summary(&details_path, Some(scope))?; - summary.functions_percent = normalized.functions_percent; - summary.summary_regions_percent = normalized.regions_percent; - Ok(()) -} - -#[cfg_attr(not(test), allow(dead_code))] -fn report_gate(args: &[String]) -> Result<(), String> { - let root = workspace_root(); - report_gate_with_root(args, &root) -} - -fn report_missing_gate_with_root(args: &[String], root: &Path) -> Result<(), String> { - let scope = parse_string_arg(args, "scope")?; - let out_path = PathBuf::from(parse_string_arg(args, "out")?); - let reason = parse_string_arg(args, "reason")?; - let policy = read_coverage_policy(&coverage_policy_path(root))?; - let thresholds = policy.thresholds_for_scope(&scope); - - let report = CoverageGateReport { - scope: scope.clone(), - thresholds: CoverageGateReportThresholds { - executable_lines: thresholds.fail_under_exec_lines, - functions: thresholds.fail_under_functions, - regions: thresholds.fail_under_regions, - branches: thresholds.fail_under_branches, - branches_required: thresholds.require_branches, - }, - measured: CoverageGateReportMeasured { - executable_lines_percent: 0.0, - executable_lines_source: executable_source_label(ExecutableSource::Da).to_string(), - functions_percent: 0.0, - branches_percent: None, - branches_available: false, - summary_lines_percent: 0.0, - summary_regions_percent: 0.0, - }, - counts: CoverageGateReportCounts { - executable_lines: CoverageCount { - covered: 0, - total: 0, - }, - branches: CoverageCount { - covered: 0, - total: 0, - }, - }, - result: CoverageGateReportResult { - pass: false, - fail_reasons: vec![reason.clone()], - }, - }; - write_gate_report(&out_path, &report)?; - eprintln!("{scope} gate fail: {reason}"); - Ok(()) -} - -fn write_gate_report(out_path: &Path, report: &CoverageGateReport) -> Result<(), String> { - let json = serde_json::to_string_pretty(report) - .expect("serializing coverage gate report should succeed"); - if let Err(err) = fs::write(out_path, format!("{json}\n")) { - return Err(format!("failed to write {}: {err}", out_path.display())); - } - Ok(()) -} - -fn coverage_report_path(reports_root: &Path, crate_name: &str) -> PathBuf { - reports_root - .join(crate_name.replace('-', "_")) - .join("gate-report.json") -} - -fn read_gate_report(path: &Path) -> Result<CoverageGateReport, String> { - let raw = match fs::read_to_string(path) { - Ok(raw) => raw, - Err(err) => { - return Err(format!( - "failed to read gate report {}: {err}", - path.display() - )); - } - }; - match serde_json::from_str::<CoverageGateReport>(&raw) { - Ok(report) => Ok(report), - Err(err) => Err(format!( - "failed to parse gate report {}: {err}", - path.display() - )), - } -} - -fn list_required_crates_with_root(root: &Path, writer: &mut dyn Write) -> Result<(), String> { - let required_path = coverage_policy_path(root); - let crates = read_required_crates(&required_path)?; - write_crate_names_output(writer, crates, "required crates") -} - -fn list_workspace_crates_with_root(root: &Path, writer: &mut dyn Write) -> Result<(), String> { - let crates = read_workspace_crates(root)?; - write_crate_names_output(writer, crates, "workspace crates") -} - -fn write_crate_names_output( - writer: &mut dyn Write, - crates: Vec<String>, - label: &str, -) -> Result<(), String> { - for crate_name in crates { - if let Err(err) = writeln!(writer, "{crate_name}") { - return Err(format!("failed to write {label} output: {err}")); - } - } - Ok(()) -} - -fn run_with_root(args: &[String], root: &Path) -> Result<(), String> { - match args.first().map(String::as_str) { - Some("help") => Ok(()), - Some("run-crate") => run_crate(&args[1..]), - Some("report") => report_gate_with_root(&args[1..], root), - Some("report-missing") => report_missing_gate_with_root(&args[1..], root), - Some("refresh-summary") => { - let reports_root = match parse_optional_string_arg(&args[1..], "reports-root") { - Some(raw) => PathBuf::from(raw), - None => PathBuf::from("target/coverage"), - }; - let out_path = match parse_optional_string_arg(&args[1..], "out") { - Some(raw) => PathBuf::from(raw), - None => PathBuf::from("target/coverage/coverage-refresh.tsv"), - }; - let status_out_path = - parse_optional_string_arg(&args[1..], "status-out").map(PathBuf::from); - let required_crates = read_required_crates(&coverage_policy_path(root))?; - - let mut refresh_rows = - String::from("crate\tstatus\texec\tfunc\tbranch\tregion\treport\n"); - let mut status_rows = String::from("crate\tstatus\n"); - - for crate_name in required_crates { - let report_path = coverage_report_path(&reports_root, &crate_name); - let report = read_gate_report(&report_path)?; - let status = if report.result.pass { "pass" } else { "fail" }; - let branch = report - .measured - .branches_percent - .map(|value| format!("{value:.6}")) - .unwrap_or_else(|| "unavailable".to_string()); - refresh_rows.push_str(&format!( - "{}\t{}\t{:.6}\t{:.6}\t{}\t{:.6}\t{}\n", - crate_name, - status, - report.measured.executable_lines_percent, - report.measured.functions_percent, - branch, - report.measured.summary_regions_percent, - report_path.display() - )); - status_rows.push_str(&format!("{}\t{}\n", crate_name, status)); - } - - if let Some(parent) = out_path.parent() - && !parent.as_os_str().is_empty() - && let Err(err) = fs::create_dir_all(parent) - { - return Err(format!("failed to create {}: {err}", parent.display())); - } - fs::write(&out_path, refresh_rows) - .map_err(|err| format!("failed to write {}: {err}", out_path.display()))?; - - if let Some(status_out_path) = status_out_path { - if let Some(parent) = status_out_path.parent() - && !parent.as_os_str().is_empty() - { - fs::create_dir_all(parent) - .map_err(|err| format!("failed to create {}: {err}", parent.display()))?; - } - fs::write(&status_out_path, status_rows).map_err(|err| { - format!("failed to write {}: {err}", status_out_path.display()) - })?; - } - - Ok(()) - } - Some("required-crates") => { - let mut stdout = std::io::stdout().lock(); - list_required_crates_with_root(root, &mut stdout) - } - Some("workspace-crates") => { - let mut stdout = std::io::stdout().lock(); - list_workspace_crates_with_root(root, &mut stdout) - } - Some(_) => Err("unknown sdk coverage subcommand".to_string()), - None => Err("missing sdk coverage subcommand".to_string()), - } -} - -pub fn run(args: &[String]) -> Result<(), String> { - let root = workspace_root(); - run_with_root(args, &root) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use std::io::{self, Write}; - use std::path::Path; - use std::sync::{Mutex, MutexGuard, OnceLock}; - use std::time::{SystemTime, UNIX_EPOCH}; - - fn temp_file_path(prefix: &str) -> PathBuf { - let ns = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time") - .as_nanos(); - std::env::temp_dir().join(format!("radroots_xtask_coverage_{prefix}_{ns}.tmp")) - } - - fn temp_dir_path(prefix: &str) -> PathBuf { - let ns = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time") - .as_nanos(); - std::env::temp_dir().join(format!("radroots_xtask_coverage_{prefix}_{ns}")) - } - - fn write_file(path: &Path, content: &str) { - let _ = fs::create_dir_all(path.parent().unwrap_or(Path::new(""))); - fs::write(path, content).expect("write file"); - } - - fn cwd_lock() -> &'static Mutex<()> { - static LOCK: OnceLock<Mutex<()>> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) - } - - fn recover_lock(lock: &'static Mutex<()>) -> MutexGuard<'static, ()> { - match lock.lock() { - Ok(guard) => guard, - Err(poisoned) => poisoned.into_inner(), - } - } - - fn lock_cwd() -> MutexGuard<'static, ()> { - recover_lock(cwd_lock()) - } - - fn collect_command_envs(cmd: &Command) -> BTreeMap<String, Option<String>> { - let mut envs = BTreeMap::new(); - for (key, value) in cmd.get_envs() { - envs.insert( - key.to_string_lossy().to_string(), - value.map(|raw| raw.to_string_lossy().to_string()), - ); - } - envs - } - - fn ok_runner(_cmd: Command, _name: &str) -> Result<(), String> { - Ok(()) - } - - struct FailingWriter; - - impl Write for FailingWriter { - fn write(&mut self, _buf: &[u8]) -> io::Result<usize> { - Err(io::Error::other("forced write failure")) - } - - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } - } - - #[test] - fn reads_summary_totals_from_llvm_cov_json() { - let path = temp_file_path("summary"); - fs::write( - &path, - r#"{ - "data": [ - { - "totals": { - "functions": {"percent": 91.25}, - "lines": {"percent": 88.5}, - "regions": {"percent": 86.75} - } - } - ] -}"#, - ) - .expect("write summary"); - - let summary = read_summary(&path).expect("parse summary"); - assert_eq!(summary.functions_percent, 91.25); - assert_eq!(summary.summary_lines_percent, 88.5); - assert_eq!(summary.summary_regions_percent, 86.75); - - fs::remove_file(path).expect("remove summary"); - } - - #[test] - fn read_summary_normalizes_duplicate_generic_detail_records() { - let root = temp_dir_path("summary_details_normalized"); - let summary_path = root.join("coverage-summary.json"); - write_file( - &summary_path, - r#"{ - "data": [ - { - "totals": { - "functions": {"percent": 100.0}, - "lines": {"percent": 88.5}, - "regions": {"percent": 22.0} - } - } - ] -}"#, - ); - write_file( - &root.join("coverage-details.json"), - r#"{ - "data": [ - { - "functions": [ - { - "count": 4, - "filenames": ["/tmp/lib.rs"], - "regions": [ - [10, 1, 12, 2, 4, 0, 0, 0], - [13, 1, 13, 8, 4, 0, 0, 0] - ] - }, - { - "count": 0, - "filenames": ["/tmp/lib.rs"], - "regions": [ - [10, 1, 12, 2, 0, 0, 0, 0], - [13, 1, 13, 8, 0, 0, 0, 0] - ] - } - ] - } - ] -}"#, - ); - - let summary = read_summary(&summary_path).expect("parse normalized summary"); - assert_eq!(summary.functions_percent, 100.0); - assert_eq!(summary.summary_lines_percent, 88.5); - assert_eq!(summary.summary_regions_percent, 100.0); - - fs::remove_dir_all(root).expect("remove summary details root"); - } - - #[test] - fn read_summary_keeps_original_regions_when_functions_are_not_perfect() { - let root = temp_dir_path("summary_details_not_applied"); - let summary_path = root.join("coverage-summary.json"); - write_file( - &summary_path, - r#"{ - "data": [ - { - "totals": { - "functions": {"percent": 95.0}, - "lines": {"percent": 88.5}, - "regions": {"percent": 22.0} - } - } - ] -}"#, - ); - write_file( - &root.join("coverage-details.json"), - r#"{ - "data": [ - { - "functions": [ - { - "count": 4, - "filenames": ["/tmp/lib.rs"], - "regions": [ - [10, 1, 12, 2, 4, 0, 0, 0] - ] - } - ] - } - ] -}"#, - ); - - let summary = read_summary(&summary_path).expect("parse preserved summary"); - assert_eq!(summary.functions_percent, 95.0); - assert_eq!(summary.summary_regions_percent, 22.0); - - fs::remove_dir_all(root).expect("remove summary preserve root"); - } - - #[test] - fn read_summary_for_scope_ignores_other_crate_detail_records() { - let root = temp_dir_path("summary_details_scope_filtered"); - let summary_path = root.join("coverage-summary.json"); - write_file( - &summary_path, - r#"{ - "data": [ - { - "totals": { - "functions": {"percent": 100.0}, - "lines": {"percent": 88.5}, - "regions": {"percent": 22.0} - } - } - ] -}"#, - ); - write_file( - &root.join("coverage-details.json"), - r#"{ - "data": [ - { - "functions": [ - { - "count": 4, - "filenames": ["/workspace/crates/a/src/lib.rs"], - "regions": [ - [10, 1, 12, 2, 4, 0, 0, 0] - ] - }, - { - "count": 9, - "filenames": ["/workspace/crates/b/src/lib.rs"], - "regions": [ - [20, 1, 20, 6, 0, 0, 0, 0] - ] - } - ] - } - ] -}"#, - ); - - let summary = - read_summary_for_scope(&summary_path, Some("radroots_a")).expect("parse scope summary"); - assert_eq!(summary.functions_percent, 100.0); - assert_eq!(summary.summary_lines_percent, 88.5); - assert_eq!(summary.summary_regions_percent, 100.0); - - fs::remove_dir_all(root).expect("remove summary scope root"); - } - - #[test] - fn coverage_details_path_uses_summary_parent() { - let summary_path = Path::new("target/coverage/radroots_a/coverage-summary.json"); - assert_eq!( - coverage_details_path(summary_path), - Path::new("target/coverage/radroots_a/coverage-details.json") - ); - } - - #[test] - fn read_detailed_summary_covers_empty_skip_and_filter_paths() { - let root = temp_dir_path("details_empty_skip_filter"); - let missing = root.join("missing-details.json"); - let err = read_detailed_summary(&missing, None).expect_err("missing details"); - assert!(err.contains("failed to read coverage details")); - - let empty = root.join("empty-details.json"); - write_file(&empty, r#"{"data":[]}"#); - let err = read_detailed_summary(&empty, None).expect_err("empty details"); - assert!(err.contains("coverage details data is empty")); - - let skipped = root.join("skipped-details.json"); - write_file( - &skipped, - r#"{ - "data": [ - { - "functions": [ - { - "count": 1, - "filenames": [], - "regions": [[10, 1, 10, 2, 1, 0, 0, 0]] - }, - { - "count": 1, - "filenames": ["/workspace/crates/a/src/lib.rs"], - "regions": [] - } - ] - } - ] -}"#, - ); - let err = read_detailed_summary(&skipped, None).expect_err("skipped details"); - assert!(err.contains("coverage details functions are empty")); - - let filtered = root.join("filtered-details.json"); - write_file( - &filtered, - r#"{ - "data": [ - { - "functions": [ - { - "count": 0, - "filenames": ["/workspace/crates/a/src/lib.rs"], - "regions": [[10, 1, 10, 2, 0, 0, 0, 0]] - }, - { - "count": 1, - "filenames": ["/workspace/crates/b/src/lib.rs"], - "regions": [[20, 1, 20, 2, 1, 0, 0, 0]] - } - ] - } - ] -}"#, - ); - let summary = - read_detailed_summary(&filtered, Some("radroots_a")).expect("filtered summary"); - assert_eq!(summary.functions_percent, 0.0); - assert_eq!(summary.regions_percent, 0.0); - - fs::remove_dir_all(root).expect("remove detail edge root"); - } - - #[test] - fn read_detailed_summary_ignores_synthetic_regions_from_source() { - let root = temp_dir_path("details_synthetic_regions"); - let source_path = root - .join("crates") - .join("radroots_a") - .join("src") - .join("lib.rs"); - write_file(&source_path, "pub fn load() { let _value = call()?; }\n"); - let details_path = root.join("coverage-details.json"); - let raw = serde_json::json!({ - "data": [ - { - "functions": [ - { - "count": 1, - "filenames": [source_path.display().to_string()], - "regions": [ - [1, 1, 1, 37, 1, 0, 0, 0], - [1, 34, 1, 35, 0, 0, 0, 0] - ] - } - ] - } - ] - }); - write_file(&details_path, &raw.to_string()); - - let summary = - read_detailed_summary(&details_path, Some("radroots_a")).expect("synthetic summary"); - assert_eq!(summary.functions_percent, 100.0); - assert_eq!(summary.regions_percent, 100.0); - - fs::remove_dir_all(root).expect("remove synthetic details root"); - } - - #[test] - fn read_summary_reports_read_and_parse_errors() { - let missing = temp_file_path("summary_missing"); - let read_err = read_summary(&missing).expect_err("missing summary should fail"); - assert!(read_err.contains("failed to read summary")); - - let invalid = temp_file_path("summary_invalid"); - write_file(&invalid, "{not-json"); - let parse_err = read_summary(&invalid).expect_err("invalid summary should fail"); - assert!(parse_err.contains("failed to parse summary")); - fs::remove_file(invalid).expect("remove invalid summary"); - } - - #[test] - fn read_summary_reports_detail_parse_errors() { - let root = temp_dir_path("summary_invalid_details"); - let summary_path = root.join("coverage-summary.json"); - write_file( - &summary_path, - r#"{ - "data": [ - { - "totals": { - "functions": {"percent": 91.25}, - "lines": {"percent": 88.5}, - "regions": {"percent": 86.75} - } - } - ] -}"#, - ); - write_file(&root.join("coverage-details.json"), "{not-json"); - - let err = read_summary(&summary_path).expect_err("invalid details should fail"); - assert!(err.contains("failed to parse coverage details")); - - fs::remove_dir_all(root).expect("remove invalid details root"); - } - - #[test] - fn ignorable_question_mark_regions_require_single_char_question_mark() { - let path = temp_file_path("coverage_question_mark_region"); - write_file(&path, "let value = call()?;\nreturn Err(());\n"); - let mut cache = BTreeMap::new(); - - let question_mark = RegionCoverageKey { - line_start: 1, - column_start: 19, - line_end: 1, - column_end: 20, - kind: 0, - }; - assert!(is_ignorable_synthetic_region( - path.to_str().expect("utf-8 path"), - &question_mark, - &mut cache, - )); - - let not_question_mark = RegionCoverageKey { - line_start: 2, - column_start: 8, - line_end: 2, - column_end: 15, - kind: 0, - }; - assert!(!is_ignorable_synthetic_region( - path.to_str().expect("utf-8 path"), - &not_question_mark, - &mut cache, - )); - - let single_char_not_question_mark = RegionCoverageKey { - line_start: 2, - column_start: 1, - line_end: 2, - column_end: 2, - kind: 0, - }; - assert!(!is_ignorable_synthetic_region( - path.to_str().expect("utf-8 path"), - &single_char_not_question_mark, - &mut cache, - )); - - fs::remove_file(path).expect("remove question mark source"); - } - - #[test] - fn ignorable_unexpected_panic_regions_require_test_fallback_lines() { - let root = temp_dir_path("coverage_unexpected_panic_region"); - let path = root.join("tests.rs"); - write_file( - &path, - "match &err {\n RuntimeProtectedFileError::Io { .. } => {}\n other => panic!(\"unexpected io error: {other}\"),\n}\n", - ); - let mut cache = BTreeMap::new(); - - let other_region = RegionCoverageKey { - line_start: 3, - column_start: 9, - line_end: 3, - column_end: 14, - kind: 0, - }; - assert!(is_ignorable_synthetic_region( - path.to_str().expect("utf-8 path"), - &other_region, - &mut cache, - )); - - let panic_region = RegionCoverageKey { - line_start: 3, - column_start: 18, - line_end: 3, - column_end: 24, - kind: 0, - }; - assert!(is_ignorable_synthetic_region( - path.to_str().expect("utf-8 path"), - &panic_region, - &mut cache, - )); - - let non_test_path = root.join("source.rs"); - write_file( - &non_test_path, - "match &err {\n RuntimeProtectedFileError::Io { .. } => {}\n other => panic!(\"unexpected io error: {other}\"),\n}\n", - ); - assert!(!is_ignorable_synthetic_region( - non_test_path.to_str().expect("utf-8 path"), - &other_region, - &mut cache, - )); - - let multiline = RegionCoverageKey { - line_start: 1, - column_start: 1, - line_end: 2, - column_end: 1, - kind: 0, - }; - assert!(!is_ignorable_synthetic_region( - path.to_str().expect("utf-8 path"), - &multiline, - &mut cache, - )); - - let missing_file = root.join("missing.rs"); - assert!(!is_ignorable_synthetic_region( - missing_file.to_str().expect("utf-8 path"), - &other_region, - &mut cache, - )); - - let out_of_range = RegionCoverageKey { - line_start: 99, - column_start: 1, - line_end: 99, - column_end: 2, - kind: 0, - }; - assert!(!is_ignorable_synthetic_region( - path.to_str().expect("utf-8 path"), - &out_of_range, - &mut cache, - )); - - let non_panic_test_path = root.join("non_panic_tests.rs"); - write_file(&non_panic_test_path, " other => Ok(()),\n"); - let non_panic_other_region = RegionCoverageKey { - line_start: 1, - column_start: 9, - line_end: 1, - column_end: 14, - kind: 0, - }; - assert!(!is_ignorable_synthetic_region( - non_panic_test_path.to_str().expect("utf-8 path"), - &non_panic_other_region, - &mut cache, - )); - - let non_fallback_region = RegionCoverageKey { - line_start: 3, - column_start: 39, - line_end: 3, - column_end: 44, - kind: 0, - }; - assert!(!is_ignorable_synthetic_region( - path.to_str().expect("utf-8 path"), - &non_fallback_region, - &mut cache, - )); - - fs::remove_dir_all(root).expect("remove unexpected panic source"); - } - - #[test] - fn read_coverage_policy_rejects_non_finite_and_out_of_range_thresholds() { - let non_finite = temp_file_path("coverage_policy_non_finite"); - write_file( - &non_finite, - "[gate]\nfail_under_exec_lines = inf\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", - ); - let non_finite_err = - read_coverage_policy(&non_finite).expect_err("non-finite threshold should fail"); - assert!(non_finite_err.contains("must be finite")); - fs::remove_file(non_finite).expect("remove non-finite policy"); - - let out_of_range = temp_file_path("coverage_policy_out_of_range"); - write_file( - &out_of_range, - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 101.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", - ); - let out_of_range_err = - read_coverage_policy(&out_of_range).expect_err("out-of-range threshold should fail"); - assert!(out_of_range_err.contains("must be within 0..=100")); - fs::remove_file(out_of_range).expect("remove out-of-range policy"); - } - - #[test] - fn coverage_policy_resolves_scope_specific_temporary_overrides() { - let path = temp_file_path("coverage_policy_override_scope"); - write_file( - &path, - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\", \"radroots_b\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 88.5\nfail_under_functions = 77.5\nfail_under_regions = 66.5\nfail_under_branches = 55.5\nrequire_branches = false\ntemporary = true\nreason = \"temporary publish unblocker\"\n", - ); - let policy = read_coverage_policy(&path).expect("parse scoped override policy"); - let override_thresholds = policy.thresholds_for_scope("radroots_a"); - assert_eq!(override_thresholds.fail_under_exec_lines, 88.5); - assert_eq!(override_thresholds.fail_under_functions, 77.5); - assert_eq!(override_thresholds.fail_under_regions, 66.5); - assert_eq!(override_thresholds.fail_under_branches, 55.5); - assert!(!override_thresholds.require_branches); - - let default_thresholds = policy.thresholds_for_scope("radroots_b"); - assert_eq!(default_thresholds.fail_under_exec_lines, 100.0); - assert_eq!(default_thresholds.fail_under_functions, 100.0); - assert_eq!(default_thresholds.fail_under_regions, 100.0); - assert_eq!(default_thresholds.fail_under_branches, 100.0); - assert!(default_thresholds.require_branches); - - fs::remove_file(path).expect("remove override scope policy"); - } - - #[test] - fn read_coverage_policy_rejects_invalid_override_shapes() { - let non_required = temp_file_path("coverage_policy_override_non_required"); - write_file( - &non_required, - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_b]\nfail_under_exec_lines = 90.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n", - ); - let non_required_err = - read_coverage_policy(&non_required).expect_err("non-required override should fail"); - assert!(non_required_err.contains("must target a required crate")); - fs::remove_file(non_required).expect("remove non-required override policy"); - - let missing_temporary = temp_file_path("coverage_policy_override_missing_temporary"); - write_file( - &missing_temporary, - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 90.0\ntemporary = false\nreason = \"temporary publish unblocker\"\n", - ); - let missing_temporary_err = read_coverage_policy(&missing_temporary) - .expect_err("override without temporary=true should fail"); - assert!(missing_temporary_err.contains("temporary = true")); - fs::remove_file(missing_temporary).expect("remove temporary override policy"); - - let missing_reason = temp_file_path("coverage_policy_override_missing_reason"); - write_file( - &missing_reason, - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 90.0\ntemporary = true\nreason = \" \"\n", - ); - let missing_reason_err = - read_coverage_policy(&missing_reason).expect_err("blank override reason should fail"); - assert!(missing_reason_err.contains("non-empty reason")); - fs::remove_file(missing_reason).expect("remove missing reason policy"); - - let stricter = temp_file_path("coverage_policy_override_stricter"); - write_file( - &stricter, - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 100.1\ntemporary = true\nreason = \"temporary publish unblocker\"\n", - ); - let stricter_err = - read_coverage_policy(&stricter).expect_err("stricter override should fail"); - assert!(stricter_err.contains("must be within 0..=100")); - fs::remove_file(stricter).expect("remove stricter override policy"); - - let above_global = temp_file_path("coverage_policy_override_above_global"); - write_file( - &above_global, - "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 99.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n", - ); - let above_global_err = - read_coverage_policy(&above_global).expect_err("above-global override should fail"); - assert!(above_global_err.contains("must not exceed the global gate")); - fs::remove_file(above_global).expect("remove above-global override policy"); - - let above_global_functions = temp_file_path("coverage_policy_override_above_global_fn"); - write_file( - &above_global_functions, - "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_functions = 99.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n", - ); - let above_global_functions_err = read_coverage_policy(&above_global_functions) - .expect_err("above-global function override should fail"); - assert!(above_global_functions_err.contains("must not exceed the global gate")); - fs::remove_file(above_global_functions).expect("remove above-global function policy"); - - let above_global_regions = temp_file_path("coverage_policy_override_above_global_regions"); - write_file( - &above_global_regions, - "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_regions = 99.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n", - ); - let above_global_regions_err = read_coverage_policy(&above_global_regions) - .expect_err("above-global region override should fail"); - assert!(above_global_regions_err.contains("must not exceed the global gate")); - fs::remove_file(above_global_regions).expect("remove above-global region policy"); - - let above_global_branches = - temp_file_path("coverage_policy_override_above_global_branches"); - write_file( - &above_global_branches, - "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_branches = 99.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n", - ); - let above_global_branches_err = read_coverage_policy(&above_global_branches) - .expect_err("above-global branch override should fail"); - assert!(above_global_branches_err.contains("must not exceed the global gate")); - fs::remove_file(above_global_branches).expect("remove above-global branch policy"); - - let non_finite_override = temp_file_path("coverage_policy_override_non_finite"); - write_file( - &non_finite_override, - "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = inf\ntemporary = true\nreason = \"temporary publish unblocker\"\n", - ); - let non_finite_override_err = read_coverage_policy(&non_finite_override) - .expect_err("non-finite override should fail"); - assert!(non_finite_override_err.contains("must be finite")); - fs::remove_file(non_finite_override).expect("remove non-finite override policy"); - - let stricter_branch_presence = temp_file_path("coverage_policy_override_branch_required"); - write_file( - &stricter_branch_presence, - "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = false\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nrequire_branches = true\ntemporary = true\nreason = \"temporary publish unblocker\"\n", - ); - let branch_presence_err = read_coverage_policy(&stricter_branch_presence) - .expect_err("stricter branch presence should fail"); - assert!(branch_presence_err.contains("require_branches cannot be stricter")); - fs::remove_file(stricter_branch_presence).expect("remove branch presence policy"); - } - - #[test] - fn report_missing_gate_uses_policy_thresholds() { - let root = temp_dir_path("report_missing_gate_root"); - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - write_file( - &coverage_dir.join("coverage.toml"), - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", - ); - let out_path = root.join("gate-report.json"); - - report_missing_gate_with_root( - &[ - "--scope".to_string(), - "radroots_a_blocking".to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--reason".to_string(), - "missing-coverage-artifacts".to_string(), - ], - &root, - ) - .expect("report missing gate"); - - let report_raw = fs::read_to_string(&out_path).expect("read gate report"); - let report_json: serde_json::Value = - serde_json::from_str(&report_raw).expect("parse gate report json"); - assert_eq!( - report_json["thresholds"]["executable_lines"], - serde_json::json!(100.0) - ); - assert_eq!( - report_json["thresholds"]["branches_required"], - serde_json::json!(true) - ); - assert_eq!(report_json["result"]["pass"], serde_json::json!(false)); - assert_eq!( - report_json["result"]["fail_reasons"], - serde_json::json!(["missing-coverage-artifacts"]) - ); - - fs::remove_dir_all(root).expect("remove root"); - } - - #[test] - fn report_missing_gate_uses_scope_specific_override_thresholds() { - let root = temp_dir_path("report_missing_gate_override_root"); - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - write_file( - &coverage_dir.join("coverage.toml"), - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 88.5\nfail_under_functions = 77.5\nfail_under_regions = 66.5\nfail_under_branches = 55.5\nrequire_branches = false\ntemporary = true\nreason = \"temporary publish unblocker\"\n", - ); - let out_path = root.join("gate-report.json"); - - report_missing_gate_with_root( - &[ - "--scope".to_string(), - "radroots_a".to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--reason".to_string(), - "missing-coverage-artifacts".to_string(), - ], - &root, - ) - .expect("report missing gate with override"); - - let report_raw = fs::read_to_string(&out_path).expect("read gate report"); - let report_json: serde_json::Value = - serde_json::from_str(&report_raw).expect("parse gate report json"); - assert_eq!( - report_json["thresholds"]["executable_lines"], - serde_json::json!(88.5) - ); - assert_eq!( - report_json["thresholds"]["functions"], - serde_json::json!(77.5) - ); - assert_eq!( - report_json["thresholds"]["regions"], - serde_json::json!(66.5) - ); - assert_eq!( - report_json["thresholds"]["branches"], - serde_json::json!(55.5) - ); - assert_eq!( - report_json["thresholds"]["branches_required"], - serde_json::json!(false) - ); - - fs::remove_dir_all(root).expect("remove override root"); - } - - #[test] - fn report_missing_gate_reports_argument_policy_and_write_errors() { - let root = temp_dir_path("report_missing_gate_error_root"); - let missing_scope = - report_missing_gate_with_root(&[], &root).expect_err("missing scope should fail"); - assert!(missing_scope.contains("missing --scope")); - - let missing_out = report_missing_gate_with_root( - &[ - "--scope".to_string(), - "radroots_a_blocking".to_string(), - "--reason".to_string(), - "missing-coverage-artifacts".to_string(), - ], - &root, - ) - .expect_err("missing out should fail"); - assert!(missing_out.contains("missing --out")); - - let missing_reason = report_missing_gate_with_root( - &[ - "--scope".to_string(), - "radroots_a_blocking".to_string(), - "--out".to_string(), - root.join("missing-gate.json").display().to_string(), - ], - &root, - ) - .expect_err("missing reason should fail"); - assert!(missing_reason.contains("missing --reason")); - - let policy_err = report_missing_gate_with_root( - &[ - "--scope".to_string(), - "radroots_a_blocking".to_string(), - "--out".to_string(), - root.join("missing-gate.json").display().to_string(), - "--reason".to_string(), - "missing-coverage-artifacts".to_string(), - ], - &root, - ) - .expect_err("missing policy should fail"); - assert!(policy_err.contains("failed to read coverage policy")); - - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - write_file( - &coverage_dir.join("coverage.toml"), - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", - ); - let out_path = root.join("gate-report.json"); - fs::create_dir_all(&out_path).expect("create blocking output dir"); - let write_err = report_missing_gate_with_root( - &[ - "--scope".to_string(), - "radroots_a_blocking".to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--reason".to_string(), - "missing-coverage-artifacts".to_string(), - ], - &root, - ) - .expect_err("directory output should fail"); - assert!(write_err.contains("failed to write")); - - fs::remove_dir_all(root).expect("remove report missing gate error root"); - } - - #[test] - fn refresh_summary_uses_measured_gate_report_values() { - let root = temp_dir_path("refresh_summary_root"); - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - write_file( - &coverage_dir.join("coverage.toml"), - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\", \"radroots_b\"]\n", - ); - - let reports_root = root.join("target").join("coverage"); - let crate_dir = reports_root.join("radroots_a"); - fs::create_dir_all(&crate_dir).expect("create crate dir"); - write_file( - &crate_dir.join("gate-report.json"), - r#"{ - "scope": "radroots_a", - "thresholds": { - "executable_lines": 100.0, - "functions": 100.0, - "regions": 100.0, - "branches": 100.0, - "branches_required": true - }, - "measured": { - "executable_lines_percent": 100.0, - "executable_lines_source": "da", - "functions_percent": 100.0, - "branches_percent": 100.0, - "branches_available": true, - "summary_lines_percent": 100.0, - "summary_regions_percent": 97.5 - }, - "counts": { - "executable_lines": { - "covered": 4, - "total": 4 - }, - "branches": { - "covered": 2, - "total": 2 - } - }, - "result": { - "pass": true, - "fail_reasons": [] - } -}"#, - ); - let no_branch_crate_dir = reports_root.join("radroots_b"); - fs::create_dir_all(&no_branch_crate_dir).expect("create no branch crate dir"); - write_file( - &no_branch_crate_dir.join("gate-report.json"), - r#"{ - "scope": "radroots_b", - "thresholds": { - "executable_lines": 100.0, - "functions": 100.0, - "regions": 100.0, - "branches": 100.0, - "branches_required": false - }, - "measured": { - "executable_lines_percent": 100.0, - "executable_lines_source": "da", - "functions_percent": 100.0, - "branches_percent": null, - "branches_available": false, - "summary_lines_percent": 100.0, - "summary_regions_percent": 100.0 - }, - "counts": { - "executable_lines": { - "covered": 4, - "total": 4 - }, - "branches": { - "covered": 0, - "total": 0 - } - }, - "result": { - "pass": true, - "fail_reasons": [] - } -}"#, - ); - - let refresh_out = reports_root.join("coverage-refresh.tsv"); - let status_out = reports_root.join("coverage-refresh-status.tsv"); - run_with_root( - &[ - "refresh-summary".to_string(), - "--reports-root".to_string(), - reports_root.display().to_string(), - "--out".to_string(), - refresh_out.display().to_string(), - "--status-out".to_string(), - status_out.display().to_string(), - ], - &root, - ) - .expect("write refresh summary"); - - let refresh = fs::read_to_string(&refresh_out).expect("read refresh summary"); - assert!(refresh.contains("crate\tstatus\texec\tfunc\tbranch\tregion\treport")); - assert!( - refresh.contains("radroots_a\tpass\t100.000000\t100.000000\t100.000000\t97.500000\t") - ); - assert!( - refresh.contains("radroots_b\tpass\t100.000000\t100.000000\tunavailable\t100.000000\t") - ); - - let status = fs::read_to_string(&status_out).expect("read status summary"); - assert_eq!( - status, - "crate\tstatus\nradroots_a\tpass\nradroots_b\tpass\n" - ); - - fs::remove_dir_all(root).expect("remove root"); - - let defaults_root = temp_dir_path("refresh_summary_defaults_root"); - let defaults_coverage_dir = defaults_root.join("contracts"); - fs::create_dir_all(&defaults_coverage_dir).expect("create defaults coverage dir"); - write_file( - &defaults_coverage_dir.join("coverage.toml"), - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", - ); - write_file( - &defaults_root - .join("target") - .join("coverage") - .join("radroots_a") - .join("gate-report.json"), - r#"{ - "scope": "radroots_a", - "thresholds": { - "executable_lines": 100.0, - "functions": 100.0, - "regions": 100.0, - "branches": 100.0, - "branches_required": true - }, - "measured": { - "executable_lines_percent": 100.0, - "executable_lines_source": "da", - "functions_percent": 100.0, - "branches_percent": 100.0, - "branches_available": true, - "summary_lines_percent": 100.0, - "summary_regions_percent": 100.0 - }, - "counts": { - "executable_lines": { - "covered": 4, - "total": 4 - }, - "branches": { - "covered": 2, - "total": 2 - } - }, - "result": { - "pass": false, - "fail_reasons": ["synthetic-fail"] - } -}"#, - ); - - let _guard = lock_cwd(); - let previous_dir = std::env::current_dir().expect("read current dir"); - std::env::set_current_dir(&defaults_root).expect("set current dir"); - run_with_root(&["refresh-summary".to_string()], &defaults_root) - .expect("write refresh summary defaults"); - let defaults_refresh = fs::read_to_string( - defaults_root - .join("target") - .join("coverage") - .join("coverage-refresh.tsv"), - ) - .expect("read defaults refresh summary"); - assert!( - defaults_refresh - .contains("radroots_a\tfail\t100.000000\t100.000000\t100.000000\t100.000000\t") - ); - - let dispatch_root = temp_dir_path("refresh_summary_parentless_root"); - let dispatch_coverage_dir = dispatch_root.join("contracts"); - fs::create_dir_all(&dispatch_coverage_dir).expect("create dispatch coverage dir"); - write_file( - &dispatch_coverage_dir.join("coverage.toml"), - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", - ); - write_file( - &dispatch_root.join("Cargo.toml"), - "[workspace]\nmembers = []\nresolver = \"2\"\n", - ); - write_file( - &dispatch_root - .join("target") - .join("coverage") - .join("radroots_a") - .join("gate-report.json"), - r#"{ - "scope": "radroots_a", - "thresholds": { - "executable_lines": 100.0, - "functions": 100.0, - "regions": 100.0, - "branches": 100.0, - "branches_required": true - }, - "measured": { - "executable_lines_percent": 100.0, - "executable_lines_source": "da", - "functions_percent": 100.0, - "branches_percent": 100.0, - "branches_available": true, - "summary_lines_percent": 100.0, - "summary_regions_percent": 100.0 - }, - "counts": { - "executable_lines": { - "covered": 4, - "total": 4 - }, - "branches": { - "covered": 2, - "total": 2 - } - }, - "result": { - "pass": true, - "fail_reasons": [] - } -}"#, - ); - std::env::set_current_dir(&dispatch_root).expect("set dispatch current dir"); - run_with_root( - &[ - "report-missing".to_string(), - "--scope".to_string(), - "radroots_a_blocking".to_string(), - "--out".to_string(), - "missing-gate.json".to_string(), - "--reason".to_string(), - "missing-coverage-artifacts".to_string(), - ], - &dispatch_root, - ) - .expect("dispatch report-missing"); - run_with_root( - &[ - "refresh-summary".to_string(), - "--out".to_string(), - "coverage-refresh.tsv".to_string(), - "--status-out".to_string(), - "coverage-refresh-status.tsv".to_string(), - ], - &dispatch_root, - ) - .expect("dispatch refresh-summary"); - std::env::set_current_dir(previous_dir).expect("restore current dir"); - - assert!(dispatch_root.join("missing-gate.json").exists()); - assert!(dispatch_root.join("coverage-refresh.tsv").exists()); - assert!(dispatch_root.join("coverage-refresh-status.tsv").exists()); - - fs::remove_dir_all(defaults_root).expect("remove defaults root"); - fs::remove_dir_all(dispatch_root).expect("remove dispatch root"); - } - - #[test] - fn refresh_summary_rejects_empty_output_paths() { - let root = temp_dir_path("refresh_summary_empty_paths_root"); - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - write_file( - &coverage_dir.join("coverage.toml"), - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", - ); - write_file( - &root - .join("target") - .join("coverage") - .join("radroots_a") - .join("gate-report.json"), - r#"{ - "scope": "radroots_a", - "thresholds": { - "executable_lines": 100.0, - "functions": 100.0, - "regions": 100.0, - "branches": 100.0, - "branches_required": true - }, - "measured": { - "executable_lines_percent": 100.0, - "executable_lines_source": "da", - "functions_percent": 100.0, - "branches_percent": 100.0, - "branches_available": true, - "summary_lines_percent": 100.0, - "summary_regions_percent": 100.0 - }, - "counts": { - "executable_lines": { - "covered": 4, - "total": 4 - }, - "branches": { - "covered": 2, - "total": 2 - } - }, - "result": { - "pass": true, - "fail_reasons": [] - } -}"#, - ); - - let out_err = run_with_root( - &[ - "refresh-summary".to_string(), - "--reports-root".to_string(), - root.join("target").join("coverage").display().to_string(), - "--out".to_string(), - String::new(), - ], - &root, - ) - .expect_err("empty out path should fail"); - assert!(out_err.contains("failed to write")); - - let status_err = run_with_root( - &[ - "refresh-summary".to_string(), - "--reports-root".to_string(), - root.join("target").join("coverage").display().to_string(), - "--out".to_string(), - root.join("target") - .join("coverage") - .join("coverage-refresh.tsv") - .display() - .to_string(), - "--status-out".to_string(), - String::new(), - ], - &root, - ) - .expect_err("empty status out path should fail"); - assert!(status_err.contains("failed to write")); - - fs::remove_dir_all(root).expect("remove empty path root"); - } - - #[test] - fn refresh_summary_reports_output_parent_creation_failure() { - let root = temp_dir_path("refresh_summary_out_parent_fail"); - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - write_file( - &coverage_dir.join("coverage.toml"), - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", - ); - write_file( - &root - .join("target") - .join("coverage") - .join("radroots_a") - .join("gate-report.json"), - r#"{ - "scope": "radroots_a", - "thresholds": { - "executable_lines": 100.0, - "functions": 100.0, - "regions": 100.0, - "branches": 100.0, - "branches_required": true - }, - "measured": { - "executable_lines_percent": 100.0, - "executable_lines_source": "da", - "functions_percent": 100.0, - "branches_percent": 100.0, - "branches_available": true, - "summary_lines_percent": 100.0, - "summary_regions_percent": 100.0 - }, - "counts": { - "executable_lines": { - "covered": 4, - "total": 4 - }, - "branches": { - "covered": 2, - "total": 2 - } - }, - "result": { - "pass": true, - "fail_reasons": [] - } -}"#, - ); - write_file(&root.join("out-blocker"), "x"); - - let err = run_with_root( - &[ - "refresh-summary".to_string(), - "--reports-root".to_string(), - root.join("target").join("coverage").display().to_string(), - "--out".to_string(), - root.join("out-blocker") - .join("nested") - .join("coverage-refresh.tsv") - .display() - .to_string(), - ], - &root, - ) - .expect_err("out parent create failure should bubble up"); - assert!(err.contains("failed to create")); - - fs::remove_dir_all(root).expect("remove out parent fail root"); - } - - #[test] - fn refresh_summary_reports_status_output_parent_creation_failure() { - let root = temp_dir_path("refresh_summary_status_parent_fail"); - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - write_file( - &coverage_dir.join("coverage.toml"), - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", - ); - write_file( - &root - .join("target") - .join("coverage") - .join("radroots_a") - .join("gate-report.json"), - r#"{ - "scope": "radroots_a", - "thresholds": { - "executable_lines": 100.0, - "functions": 100.0, - "regions": 100.0, - "branches": 100.0, - "branches_required": true - }, - "measured": { - "executable_lines_percent": 100.0, - "executable_lines_source": "da", - "functions_percent": 100.0, - "branches_percent": 100.0, - "branches_available": true, - "summary_lines_percent": 100.0, - "summary_regions_percent": 100.0 - }, - "counts": { - "executable_lines": { - "covered": 4, - "total": 4 - }, - "branches": { - "covered": 2, - "total": 2 - } - }, - "result": { - "pass": true, - "fail_reasons": [] - } -}"#, - ); - write_file(&root.join("status-blocker"), "x"); - - let err = run_with_root( - &[ - "refresh-summary".to_string(), - "--reports-root".to_string(), - root.join("target").join("coverage").display().to_string(), - "--out".to_string(), - root.join("target") - .join("coverage") - .join("coverage-refresh.tsv") - .display() - .to_string(), - "--status-out".to_string(), - root.join("status-blocker") - .join("nested") - .join("coverage-refresh-status.tsv") - .display() - .to_string(), - ], - &root, - ) - .expect_err("status-out parent create failure should bubble up"); - assert!(err.contains("failed to create")); - - fs::remove_dir_all(root).expect("remove status parent fail root"); - } - - #[test] - fn refresh_summary_reports_policy_and_gate_report_errors() { - let root = temp_dir_path("refresh_summary_error_root"); - let policy_err = run_with_root( - &[ - "refresh-summary".to_string(), - "--reports-root".to_string(), - root.join("target").join("coverage").display().to_string(), - ], - &root, - ) - .expect_err("missing policy should fail"); - assert!(policy_err.contains("failed to read coverage policy")); - - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - write_file( - &coverage_dir.join("coverage.toml"), - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", - ); - let gate_err = run_with_root( - &[ - "refresh-summary".to_string(), - "--reports-root".to_string(), - root.join("target").join("coverage").display().to_string(), - ], - &root, - ) - .expect_err("missing gate report should fail"); - assert!(gate_err.contains("failed to read gate report")); - - fs::remove_dir_all(root).expect("remove refresh summary error root"); - } - - #[test] - fn recover_lock_covers_ok_and_poisoned_paths() { - let ok_lock: &'static Mutex<()> = Box::leak(Box::new(Mutex::new(()))); - let _ok_guard = recover_lock(ok_lock); - - let poisoned_lock: &'static Mutex<()> = Box::leak(Box::new(Mutex::new(()))); - let handle = std::thread::spawn(move || { - let _guard = poisoned_lock.lock().expect("lock poisoned mutex"); - panic!("poison test mutex"); - }); - assert!(handle.join().is_err()); - - let _poisoned_guard = recover_lock(poisoned_lock); - } - - #[test] - fn read_summary_reports_empty_data_error() { - let path = temp_file_path("summary_empty_data"); - write_file(&path, r#"{"data":[]}"#); - let err = read_summary(&path).expect_err("summary without data should fail"); - assert!(err.contains("summary data is empty")); - fs::remove_file(path).expect("remove empty summary"); - } - - #[test] - fn reads_lcov_da_and_branch_metrics() { - let path = temp_file_path("lcov"); - fs::write( - &path, - "DA:1,1\nDA:2,0\nDA:3,1\nBRDA:1,0,0,1\nBRDA:1,0,1,0\nBRDA:2,0,0,3\nBRDA:2,0,1,-\n", - ) - .expect("write lcov"); - - let lcov = read_lcov(&path).expect("parse lcov"); - assert_eq!(lcov.executable_total, 3); - assert_eq!(lcov.executable_covered, 2); - assert!(lcov.branches_available); - assert_eq!(lcov.branch_total, 3); - assert_eq!(lcov.branch_covered, 2); - assert_eq!(lcov.branch_percent, Some(66.66666666666666)); - - fs::remove_file(path).expect("remove lcov"); - } - - #[test] - fn reads_lcov_branch_metrics_from_brf_brh_when_brda_missing() { - let path = temp_file_path("lcov_fallback"); - fs::write(&path, "DA:1,1\nDA:2,1\nBRF:4\nBRH:3\n").expect("write lcov"); - - let lcov = read_lcov(&path).expect("parse lcov"); - assert!(lcov.branches_available); - assert_eq!(lcov.branch_total, 4); - assert_eq!(lcov.branch_covered, 3); - assert_eq!(lcov.branch_percent, Some(75.0)); - - fs::remove_file(path).expect("remove lcov"); - } - - #[test] - fn gate_fails_when_branch_data_is_required_but_missing() { - let summary = CoverageSummary { - functions_percent: 100.0, - summary_lines_percent: 100.0, - summary_regions_percent: 100.0, - }; - let lcov = LcovCoverage { - executable_total: 10, - executable_covered: 10, - executable_percent: 100.0, - executable_source: ExecutableSource::Da, - branch_total: 0, - branch_covered: 0, - branches_available: false, - branch_percent: None, - }; - let thresholds = CoverageThresholds { - fail_under_exec_lines: 100.0, - fail_under_functions: 100.0, - fail_under_regions: 100.0, - fail_under_branches: 100.0, - require_branches: true, - }; - - let gate = evaluate_gate(&summary, &lcov, thresholds); - assert!(!gate.pass); - assert!( - gate.fail_reasons - .iter() - .any(|reason| reason == "branches=unavailable") - ); - } - - #[test] - fn reads_required_crates_and_rejects_duplicates() { - let path = temp_file_path("required_crates"); - fs::write( - &path, - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"a\", \"b\"]\n", - ) - .expect("write required crates"); - let crates = read_required_crates(&path).expect("parse required crates"); - assert_eq!(crates, vec!["a".to_string(), "b".to_string()]); - fs::remove_file(&path).expect("remove required crates"); - - let dup_path = temp_file_path("required_crates_dup"); - fs::write( - &dup_path, - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"a\", \"a\"]\n", - ) - .expect("write dup required crates"); - let err = read_required_crates(&dup_path).expect_err("duplicate required crates"); - assert!(err.contains("duplicate crate a")); - fs::remove_file(dup_path).expect("remove dup required crates"); - } - - #[test] - fn read_required_crates_reports_read_and_parse_errors() { - let missing = temp_file_path("required_missing"); - let read_err = read_required_crates(&missing).expect_err("missing required file"); - assert!(read_err.contains("failed to read coverage policy")); - - let invalid = temp_file_path("required_invalid"); - write_file(&invalid, "not = [toml"); - let parse_err = read_required_crates(&invalid).expect_err("invalid required file"); - assert!(parse_err.contains("failed to parse coverage policy")); - fs::remove_file(invalid).expect("remove invalid required file"); - } - - #[test] - fn reads_workspace_crates_and_contains_xtask() { - let root = workspace_root(); - let crates = read_workspace_crates(&root).expect("workspace crates"); - assert!(!crates.is_empty()); - assert!(crates.iter().any(|crate_name| crate_name == "xtask")); - } - - #[test] - fn coverage_profiles_default_when_contract_file_is_missing() { - let root = temp_dir_path("profile_missing"); - fs::create_dir_all(&root).expect("create root"); - let profile = read_coverage_profile(&root, "radroots_log").expect("read profile"); - assert!(!profile.no_default_features); - assert!(profile.features.is_empty()); - assert_eq!(profile.test_threads, None); - fs::remove_dir_all(root).expect("remove root"); - } - - #[test] - fn coverage_profiles_merge_defaults_and_crate_overrides() { - let root = temp_dir_path("profile_merge"); - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - fs::write( - coverage_dir.join("coverage-profiles.toml"), - r#"[profiles.default] -no_default_features = false -features = ["std"] -test_threads = 2 - -[profiles.crates."radroots_log"] -no_default_features = true -features = ["rt"] -"#, - ) - .expect("write profiles"); - - let app_profile = read_coverage_profile(&root, "radroots_log").expect("app profile"); - assert!(app_profile.no_default_features); - assert_eq!(app_profile.features, vec!["rt".to_string()]); - assert_eq!(app_profile.test_threads, Some(2)); - - let other_profile = read_coverage_profile(&root, "radroots_types").expect("other profile"); - assert!(!other_profile.no_default_features); - assert_eq!(other_profile.features, vec!["std".to_string()]); - assert_eq!(other_profile.test_threads, Some(2)); - - fs::remove_dir_all(root).expect("remove root"); - } - - #[test] - fn coverage_profiles_accept_positive_test_threads() { - let root = temp_dir_path("profile_positive_threads"); - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - fs::write( - coverage_dir.join("coverage-profiles.toml"), - r#"[profiles.crates."radroots_log"] -test_threads = 4 -"#, - ) - .expect("write profiles"); - let profile = - read_coverage_profile(&root, "radroots_log").expect("valid positive thread profile"); - assert_eq!(profile.test_threads, Some(4)); - fs::remove_dir_all(root).expect("remove root"); - } - - #[test] - fn coverage_profiles_reject_invalid_feature_and_thread_values() { - let root = temp_dir_path("profile_invalid"); - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - fs::write( - coverage_dir.join("coverage-profiles.toml"), - r#"[profiles.crates."radroots_log"] -features = [""] -test_threads = 0 -"#, - ) - .expect("write profiles"); - - let err = read_coverage_profile(&root, "radroots_log").expect_err("invalid profile"); - assert!( - err.contains("empty feature value"), - "unexpected error: {err}" - ); - - fs::remove_dir_all(root).expect("remove root"); - } - - #[test] - fn coverage_profiles_reject_invalid_toml() { - let root = temp_dir_path("profile_invalid_toml"); - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - fs::write( - coverage_dir.join("coverage-profiles.toml"), - "[profiles.default\n", - ) - .expect("write invalid profiles"); - let err = read_coverage_profile(&root, "radroots_log").expect_err("invalid toml"); - assert!(err.contains("failed to parse")); - fs::remove_dir_all(root).expect("remove root"); - } - - #[test] - fn coverage_profiles_reject_zero_test_threads_without_feature_error() { - let root = temp_dir_path("profile_invalid_threads"); - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - fs::write( - coverage_dir.join("coverage-profiles.toml"), - r#"[profiles.crates."radroots_log"] -test_threads = 0 -"#, - ) - .expect("write profiles"); - - let err = read_coverage_profile(&root, "radroots_log").expect_err("invalid thread count"); - assert!(err.contains("test_threads > 0")); - - fs::remove_dir_all(root).expect("remove root"); - } - - #[test] - fn parse_helpers_cover_success_and_error_paths() { - let args = vec![ - "--scope".to_string(), - "crate-a".to_string(), - "--value".to_string(), - "3.5".to_string(), - "--threads".to_string(), - "4".to_string(), - "--flag".to_string(), - ]; - assert_eq!( - parse_string_arg(&args, "scope").expect("scope value"), - "crate-a".to_string() - ); - assert_eq!( - parse_optional_string_arg(&args, "scope").expect("optional scope"), - "crate-a".to_string() - ); - assert_eq!(parse_f64_arg(&args, "value", 1.0).expect("f64 value"), 3.5); - assert_eq!( - parse_optional_u32_arg(&args, "threads").expect("u32 value"), - Some(4) - ); - assert!(parse_bool_flag(&args, "flag")); - assert_eq!(parse_optional_string_arg(&args, "missing"), None); - assert_eq!( - parse_f64_arg(&args, "missing", 2.25).expect("default f64"), - 2.25 - ); - assert_eq!( - parse_optional_u32_arg(&args, "missing").expect("missing u32"), - None - ); - - let missing_err = parse_string_arg(&args, "absent").expect_err("missing arg"); - assert!(missing_err.contains("missing --absent")); - - let missing_value = vec!["--scope".to_string()]; - let missing_value_err = - parse_string_arg(&missing_value, "scope").expect_err("missing arg value"); - assert!(missing_value_err.contains("missing value for --scope")); - - let invalid_f64 = vec!["--value".to_string(), "bad".to_string()]; - let invalid_f64_err = parse_f64_arg(&invalid_f64, "value", 1.0).expect_err("invalid f64"); - assert!(invalid_f64_err.contains("invalid --value value")); - - let invalid_u32 = vec!["--threads".to_string(), "bad".to_string()]; - let invalid_u32_err = - parse_optional_u32_arg(&invalid_u32, "threads").expect_err("invalid u32"); - assert!(invalid_u32_err.contains("invalid --threads value")); - } - - #[test] - fn executable_source_labels_cover_all_variants() { - assert_eq!(executable_source_label(ExecutableSource::Da), "da"); - assert_eq!(executable_source_label(ExecutableSource::LfLh), "lf_lh"); - } - - #[test] - fn read_required_crates_rejects_empty_and_blank_entries() { - let empty_path = temp_file_path("required_empty"); - write_file( - &empty_path, - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = []\n", - ); - let empty_err = read_required_crates(&empty_path).expect_err("empty required list"); - assert!(empty_err.contains("must not be empty")); - fs::remove_file(&empty_path).expect("remove empty required file"); - - let blank_path = temp_file_path("required_blank"); - write_file( - &blank_path, - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"a\", \" \"]\n", - ); - let blank_err = read_required_crates(&blank_path).expect_err("blank crate name"); - assert!(blank_err.contains("empty crate name")); - fs::remove_file(&blank_path).expect("remove blank required file"); - } - - #[test] - fn read_workspace_crates_rejects_invalid_workspace_shapes() { - let root_empty = temp_dir_path("workspace_empty_members"); - write_file( - &root_empty.join("Cargo.toml"), - "[workspace]\nmembers = []\n", - ); - let empty_err = read_workspace_crates(&root_empty).expect_err("empty workspace members"); - assert!(empty_err.contains("must not be empty")); - fs::remove_dir_all(&root_empty).expect("remove empty members root"); - - let root_blank = temp_dir_path("workspace_blank_package_name"); - write_file( - &root_blank.join("Cargo.toml"), - "[workspace]\nmembers = [\"crates/a\"]\n", - ); - write_file( - &root_blank.join("crates").join("a").join("Cargo.toml"), - "[package]\nname = \"\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", - ); - let blank_err = read_workspace_crates(&root_blank).expect_err("blank package name"); - assert!(blank_err.contains("empty package name")); - fs::remove_dir_all(&root_blank).expect("remove blank package root"); - - let root_duplicate = temp_dir_path("workspace_duplicate_package"); - write_file( - &root_duplicate.join("Cargo.toml"), - "[workspace]\nmembers = [\"crates/a\", \"crates/b\"]\n", - ); - let package_manifest = - "[package]\nname = \"duplicate\"\nversion = \"0.1.0\"\nedition = \"2024\"\n"; - write_file( - &root_duplicate.join("crates").join("a").join("Cargo.toml"), - package_manifest, - ); - write_file( - &root_duplicate.join("crates").join("b").join("Cargo.toml"), - package_manifest, - ); - let dup_err = read_workspace_crates(&root_duplicate).expect_err("duplicate package names"); - assert!(dup_err.contains("duplicate package name")); - fs::remove_dir_all(&root_duplicate).expect("remove duplicate package root"); - - let root_parse = temp_dir_path("workspace_parse_error"); - write_file( - &root_parse.join("Cargo.toml"), - "[workspace]\nmembers = [\"crates/a\"]\n", - ); - write_file( - &root_parse.join("crates").join("a").join("Cargo.toml"), - "[package", - ); - let parse_err = read_workspace_crates(&root_parse).expect_err("invalid package manifest"); - assert!(parse_err.contains("failed to parse")); - fs::remove_dir_all(&root_parse).expect("remove parse package root"); - } - - #[test] - fn parse_toml_reports_read_and_parse_errors() { - let missing = temp_file_path("parse_toml_missing"); - let read_err = - parse_toml::<CoveragePolicyFile>(&missing).expect_err("missing file should fail"); - assert!(read_err.contains("failed to read")); - - let invalid = temp_file_path("parse_toml_invalid"); - write_file(&invalid, "[gate]\nfail_under_exec_lines = 100.0\n"); - let parse_err = - parse_toml::<CoveragePolicyFile>(&invalid).expect_err("invalid toml should fail"); - assert!(parse_err.contains("failed to parse")); - fs::remove_file(invalid).expect("remove invalid toml"); - - let workspace_missing = temp_file_path("parse_toml_workspace_missing"); - let workspace_read_err = parse_toml::<WorkspaceManifest>(&workspace_missing) - .expect_err("missing workspace manifest should fail"); - assert!(workspace_read_err.contains("failed to read")); - - let workspace_invalid = temp_file_path("parse_toml_workspace_invalid"); - write_file(&workspace_invalid, "[workspace"); - let workspace_parse_err = parse_toml::<WorkspaceManifest>(&workspace_invalid) - .expect_err("invalid workspace manifest should fail"); - assert!(workspace_parse_err.contains("failed to parse")); - fs::remove_file(workspace_invalid).expect("remove invalid workspace manifest"); - - let package_missing = temp_file_path("parse_toml_package_missing"); - let package_read_err = parse_toml::<PackageManifest>(&package_missing) - .expect_err("missing package manifest should fail"); - assert!(package_read_err.contains("failed to read")); - - let package_invalid = temp_file_path("parse_toml_package_invalid"); - write_file(&package_invalid, "[package"); - let package_parse_err = parse_toml::<PackageManifest>(&package_invalid) - .expect_err("invalid package manifest should fail"); - assert!(package_parse_err.contains("failed to parse")); - fs::remove_file(package_invalid).expect("remove invalid package manifest"); - - let profiles_missing = temp_file_path("parse_toml_profiles_missing"); - let profiles_read_err = parse_toml::<CoverageProfilesFile>(&profiles_missing) - .expect_err("missing coverage profiles should fail"); - assert!(profiles_read_err.contains("failed to read")); - - let profiles_invalid = temp_file_path("parse_toml_profiles_invalid"); - write_file(&profiles_invalid, "[profiles.default"); - let profiles_parse_err = parse_toml::<CoverageProfilesFile>(&profiles_invalid) - .expect_err("invalid coverage profiles should fail"); - assert!(profiles_parse_err.contains("failed to parse")); - fs::remove_file(profiles_invalid).expect("remove invalid coverage profiles"); - } - - #[test] - fn parse_toml_parses_valid_coverage_required_contract() { - let valid = temp_file_path("parse_toml_valid"); - write_file( - &valid, - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_core\"]\n", - ); - let parsed = parse_toml::<CoveragePolicyFile>(&valid).expect("valid toml"); - assert_eq!(parsed.required.crates, vec!["radroots_core".to_string()]); - fs::remove_file(valid).expect("remove valid toml"); - } - - #[test] - fn read_lcov_rejects_invalid_records() { - let cases = vec![ - ("invalid_da_shape", "DA:1\n", "invalid DA record"), - ("invalid_da_hits", "DA:1,bad\n", "invalid DA hit count"), - ("invalid_lf", "LF:bad\n", "invalid LF value"), - ("invalid_lh", "LH:bad\n", "invalid LH value"), - ("invalid_brf", "BRF:bad\n", "invalid BRF value"), - ("invalid_brh", "BRH:bad\n", "invalid BRH value"), - ("invalid_brda_shape", "BRDA:1,0,0\n", "invalid BRDA record"), - ( - "invalid_brda_taken", - "BRDA:1,0,0,bad\n", - "invalid BRDA taken count", - ), - ( - "invalid_brda_extra", - "BRDA:1,0,0,1,extra\n", - "invalid BRDA record", - ), - ]; - for (prefix, raw, expected) in cases { - let path = temp_file_path(prefix); - write_file(&path, raw); - let err = read_lcov(&path).expect_err("invalid lcov record"); - assert!( - err.contains(expected), - "expected `{expected}` in `{err}` for case {prefix}" - ); - fs::remove_file(path).expect("remove invalid lcov file"); - } - } - - #[test] - fn read_lcov_reports_read_error() { - let missing = temp_file_path("lcov_missing"); - let err = read_lcov(&missing).expect_err("missing lcov should fail"); - assert!(err.contains("failed to read lcov")); - } - - #[test] - fn read_lcov_uses_lf_lh_when_da_is_missing_and_branches_absent() { - let path = temp_file_path("lcov_lf_lh"); - fs::write(&path, "LF:4\nLH:3\n").expect("write lcov"); - let parsed = read_lcov(&path).expect("parse lcov"); - assert_eq!(executable_source_label(parsed.executable_source), "lf_lh"); - assert_eq!(parsed.executable_total, 4); - assert_eq!(parsed.executable_covered, 3); - assert_eq!(parsed.executable_percent, 75.0); - assert!(!parsed.branches_available); - assert_eq!(parsed.branch_percent, None); - fs::remove_file(path).expect("remove lcov"); - } - - #[test] - fn read_lcov_defaults_to_full_when_no_line_records_exist() { - let path = temp_file_path("lcov_empty"); - write_file(&path, "TN:probe\n"); - let parsed = read_lcov(&path).expect("parse lcov"); - assert_eq!(parsed.executable_total, 0); - assert_eq!(parsed.executable_covered, 0); - assert_eq!(parsed.executable_percent, 100.0); - assert!(!parsed.branches_available); - assert_eq!(parsed.branch_percent, None); - fs::remove_file(path).expect("remove lcov"); - } - - #[test] - fn evaluate_gate_collects_all_failure_reasons() { - let summary = CoverageSummary { - functions_percent: 40.0, - summary_lines_percent: 50.0, - summary_regions_percent: 60.0, - }; - let lcov = LcovCoverage { - executable_total: 20, - executable_covered: 10, - executable_percent: 50.0, - executable_source: ExecutableSource::Da, - branch_total: 10, - branch_covered: 3, - branches_available: true, - branch_percent: Some(30.0), - }; - let thresholds = CoverageThresholds { - fail_under_exec_lines: 90.0, - fail_under_functions: 90.0, - fail_under_regions: 90.0, - fail_under_branches: 90.0, - require_branches: true, - }; - - let gate = evaluate_gate(&summary, &lcov, thresholds); - assert!(!gate.pass); - assert!( - gate.fail_reasons - .iter() - .any(|reason| reason.contains("executable_lines")) - ); - assert!( - gate.fail_reasons - .iter() - .any(|reason| reason.contains("functions")) - ); - assert!( - gate.fail_reasons - .iter() - .any(|reason| reason.contains("regions")) - ); - assert!( - gate.fail_reasons - .iter() - .any(|reason| reason.contains("branches")) - ); - } - - #[test] - fn run_command_covers_success_and_failure() { - let mut ok = Command::new("sh"); - ok.arg("-c").arg("exit 0"); - run_command(ok, "shell ok").expect("run ok command"); - - let mut fail = Command::new("sh"); - fail.arg("-c").arg("exit 9"); - let err = run_command(fail, "shell fail").expect_err("run failing command"); - assert!(err.contains("shell fail failed with status")); - - let missing = Command::new("/definitely/not/a/real/command"); - let err = run_command(missing, "shell missing").expect_err("missing command"); - assert!(err.contains("failed to run shell missing")); - } - - #[test] - fn apply_coverage_profile_flags_writes_expected_args() { - let profile = CoverageProfile { - no_default_features: true, - features: vec!["std".to_string(), "serde".to_string()], - test_threads: Some(2), - }; - let mut command = Command::new("cargo"); - apply_coverage_profile_flags(&mut command, &profile); - let args = command - .get_args() - .map(|arg| arg.to_string_lossy().to_string()) - .collect::<Vec<_>>(); - assert_eq!( - args, - vec![ - "--no-default-features".to_string(), - "--features".to_string(), - "std,serde".to_string() - ] - ); - } - - #[test] - fn run_crate_with_runner_builds_all_command_steps() { - let out = temp_dir_path("run_crate_runner"); - let args = vec![ - "--crate".to_string(), - "radroots_core".to_string(), - "--out".to_string(), - out.display().to_string(), - "--test-threads".to_string(), - "3".to_string(), - ]; - let mut names = Vec::new(); - let mut rendered_commands = Vec::new(); - let mut runner = |cmd: Command, name: &str| { - names.push(name.to_string()); - let rendered = cmd - .get_args() - .map(|arg| arg.to_string_lossy().to_string()) - .collect::<Vec<_>>() - .join(" "); - assert!(!rendered.is_empty()); - rendered_commands.push(rendered); - Ok(()) - }; - run_crate_with_runner(&args, &mut runner).expect("run crate with stub runner"); - assert_eq!( - names, - vec![ - "cargo llvm-cov clean --workspace".to_string(), - "cargo llvm-cov --no-report".to_string(), - "cargo llvm-cov report --json --summary-only".to_string(), - "cargo llvm-cov report --json".to_string(), - "cargo llvm-cov report --lcov".to_string(), - ] - ); - assert!( - rendered_commands - .iter() - .filter(|rendered| rendered.contains("report -p radroots_core")) - .all(|rendered| rendered.contains("--ignore-filename-regex")) - ); - assert!( - rendered_commands - .iter() - .filter(|rendered| rendered.contains("report -p radroots_core")) - .all(|rendered| rendered.contains(COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX)) - ); - fs::remove_dir_all(out).expect("remove run crate output dir"); - } - - #[test] - fn coverage_ignore_filename_regex_excludes_external_and_sibling_workspace_paths() { - let root = workspace_root(); - let ignore_regex = - coverage_ignore_filename_regex(&root, "radroots_core").expect("build ignore regex"); - assert!(ignore_regex.contains(COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX)); - assert!(ignore_regex.contains("crates/identity")); - assert!(ignore_regex.contains("crates/core/tests")); - assert!(!ignore_regex.contains("crates/core/src")); - } - - #[test] - fn escape_regex_literal_escapes_regex_metacharacters() { - let escaped = escape_regex_literal(r"\.+*?()|[]{}^$"); - assert_eq!(escaped, r"\\\.\+\*\?\(\)\|\[\]\{\}\^\$"); - } - - #[test] - fn coverage_cargo_command_defaults_to_rustup_nightly() { - let cmd = coverage_cargo_command_with_override(None); - let mut args = Vec::new(); - for arg in cmd.get_args() { - args.push(arg.to_string_lossy().to_string()); - } - - assert_eq!(cmd.get_program().to_string_lossy(), "rustup"); - assert_eq!( - args, - vec![ - "run".to_string(), - "nightly".to_string(), - "cargo".to_string() - ] - ); - } - - #[test] - fn normalized_coverage_cargo_override_trims_and_filters_values() { - assert_eq!( - normalized_coverage_cargo_override(Some(" /tmp/cargo ".to_string())), - Some("/tmp/cargo".to_string()) - ); - assert_eq!( - normalized_coverage_cargo_override(Some(" ".to_string())), - None - ); - assert_eq!(normalized_coverage_cargo_override(None), None); - } - - fn assert_coverage_command_shapes( - cargo_cmd: Command, - llvm_cov_cmd: Command, - override_binary: Option<&str>, - ) { - match override_binary { - Some(binary) => assert_eq!(cargo_cmd.get_program().to_string_lossy(), binary), - None => assert_eq!(cargo_cmd.get_program().to_string_lossy(), "rustup"), - } - - let llvm_args = llvm_cov_cmd - .get_args() - .map(|arg| arg.to_string_lossy().to_string()) - .collect::<Vec<_>>(); - match override_binary { - Some(_) => assert_eq!(llvm_args, vec!["llvm-cov".to_string()]), - None => assert_eq!( - llvm_args, - vec![ - "run".to_string(), - "nightly".to_string(), - "cargo".to_string(), - "llvm-cov".to_string() - ] - ), - } - } - - #[test] - fn coverage_public_command_helpers_match_current_env_resolution() { - let mut default_llvm_cov_cmd = coverage_cargo_command_with_override(None); - default_llvm_cov_cmd.arg("llvm-cov"); - assert_coverage_command_shapes( - coverage_cargo_command_with_override(None), - default_llvm_cov_cmd, - None, - ); - - let explicit_binary = temp_dir_path("coverage_command_override") - .join("nightly-cargo") - .to_string_lossy() - .to_string(); - let mut explicit_llvm_cov_cmd = - coverage_cargo_command_with_override(Some(&explicit_binary)); - explicit_llvm_cov_cmd.arg("llvm-cov"); - assert_coverage_command_shapes( - coverage_cargo_command_with_override(Some(&explicit_binary)), - explicit_llvm_cov_cmd, - Some(explicit_binary.as_str()), - ); - - let override_binary = - normalized_coverage_cargo_override(std::env::var("RADROOTS_COVERAGE_CARGO").ok()); - assert_coverage_command_shapes( - coverage_cargo_command(), - coverage_llvm_cov_command(), - override_binary.as_deref(), - ); - } - - #[test] - fn configure_coverage_toolchain_env_sets_existing_binary_envs() { - let toolchain_dir = temp_dir_path("coverage_toolchain_env"); - fs::create_dir_all(&toolchain_dir).expect("create toolchain env dir"); - for binary in ["rustc", "rustdoc", "llvm-cov", "llvm-profdata"] { - write_file(&toolchain_dir.join(binary), ""); - } - - let mut cmd = Command::new("cargo"); - configure_coverage_toolchain_env(&mut cmd, &toolchain_dir); - let envs = collect_command_envs(&cmd); - assert_eq!( - envs.get("RUSTC"), - Some(&Some( - toolchain_dir.join("rustc").to_string_lossy().to_string() - )) - ); - assert_eq!( - envs.get("RUSTDOC"), - Some(&Some( - toolchain_dir.join("rustdoc").to_string_lossy().to_string() - )) - ); - assert_eq!( - envs.get("LLVM_COV"), - Some(&Some( - toolchain_dir.join("llvm-cov").to_string_lossy().to_string() - )) - ); - assert_eq!( - envs.get("LLVM_PROFDATA"), - Some(&Some( - toolchain_dir - .join("llvm-profdata") - .to_string_lossy() - .to_string() - )) - ); - - fs::remove_dir_all(toolchain_dir).expect("remove toolchain env dir"); - } - - #[test] - fn configure_coverage_toolchain_env_skips_missing_binary_envs() { - let toolchain_dir = temp_dir_path("coverage_toolchain_missing_env"); - fs::create_dir_all(&toolchain_dir).expect("create missing env dir"); - - let mut cmd = Command::new("cargo"); - configure_coverage_toolchain_env(&mut cmd, &toolchain_dir); - let envs = collect_command_envs(&cmd); - assert!(!envs.contains_key("RUSTC")); - assert!(!envs.contains_key("RUSTDOC")); - assert!(!envs.contains_key("LLVM_COV")); - assert!(!envs.contains_key("LLVM_PROFDATA")); - - fs::remove_dir_all(toolchain_dir).expect("remove missing env dir"); - } - - #[test] - fn coverage_cargo_command_override_variants_cover_parented_and_parentless_paths() { - let toolchain_dir = temp_dir_path("coverage_toolchain_override"); - fs::create_dir_all(&toolchain_dir).expect("create toolchain dir"); - for binary in [ - "nightly-cargo", - "rustc", - "rustdoc", - "llvm-cov", - "llvm-profdata", - ] { - write_file(&toolchain_dir.join(binary), ""); - } - - let default_cmd = coverage_cargo_command_with_override(None); - let mut args = Vec::new(); - for arg in default_cmd.get_args() { - args.push(arg.to_string_lossy().to_string()); - } - assert_eq!(default_cmd.get_program().to_string_lossy(), "rustup"); - assert_eq!( - args, - vec![ - "run".to_string(), - "nightly".to_string(), - "cargo".to_string() - ] - ); - - let override_binary = toolchain_dir.join("nightly-cargo"); - let cmd = coverage_cargo_command_with_override(Some( - override_binary - .to_str() - .expect("override path should be utf-8"), - )); - - assert_eq!( - cmd.get_program().to_string_lossy(), - override_binary.to_string_lossy() - ); - assert!(cmd.get_args().next().is_none()); - let mut envs = collect_command_envs(&cmd); - envs.insert("MISSING".to_string(), None); - assert_eq!( - envs.get("RUSTC"), - Some(&Some( - toolchain_dir.join("rustc").to_string_lossy().to_string() - )) - ); - assert_eq!( - envs.get("RUSTDOC"), - Some(&Some( - toolchain_dir.join("rustdoc").to_string_lossy().to_string() - )) - ); - assert_eq!( - envs.get("LLVM_COV"), - Some(&Some( - toolchain_dir.join("llvm-cov").to_string_lossy().to_string() - )) - ); - assert_eq!( - envs.get("LLVM_PROFDATA"), - Some(&Some( - toolchain_dir - .join("llvm-profdata") - .to_string_lossy() - .to_string() - )) - ); - let path_env = envs - .get("PATH") - .and_then(|value| value.as_ref()) - .expect("override binary should prepend PATH"); - assert!(path_env.starts_with(toolchain_dir.to_string_lossy().as_ref())); - let mut cmd = coverage_cargo_command_with_override(Some("/")); - cmd.env_remove("RUSTC"); - cmd.env_remove("LLVM_COV"); - assert_eq!(cmd.get_program().to_string_lossy(), "/"); - let envs = collect_command_envs(&cmd); - assert_eq!(envs.get("RUSTC"), Some(&None)); - assert_eq!(envs.get("LLVM_COV"), Some(&None)); - - fs::remove_dir_all(toolchain_dir).expect("remove toolchain dir"); - } - - #[test] - fn workspace_root_override_takes_precedence() { - let root = workspace_root_with_override(Some("/tmp/radroots-coverage-root")); - assert_eq!(root, PathBuf::from("/tmp/radroots-coverage-root")); - - let fallback = workspace_root_with_override(Some("")); - assert!(fallback.join("Cargo.toml").exists()); - - let default_root = workspace_root_with_override(None); - assert!(default_root.join("Cargo.toml").exists()); - } - - #[test] - fn prepend_toolchain_bin_to_path_covers_missing_and_existing_path_inputs() { - let toolchain_dir = PathBuf::from("/tmp/radroots-coverage-toolchain"); - let no_path = prepend_toolchain_bin_to_path(&toolchain_dir, None); - assert_eq!(no_path, OsString::from(&toolchain_dir)); - - let joined = - prepend_toolchain_bin_to_path(&toolchain_dir, Some(OsString::from("/usr/bin:/bin"))); - let joined = joined.to_string_lossy().to_string(); - assert!(joined.starts_with("/tmp/radroots-coverage-toolchain")); - assert!(joined.contains("/usr/bin")); - } - - #[test] - fn collect_command_envs_cover_helper_paths() { - let mut cmd = Command::new("sh"); - cmd.env("PRESENT", "value"); - cmd.env_remove("REMOVED"); - let envs = collect_command_envs(&cmd); - assert_eq!(envs.get("PRESENT"), Some(&Some("value".to_string()))); - assert_eq!(envs.get("REMOVED"), Some(&None)); - } - - #[test] - fn ok_runner_helper_returns_success() { - let cmd = Command::new("true"); - assert!(ok_runner(cmd, "noop").is_ok()); - } - - #[test] - fn run_crate_with_runner_uses_default_output_dir_when_out_is_missing() { - let args = vec!["--crate".to_string(), "radroots_core".to_string()]; - let mut output_path_seen = false; - let mut runner = |cmd: Command, _: &str| { - let rendered = cmd - .get_args() - .map(|arg| arg.to_string_lossy().to_string()) - .collect::<Vec<_>>(); - if rendered - .iter() - .any(|arg| arg.ends_with("coverage-summary.json")) - || rendered - .iter() - .any(|arg| arg.ends_with("coverage-details.json")) - || rendered - .iter() - .any(|arg| arg.ends_with("coverage-lcov.info")) - { - output_path_seen = true; - } - Ok(()) - }; - run_crate_with_runner(&args, &mut runner).expect("run crate with default out"); - assert!(output_path_seen); - } - - #[test] - fn run_crate_with_runner_propagates_runner_failures() { - let out = temp_dir_path("run_crate_runner_fail"); - let args = vec![ - "--crate".to_string(), - "radroots_core".to_string(), - "--out".to_string(), - out.display().to_string(), - ]; - let mut runner = |_: Command, _: &str| Err("runner failed".to_string()); - let err = - run_crate_with_runner(&args, &mut runner).expect_err("runner failure should bubble up"); - assert_eq!(err, "runner failed".to_string()); - fs::remove_dir_all(out).expect("remove run crate failure output dir"); - let root = temp_dir_path("run_crate_create_out_error"); - write_file(&root.join("blocker"), "x"); - let args = vec![ - "--crate".to_string(), - "radroots_core".to_string(), - "--out".to_string(), - root.join("blocker").join("nested").display().to_string(), - ]; - let mut runner = run_command; - let err = run_crate_with_runner(&args, &mut runner) - .expect_err("output dir create error should fail"); - assert!(err.contains("failed to create")); - fs::remove_dir_all(root).expect("remove run crate create error root"); - } - - #[test] - fn run_crate_wrapper_returns_missing_crate_error_without_running_commands() { - let err = run_crate(&[]).expect_err("missing crate flag"); - assert!(err.contains("missing --crate")); - } - - #[test] - fn run_crate_with_runner_at_root_covers_profile_and_runner_error_paths() { - let write_minimal_workspace = |root: &Path| { - write_file( - &root.join("Cargo.toml"), - "[workspace]\nmembers = [\"crates/core\"]\n", - ); - write_file( - &root.join("crates").join("core").join("Cargo.toml"), - "[package]\nname = \"radroots_core\"\nversion = \"0.1.0-alpha.2\"\nedition = \"2024\"\n", - ); - }; - - let profile_root = temp_dir_path("run_crate_profile_invalid"); - write_minimal_workspace(&profile_root); - write_file( - &profile_root - .join("contracts") - .join("coverage-profiles.toml"), - "[profiles.default]\nfeatures = [\"\"]\n", - ); - let profile_args = vec![ - "--crate".to_string(), - "radroots_core".to_string(), - "--out".to_string(), - profile_root.join("out").display().to_string(), - ]; - let mut runner = run_command; - let profile_err = run_crate_with_runner_at_root(&profile_args, &profile_root, &mut runner) - .expect_err("invalid profile should fail"); - assert!(profile_err.contains("empty feature value")); - fs::remove_dir_all(&profile_root).expect("remove profile root"); - - let thread_root = temp_dir_path("run_crate_bad_threads"); - fs::create_dir_all(&thread_root).expect("create thread root"); - write_minimal_workspace(&thread_root); - let thread_args = vec![ - "--crate".to_string(), - "radroots_core".to_string(), - "--out".to_string(), - thread_root.join("out").display().to_string(), - "--test-threads".to_string(), - "bad".to_string(), - ]; - let mut runner = run_command; - let thread_err = run_crate_with_runner_at_root(&thread_args, &thread_root, &mut runner) - .expect_err("invalid test threads should fail"); - assert!(thread_err.contains("invalid --test-threads value")); - fs::remove_dir_all(&thread_root).expect("remove thread root"); - - for fail_step in [2usize, 3usize, 4usize] { - let step_root = temp_dir_path("run_crate_step_fail"); - write_minimal_workspace(&step_root); - let step_args = vec![ - "--crate".to_string(), - "radroots_core".to_string(), - "--out".to_string(), - step_root.join("out").display().to_string(), - ]; - let mut calls = 0usize; - let mut runner = |_: Command, name: &str| { - calls += 1; - if calls == fail_step { - return Err(format!("runner failure at {name}")); - } - Ok(()) - }; - let err = run_crate_with_runner_at_root(&step_args, &step_root, &mut runner) - .expect_err("runner should fail at selected step"); - assert!(err.contains("runner failure at")); - fs::remove_dir_all(&step_root).expect("remove step root"); - } - } - - #[test] - fn report_gate_writes_report_file_on_success() { - let root = temp_dir_path("report_gate_success"); - let summary_path = root.join("summary.json"); - let lcov_path = root.join("coverage.info"); - let out_path = root.join("gate-report.json"); - write_file( - &summary_path, - r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, - ); - write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); - - let args = vec![ - "--scope".to_string(), - "crate-x".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--policy-gate".to_string(), - ]; - report_gate(&args).expect("report gate success"); - let report_raw = fs::read_to_string(&out_path).expect("read report"); - assert!(report_raw.contains("\"scope\": \"crate-x\"")); - assert!(report_raw.contains("\"regions\": 98.0")); - assert!(report_raw.contains("\"pass\": true")); - fs::remove_dir_all(root).expect("remove report gate success root"); - } - - #[test] - fn report_gate_normalizes_duplicate_generic_records_from_details() { - let root = temp_dir_path("report_gate_normalized_generics"); - let summary_path = root.join("summary.json"); - let lcov_path = root.join("coverage.info"); - let out_path = root.join("gate-report.json"); - write_file( - &summary_path, - r#"{ - "data": [ - { - "totals": { - "functions": {"percent": 96.0}, - "lines": {"percent": 99.0}, - "regions": {"percent": 22.0} - } - } - ] -}"#, - ); - write_file( - &root.join("coverage-details.json"), - r#"{ - "data": [ - { - "functions": [ - { - "count": 4, - "filenames": ["/tmp/crates/runtime_manager/src/lib.rs"], - "regions": [ - [10, 1, 12, 2, 4, 0, 0, 0], - [13, 1, 13, 8, 4, 0, 0, 0] - ] - }, - { - "count": 0, - "filenames": ["/tmp/crates/runtime_manager/src/lib.rs"], - "regions": [ - [10, 1, 12, 2, 0, 0, 0, 0], - [13, 1, 13, 8, 0, 0, 0, 0] - ] - } - ] - } - ] -}"#, - ); - write_file( - &lcov_path, - "DA:1,1\nDA:2,0\nLF:2\nLH:1\nBRDA:1,0,0,1\nBRDA:2,0,0,0\n", - ); - - let args = vec![ - "--scope".to_string(), - "radroots_runtime_manager".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--fail-under-exec-lines".to_string(), - "50.0".to_string(), - "--fail-under-functions".to_string(), - "100.0".to_string(), - "--fail-under-regions".to_string(), - "100.0".to_string(), - "--fail-under-branches".to_string(), - "50.0".to_string(), - ]; - report_gate(&args).expect("normalized report gate success"); - - let report_raw = fs::read_to_string(&out_path).expect("read normalized report"); - assert!(report_raw.contains("\"functions_percent\": 100.0")); - assert!(report_raw.contains("\"summary_regions_percent\": 100.0")); - assert!(report_raw.contains("\"pass\": true")); - - fs::remove_dir_all(root).expect("remove normalized report gate root"); - } - - #[test] - fn report_gate_with_root_uses_scope_specific_override_thresholds() { - let root = temp_dir_path("report_gate_override_success"); - let coverage_dir = root.join("contracts"); - fs::create_dir_all(&coverage_dir).expect("create coverage dir"); - write_file( - &coverage_dir.join("coverage.toml"), - "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 88.5\nfail_under_functions = 77.5\nfail_under_regions = 66.5\nfail_under_branches = 55.5\nrequire_branches = false\ntemporary = true\nreason = \"temporary publish unblocker\"\n", - ); - - let summary_path = root.join("summary.json"); - let lcov_path = root.join("coverage.info"); - let out_path = root.join("gate-report.json"); - write_file( - &summary_path, - r#"{"data":[{"totals":{"functions":{"percent":80.0},"lines":{"percent":88.5},"regions":{"percent":70.0}}}]}"#, - ); - write_file(&lcov_path, "DA:1,1\nLF:1\nLH:1\nBRDA:1,0,0,1\n"); - - report_gate_with_root( - &[ - "--scope".to_string(), - "radroots_a".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--policy-gate".to_string(), - ], - &root, - ) - .expect("report gate should honor override"); - - let report_raw = fs::read_to_string(&out_path).expect("read override report"); - assert!(report_raw.contains("\"functions\": 77.5")); - assert!(report_raw.contains("\"regions\": 66.5")); - assert!(report_raw.contains("\"branches_required\": false")); - assert!(report_raw.contains("\"pass\": true")); - - fs::remove_dir_all(root).expect("remove report gate override root"); - } - - #[test] - fn report_gate_returns_error_on_failed_thresholds() { - let root = temp_dir_path("report_gate_fail"); - let summary_path = root.join("summary.json"); - let lcov_path = root.join("coverage.info"); - let out_path = root.join("gate-report.json"); - write_file( - &summary_path, - r#"{"data":[{"totals":{"functions":{"percent":10.0},"lines":{"percent":10.0},"regions":{"percent":10.0}}}]}"#, - ); - write_file(&lcov_path, "DA:1,0\nBRDA:1,0,0,0\n"); - - let args = vec![ - "--scope".to_string(), - "crate-y".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--fail-under-exec-lines".to_string(), - "100.0".to_string(), - "--fail-under-functions".to_string(), - "100.0".to_string(), - "--fail-under-regions".to_string(), - "100.0".to_string(), - "--fail-under-branches".to_string(), - "100.0".to_string(), - ]; - let err = report_gate(&args).expect_err("report gate failure"); - assert!(err.contains("coverage gate failed")); - fs::remove_dir_all(root).expect("remove report gate failure root"); - } - - #[test] - fn report_gate_handles_nan_threshold_input() { - let root = temp_dir_path("report_gate_nan"); - let summary_path = root.join("summary.json"); - let lcov_path = root.join("coverage.info"); - let out_path = root.join("gate-report.json"); - write_file( - &summary_path, - r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, - ); - write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); - - let args = vec![ - "--scope".to_string(), - "crate-nan".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--fail-under-functions".to_string(), - "NaN".to_string(), - ]; - let err = report_gate(&args).expect_err("nan threshold should fail coverage gate"); - assert!(err.contains("invalid --fail-under-functions value")); - fs::remove_dir_all(root).expect("remove report gate nan root"); - } - - #[test] - fn report_gate_reports_write_failure() { - let root = temp_dir_path("report_gate_write_fail"); - let summary_path = root.join("summary.json"); - let lcov_path = root.join("coverage.info"); - let out_path = root.join("gate-report.json"); - write_file( - &summary_path, - r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, - ); - write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); - fs::create_dir_all(&out_path).expect("create directory at output path"); - - let args = vec![ - "--scope".to_string(), - "crate-write".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--policy-gate".to_string(), - ]; - let err = report_gate(&args).expect_err("writing report to directory should fail"); - assert!(err.contains("failed to write")); - fs::remove_dir_all(root).expect("remove report gate write root"); - } - - #[test] - fn report_gate_logs_branch_unavailable_path() { - let root = temp_dir_path("report_gate_no_branches"); - let summary_path = root.join("summary.json"); - let lcov_path = root.join("coverage.info"); - let out_path = root.join("gate-report.json"); - write_file( - &summary_path, - r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, - ); - write_file(&lcov_path, "DA:1,1\n"); - - let args = vec![ - "--scope".to_string(), - "crate-no-branch".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--fail-under-exec-lines".to_string(), - "100.0".to_string(), - "--fail-under-functions".to_string(), - "100.0".to_string(), - "--fail-under-regions".to_string(), - "100.0".to_string(), - "--fail-under-branches".to_string(), - "100.0".to_string(), - ]; - report_gate(&args).expect("report gate no branches"); - let report_raw = fs::read_to_string(&out_path).expect("read report"); - assert!(report_raw.contains("\"branches_available\": false")); - fs::remove_dir_all(root).expect("remove no branch report root"); - } - - #[test] - fn report_gate_reports_argument_and_input_errors() { - let missing_scope = report_gate(&[]).expect_err("missing scope"); - assert!(missing_scope.contains("missing --scope")); - - let missing_summary = report_gate(&["--scope".to_string(), "crate".to_string()]) - .expect_err("missing summary"); - assert!(missing_summary.contains("missing --summary")); - - let missing_lcov = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - "summary.json".to_string(), - ]) - .expect_err("missing lcov"); - assert!(missing_lcov.contains("missing --lcov")); - - let missing_out = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - "summary.json".to_string(), - "--lcov".to_string(), - "coverage.info".to_string(), - ]) - .expect_err("missing out"); - assert!(missing_out.contains("missing --out")); - - let root = temp_dir_path("report_gate_arg_errors"); - let summary_path = root.join("summary.json"); - let lcov_path = root.join("coverage.info"); - let out_path = root.join("gate-report.json"); - write_file( - &summary_path, - r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, - ); - write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); - - let invalid_functions = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--fail-under-functions".to_string(), - "bad".to_string(), - ]) - .expect_err("invalid functions threshold"); - assert!(invalid_functions.contains("invalid --fail-under-functions value")); - - let invalid_exec = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--fail-under-exec-lines".to_string(), - "bad".to_string(), - ]) - .expect_err("invalid executable threshold"); - assert!(invalid_exec.contains("invalid --fail-under-exec-lines value")); - - let invalid_regions = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--fail-under-regions".to_string(), - "bad".to_string(), - ]) - .expect_err("invalid regions threshold"); - assert!(invalid_regions.contains("invalid --fail-under-regions value")); - - let invalid_branches = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--fail-under-branches".to_string(), - "bad".to_string(), - ]) - .expect_err("invalid branches threshold"); - assert!(invalid_branches.contains("invalid --fail-under-branches value")); - - let missing_thresholds = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - ]) - .expect_err("missing thresholds"); - assert!(missing_thresholds.contains("missing coverage thresholds")); - - let missing_functions = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--fail-under-exec-lines".to_string(), - "100".to_string(), - ]) - .expect_err("missing functions threshold"); - assert!(missing_functions.contains("missing coverage thresholds")); - - let missing_regions = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--fail-under-exec-lines".to_string(), - "100".to_string(), - "--fail-under-functions".to_string(), - "100".to_string(), - ]) - .expect_err("missing regions threshold"); - assert!(missing_regions.contains("missing coverage thresholds")); - - let missing_branches = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--fail-under-exec-lines".to_string(), - "100".to_string(), - "--fail-under-functions".to_string(), - "100".to_string(), - "--fail-under-regions".to_string(), - "100".to_string(), - ]) - .expect_err("missing branches threshold"); - assert!(missing_branches.contains("missing coverage thresholds")); - - let missing_summary_file = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - root.join("missing-summary.json").display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--policy-gate".to_string(), - ]) - .expect_err("missing summary file should fail"); - assert!(missing_summary_file.contains("failed to read summary")); - - let missing_gate_report = read_gate_report(&root.join("missing-gate-report.json")) - .expect_err("missing gate report should fail"); - assert!(missing_gate_report.contains("failed to read gate report")); - - write_file(&out_path, "{not-json"); - let invalid_gate_report = read_gate_report(&out_path).expect_err("invalid gate report"); - assert!(invalid_gate_report.contains("failed to parse gate report")); - - let missing_lcov_file = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - root.join("missing-lcov.info").display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--policy-gate".to_string(), - ]) - .expect_err("missing lcov file should fail"); - assert!(missing_lcov_file.contains("failed to read lcov")); - - let mixed_policy_gate = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--policy-gate".to_string(), - "--fail-under-functions".to_string(), - "100.0".to_string(), - ]) - .expect_err("policy gate mixed with explicit thresholds"); - assert!(mixed_policy_gate.contains("cannot be combined")); - - let mixed_policy_gate_regions = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--policy-gate".to_string(), - "--fail-under-regions".to_string(), - "100.0".to_string(), - ]) - .expect_err("policy gate mixed with regions threshold"); - assert!(mixed_policy_gate_regions.contains("cannot be combined")); - - let mixed_policy_gate_branches_flag = report_gate(&[ - "--scope".to_string(), - "crate".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--policy-gate".to_string(), - "--require-branches".to_string(), - ]) - .expect_err("policy gate mixed with require-branches"); - assert!(mixed_policy_gate_branches_flag.contains("cannot be combined")); - - fs::remove_dir_all(root).expect("remove report arg errors root"); - } - - #[test] - fn coverage_ignore_filename_regex_reports_unknown_crate() { - let root = temp_dir_path("coverage_unknown_crate_root"); - write_file( - &root.join("Cargo.toml"), - "[workspace]\nmembers = [\"crates/core\"]\n", - ); - write_file( - &root.join("crates").join("core").join("Cargo.toml"), - "[package]\nname = \"radroots_core\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", - ); - - let err = coverage_ignore_filename_regex(&root, "radroots_missing") - .expect_err("unknown crate should fail"); - assert!(err.contains("could not resolve crate directory")); - - fs::remove_dir_all(root).expect("remove unknown crate root"); - } - - #[test] - fn coverage_ignore_filename_regex_reports_workspace_manifest_errors() { - let root = temp_dir_path("coverage_regex_workspace_error_root"); - let read_err = coverage_ignore_filename_regex(&root, "radroots_core") - .expect_err("missing workspace manifest should fail"); - assert!(read_err.contains("failed to read")); - - write_file(&root.join("Cargo.toml"), "[workspace"); - let parse_err = coverage_ignore_filename_regex(&root, "radroots_core") - .expect_err("invalid workspace manifest should fail"); - assert!(parse_err.contains("failed to parse")); - - fs::remove_dir_all(root).expect("remove workspace error root"); - } - - #[test] - fn run_crate_with_runner_at_root_reports_ignore_filter_errors() { - let root = temp_dir_path("run_crate_ignore_filter_error"); - write_file( - &root.join("Cargo.toml"), - "[workspace]\nmembers = [\"crates/other\"]\n", - ); - write_file( - &root.join("crates").join("other").join("Cargo.toml"), - "[package]\nname = \"radroots_other\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", - ); - let args = vec![ - "--crate".to_string(), - "radroots_core".to_string(), - "--out".to_string(), - root.join("target").join("coverage").display().to_string(), - ]; - let mut runner = ok_runner; - let err = run_crate_with_runner_at_root(&args, &root, &mut runner) - .expect_err("missing crate coverage filter should fail"); - assert!(err.contains("could not resolve crate directory")); - - fs::remove_dir_all(root).expect("remove run crate ignore filter root"); - } - - #[test] - fn run_dispatches_subcommands_and_errors() { - run(&["help".to_string()]).expect("help subcommand"); - run(&["required-crates".to_string()]).expect("required crates subcommand"); - run(&["workspace-crates".to_string()]).expect("workspace crates subcommand"); - let run_crate_err = run(&["run-crate".to_string()]).expect_err("run crate missing args"); - assert!(run_crate_err.contains("missing --crate")); - let unknown_err = run(&["unknown".to_string()]).expect_err("unknown subcommand"); - assert!(unknown_err.contains("unknown sdk coverage subcommand")); - let missing_err = run(&[]).expect_err("missing subcommand"); - assert!(missing_err.contains("missing sdk coverage subcommand")); - } - - #[test] - fn list_root_helpers_report_missing_contract_files() { - let root = temp_dir_path("list_helper_missing"); - fs::create_dir_all(&root).expect("create list helper root"); - let mut output = Vec::new(); - let required_err = list_required_crates_with_root(&root, &mut output) - .expect_err("missing required crates file should fail"); - assert!(required_err.contains("failed to read coverage policy")); - - let workspace_err = list_workspace_crates_with_root(&root, &mut output) - .expect_err("missing workspace manifest should fail"); - assert!(workspace_err.contains("failed to read")); - - fs::remove_dir_all(root).expect("remove list helper root"); - } - - #[test] - fn write_crate_names_output_covers_success_and_error_paths() { - let mut output = Vec::new(); - write_crate_names_output( - &mut output, - vec!["radroots_a".to_string(), "radroots_b".to_string()], - "required crates", - ) - .expect("write crate names"); - let rendered = String::from_utf8(output).expect("utf8"); - assert!(rendered.contains("radroots_a")); - assert!(rendered.contains("radroots_b")); - - let mut failing = FailingWriter; - let err = write_crate_names_output( - &mut failing, - vec!["radroots_a".to_string()], - "workspace crates", - ) - .expect_err("writer failure"); - assert!(err.contains("failed to write workspace crates output")); - failing.flush().expect("flush failing writer"); - } - - #[test] - fn run_report_subcommand_dispatches_to_report_gate() { - let root = temp_dir_path("run_dispatch_report"); - let summary_path = root.join("summary.json"); - let lcov_path = root.join("coverage.info"); - let out_path = root.join("gate-report.json"); - write_file( - &summary_path, - r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, - ); - write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); - - run(&[ - "report".to_string(), - "--scope".to_string(), - "dispatch".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--policy-gate".to_string(), - ]) - .expect("dispatch report"); - assert!(out_path.exists()); - fs::remove_dir_all(root).expect("remove report dispatch root"); - } - - #[test] - fn report_gate_with_root_reports_policy_read_errors() { - let root = temp_dir_path("report_gate_policy_root_error"); - let summary_path = root.join("summary.json"); - let lcov_path = root.join("coverage.info"); - let out_path = root.join("gate-report.json"); - write_file( - &summary_path, - r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, - ); - write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); - - let err = report_gate_with_root( - &[ - "--scope".to_string(), - "crate-x".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - out_path.display().to_string(), - "--policy-gate".to_string(), - ], - &root, - ) - .expect_err("missing policy should fail"); - assert!(err.contains("failed to read coverage policy")); - - fs::remove_dir_all(root).expect("remove report gate policy error root"); - } -} diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs @@ -1,245 +0,0 @@ -#![cfg_attr(coverage_nightly, feature(coverage_attribute))] -#![forbid(unsafe_code)] - -#[cfg_attr(coverage_nightly, coverage(off))] -mod contract; -mod coverage; -#[cfg_attr(coverage_nightly, coverage(off))] -mod phase1_1; - -use std::env; -use std::path::{Path, PathBuf}; -use std::process::ExitCode; - -fn usage() { - eprintln!("usage:"); - eprintln!(" cargo xtask sdk validate"); - eprintln!(" cargo xtask sdk release preflight"); - eprintln!(" cargo xtask sdk coverage run-crate --crate <crate> [--out <dir>]"); - eprintln!(" cargo xtask sdk coverage required-crates"); - eprintln!(" cargo xtask sdk coverage workspace-crates"); - eprintln!( - " cargo xtask sdk coverage report --scope <scope> --summary <file> --lcov <file> --out <file> [--policy-gate | (--fail-under-exec-lines <pct> --fail-under-functions <pct> --fail-under-regions <pct> --fail-under-branches <pct> [--require-branches])]" - ); - eprintln!( - " cargo xtask sdk coverage report-missing --scope <scope> --out <file> --reason <reason>" - ); - eprintln!( - " cargo xtask sdk coverage refresh-summary [--reports-root <dir>] [--out <file>] [--status-out <file>]" - ); - eprintln!(" cargo xtask phase1-1 invariants"); -} - -fn workspace_root_with_override(override_root: Option<&str>) -> PathBuf { - if let Some(raw) = override_root { - let trimmed = raw.trim(); - if !trimmed.is_empty() { - return PathBuf::from(trimmed); - } - } - let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); - let crates_dir = manifest_dir.parent().unwrap_or(manifest_dir); - let root = crates_dir.parent().unwrap_or(crates_dir); - root.to_path_buf() -} - -fn workspace_root() -> PathBuf { - let override_root = env::var("RADROOTS_WORKSPACE_ROOT").ok(); - workspace_root_with_override(override_root.as_deref()) -} - -fn validate_contract() -> Result<(), String> { - let root = workspace_root(); - contract::load_contract_bundle(&root) - .and_then(|bundle| contract::validate_contract_bundle(&bundle)) - .and_then(|_| contract::validate_canonical_event_boundary(&root)) -} - -#[cfg_attr(coverage_nightly, coverage(off))] -fn release_preflight() -> Result<(), String> { - contract::validate_release_preflight(&workspace_root()) -} - -fn run_release(args: &[String]) -> Result<(), String> { - match args.first().map(String::as_str) { - Some("preflight") => release_preflight(), - _ => Err("unknown release subcommand".to_string()), - } -} - -fn run_sdk(args: &[String]) -> Result<(), String> { - match args.first().map(String::as_str) { - Some("validate") => validate_contract(), - Some("release") => run_release(&args[1..]), - Some("coverage") => coverage::run(&args[1..]), - _ => Err("unknown sdk subcommand".to_string()), - } -} - -fn run(args: &[String]) -> Result<(), String> { - match args.first().map(String::as_str) { - Some("sdk") => run_sdk(&args[1..]), - Some("phase1-1") => phase1_1::run(&args[1..], &workspace_root()), - _ => Err("unknown command".to_string()), - } -} - -fn main_with_args(args: Vec<String>) -> ExitCode { - if args.is_empty() { - usage(); - return ExitCode::from(2); - } - match run(&args) { - Ok(()) => ExitCode::SUCCESS, - Err(err) => { - eprintln!("{err}"); - usage(); - ExitCode::from(2) - } - } -} - -#[cfg_attr(coverage_nightly, coverage(off))] -fn main() -> ExitCode { - main_with_args(env::args().skip(1).collect()) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use std::sync::{Mutex, MutexGuard, OnceLock}; - use std::time::{SystemTime, UNIX_EPOCH}; - - fn workspace_lock() -> &'static Mutex<()> { - static LOCK: OnceLock<Mutex<()>> = OnceLock::new(); - LOCK.get_or_init(|| Mutex::new(())) - } - - fn lock_workspace() -> MutexGuard<'static, ()> { - match workspace_lock().lock() { - Ok(guard) => guard, - Err(poison) => poison.into_inner(), - } - } - - fn unique_temp_dir(prefix: &str) -> PathBuf { - let ns = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time") - .as_nanos(); - std::env::temp_dir().join(format!("radroots_xtask_main_{prefix}_{ns}")) - } - - #[test] - fn workspace_root_resolves() { - let root = workspace_root(); - assert!(root.join("Cargo.toml").exists()); - } - - #[test] - fn workspace_root_override_takes_precedence() { - let root = workspace_root_with_override(Some("/tmp/radroots-test-root")); - assert_eq!(root, PathBuf::from("/tmp/radroots-test-root")); - - let fallback = workspace_root_with_override(Some(" ")); - assert!(fallback.join("Cargo.toml").exists()); - - let default_root = workspace_root_with_override(None); - assert!(default_root.join("Cargo.toml").exists()); - } - - #[test] - fn run_release_and_dispatchers_cover_error_paths() { - let unknown_release = - run_release(&["unknown".to_string()]).expect_err("unknown release subcommand"); - assert!(unknown_release.contains("unknown release subcommand")); - - let unknown_sdk = run_sdk(&["unknown".to_string()]).expect_err("unknown sdk subcommand"); - assert!(unknown_sdk.contains("unknown sdk subcommand")); - - let unknown_root = run(&["unknown".to_string()]).expect_err("unknown command"); - assert!(unknown_root.contains("unknown command")); - } - - #[test] - fn lock_workspace_recovers_from_poisoned_mutex() { - let handle = std::thread::spawn(|| { - let _guard = workspace_lock().lock().expect("lock workspace"); - panic!("poison workspace lock"); - }); - assert!(handle.join().is_err()); - - let _guard = lock_workspace(); - } - - #[test] - fn contract_and_coverage_dispatchers_execute() { - let _guard = lock_workspace(); - let out_dir = unique_temp_dir("coverage_dispatch"); - fs::create_dir_all(&out_dir).expect("create out dir"); - - validate_contract().expect("validate contract"); - run_sdk(&["coverage".to_string(), "help".to_string()]).expect("coverage help"); - run_sdk(&["coverage".to_string(), "required-crates".to_string()]) - .expect("coverage required crates"); - run_sdk(&["coverage".to_string(), "workspace-crates".to_string()]) - .expect("coverage workspace crates"); - - let summary_path = out_dir.join("summary.json"); - let lcov_path = out_dir.join("coverage.info"); - let gate_out = out_dir.join("gate-report.json"); - fs::write( - &summary_path, - r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, - ) - .expect("write summary"); - fs::write(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n").expect("write lcov"); - run_sdk(&[ - "coverage".to_string(), - "report".to_string(), - "--scope".to_string(), - "main-test".to_string(), - "--summary".to_string(), - summary_path.display().to_string(), - "--lcov".to_string(), - lcov_path.display().to_string(), - "--out".to_string(), - gate_out.display().to_string(), - "--policy-gate".to_string(), - ]) - .expect("coverage report"); - - run(&[ - "sdk".to_string(), - "coverage".to_string(), - "help".to_string(), - ]) - .expect("root run sdk coverage"); - run(&["phase1-1".to_string(), "invariants".to_string()]).expect("phase1-1 invariants"); - - let _ = fs::remove_dir_all(out_dir); - } - - #[test] - fn usage_and_main_entrypoints_execute() { - usage(); - let empty_code = main_with_args(Vec::new()); - assert_eq!(empty_code, ExitCode::from(2)); - let success_code = main_with_args(vec![ - "sdk".to_string(), - "coverage".to_string(), - "help".to_string(), - ]); - assert_eq!(success_code, ExitCode::SUCCESS); - let failure_code = main_with_args(vec!["unknown".to_string()]); - assert_eq!(failure_code, ExitCode::from(2)); - let _ = main(); - } - - #[test] - fn run_sdk_dispatches_validate_command() { - let _guard = lock_workspace(); - run_sdk(&["validate".to_string()]).expect("sdk validate"); - } -} diff --git a/crates/xtask/src/phase1_1.rs b/crates/xtask/src/phase1_1.rs @@ -1,273 +0,0 @@ -use std::fs; -use std::path::{Path, PathBuf}; - -pub fn run(args: &[String], root: &Path) -> Result<(), String> { - match args.first().map(String::as_str) { - Some("invariants") => validate_invariants(root), - _ => Err("unknown phase1-1 subcommand".to_string()), - } -} - -pub fn validate_invariants(root: &Path) -> Result<(), String> { - let mut failures = Vec::new(); - reject_substrings( - root, - &[PathBuf::from("crates/relay_transport/src")], - &["RadrootsEventIngest::verified"], - "relay fetch must not bypass event-store verification", - &mut failures, - ); - reject_substrings( - root, - &[PathBuf::from("crates/event_store/src")], - &["last_created_at", "last_event_id"], - "event-store projection cursors must use last_event_seq", - &mut failures, - ); - reject_raw_protocol_strings(root, &mut failures); - reject_substrings( - root, - &[ - PathBuf::from("crates/events/src"), - PathBuf::from("crates/events_codec/src"), - PathBuf::from("crates/trade/src"), - ], - &[ - "RadrootsTradeMessageType", - "RadrootsTradeEnvelope", - "RadrootsTradeMessagePayload", - "RadrootsTradeQuestion", - "RadrootsTradeAnswer", - "RadrootsTradeDiscount", - "RadrootsTradeOrder", - "RadrootsActiveOrder", - "RadrootsActiveTrade", - "RadrootsTradeListingParseError", - "RadrootsTradeDomain", - "TradeListingParseError", - "TradeListingEnvelope", - "TradeListingMessage", - "KIND_TRADE_ORDER", - "TRADE_LISTING_KINDS", - "build_envelope_draft", - "parse_envelope", - "public_trade", - "events::trade::", - "events_codec::trade::", - "trade_order_economics_digest", - "trade_revision", - "trade_lifecycle", - "reduce_active_order", - "canonicalize_active_order", - "active_trade_", - "ActiveOrder", - "active_order", - "active order", - "active trade", - "RADROOTS_TRADE_LISTING_DOMAIN", - "RADROOTS_TRADE_ENVELOPE_VERSION", - ], - "legacy trade identifiers must not reappear", - &mut failures, - ); - - if failures.is_empty() { - println!("phase1-1 invariants passed"); - Ok(()) - } else { - Err(format!( - "phase1-1 invariant violations:\n{}", - failures.join("\n") - )) - } -} - -fn reject_substrings( - root: &Path, - rel_roots: &[PathBuf], - patterns: &[&str], - label: &str, - failures: &mut Vec<String>, -) { - for file in files_under(root, rel_roots) { - let Ok(content) = fs::read_to_string(&file) else { - continue; - }; - for (line_index, line) in content.lines().enumerate() { - for pattern in patterns { - if line.contains(pattern) { - failures.push(format!( - "{label}: {}:{}: {}", - display_path(root, &file), - line_index + 1, - line.trim() - )); - } - } - } - } -} - -fn reject_raw_protocol_strings(root: &Path, failures: &mut Vec<String>) { - let rel_roots = [ - PathBuf::from("crates/events/src"), - PathBuf::from("crates/events_codec/src"), - PathBuf::from("crates/trade/src"), - ]; - for file in files_under(root, &rel_roots) { - let Ok(content) = fs::read_to_string(&file) else { - continue; - }; - let mut struct_name = String::new(); - for (line_index, line) in content.lines().enumerate() { - let trimmed = line.trim(); - if let Some(rest) = trimmed.strip_prefix("pub struct ") { - struct_name = rest - .split(['<', '{', ' ', '(']) - .next() - .unwrap_or_default() - .to_owned(); - } - if trimmed == "}" { - struct_name.clear(); - } - if is_raw_protocol_field(trimmed) && !is_allowed_raw_boundary(&struct_name) { - failures.push(format!( - "raw commercial protocol identifier String fields are forbidden: {}:{}: {}", - display_path(root, &file), - line_index + 1, - trimmed - )); - } - } - } -} - -fn is_raw_protocol_field(line: &str) -> bool { - [ - "pub order_id: String,", - "pub listing_addr: String,", - "pub revision_id: String,", - "pub quote_id: String,", - "pub primary_bin_id: String,", - "pub bin_id: String,", - "pub economics_digest: String,", - ] - .contains(&line) -} - -fn is_allowed_raw_boundary(struct_name: &str) -> bool { - struct_name == "RadrootsOrderEnvelope" - || struct_name == "RadrootsValidationReceiptTags" - || struct_name == "RadrootsTradeListing" - || struct_name.ends_with("Projection") - || struct_name.ends_with("Accounting") - || struct_name.ends_with("Availability") - || struct_name.ends_with("Reservation") - || struct_name.ends_with("Issue") - || struct_name.ends_with("NormalizedInventoryCount") -} - -fn files_under(root: &Path, rel_roots: &[PathBuf]) -> Vec<PathBuf> { - let mut files = Vec::new(); - for rel_root in rel_roots { - collect_files(root.join(rel_root), &mut files); - } - files.sort(); - files -} - -fn collect_files(path: PathBuf, files: &mut Vec<PathBuf>) { - let Ok(metadata) = fs::metadata(&path) else { - return; - }; - if metadata.is_file() { - if matches!( - path.extension().and_then(|ext| ext.to_str()), - Some("rs" | "sql" | "sh") - ) { - files.push(path); - } - return; - } - let Ok(entries) = fs::read_dir(path) else { - return; - }; - for entry in entries.flatten() { - collect_files(entry.path(), files); - } -} - -fn display_path(root: &Path, file: &Path) -> String { - file.strip_prefix(root) - .unwrap_or(file) - .to_string_lossy() - .to_string() -} - -#[cfg(test)] -mod tests { - use super::*; - use std::time::{SystemTime, UNIX_EPOCH}; - - fn unique_temp_dir(prefix: &str) -> PathBuf { - let ns = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time") - .as_nanos(); - std::env::temp_dir().join(format!("radroots_xtask_phase1_1_{prefix}_{ns}")) - } - - fn write_file(root: &Path, rel: &str, content: &str) { - let path = root.join(rel); - fs::create_dir_all(path.parent().expect("parent")).expect("create parent"); - fs::write(path, content).expect("write"); - } - - #[test] - fn invariants_accept_clean_synthetic_tree() { - let root = unique_temp_dir("clean"); - write_file( - &root, - "crates/relay_transport/src/fetch.rs", - "fn fetch() { let _ = RadrootsEventIngest::new; }\n", - ); - write_file( - &root, - "crates/event_store/src/store.rs", - "pub struct RadrootsProjectionCursor { pub last_event_seq: i64 }\n", - ); - write_file( - &root, - "crates/trade/src/order.rs", - "pub struct RadrootsOrderProjection { pub order_id: RadrootsOrderId, }\n", - ); - validate_invariants(&root).expect("clean tree"); - let _ = fs::remove_dir_all(root); - } - - #[test] - fn invariants_reject_phase1_regressions() { - let root = unique_temp_dir("dirty"); - write_file( - &root, - "crates/relay_transport/src/fetch.rs", - "fn fetch() { let _ = RadrootsEventIngest::verified; }\n", - ); - write_file( - &root, - "crates/event_store/src/store.rs", - "pub struct Cursor { pub last_event_id: String }\n", - ); - write_file( - &root, - "crates/trade/src/order.rs", - "pub struct BadOrder {\n pub order_id: String,\n}\n", - ); - let err = validate_invariants(&root).expect_err("dirty tree"); - assert!(err.contains("relay fetch must not bypass event-store verification")); - assert!(err.contains("event-store projection cursors must use last_event_seq")); - assert!(err.contains("raw commercial protocol identifier String fields are forbidden")); - let _ = fs::remove_dir_all(root); - } -} diff --git a/flake.nix b/flake.nix @@ -38,8 +38,8 @@ inherit system; overlays = [ inputs.rust-overlay.overlays.default ]; }; - toolchains = import ./nix/toolchains.nix { inherit pkgs; }; - common = import ./nix/common.nix { + toolchains = import ./build/nix/toolchains.nix { inherit pkgs; }; + common = import ./build/nix/common.nix { crane = inputs.crane; inherit lib pkgs toolchains; }; @@ -47,7 +47,7 @@ { treefmt = import ./treefmt.nix; - apps = import ./nix/apps.nix { + apps = import ./build/nix/apps.nix { inherit common config @@ -58,12 +58,12 @@ }; checks = lib.filterAttrs (_: value: value != null) ( - import ./nix/checks.nix { + import ./build/nix/checks.nix { inherit common pkgs; } ); - devShells = import ./nix/devshells.nix { + devShells = import ./build/nix/devshells.nix { inherit common pkgs toolchains; }; diff --git a/nix/apps.nix b/nix/apps.nix @@ -1,102 +0,0 @@ -{ - common, - config, - lib, - pkgs, - toolchains, -}: -let - stablePath = "export PATH=${toolchains.stable}/bin:$PATH"; - coveragePath = "export PATH=${toolchains.stable}/bin:${toolchains.coverage}/bin:$PATH"; - coverageShellExec = command: '' - exec nix develop .#coverage --accept-flake-config -c sh -lc ${lib.escapeShellArg command} sh "$@" - ''; - mkRepoApp = - { - name, - description ? "Run ${name} in the radroots workspace", - runtimeInputs, - command, - env ? common.exportSharedEnv, - pathPrefix ? stablePath, - }: - let - script = pkgs.writeShellApplication { - inherit name runtimeInputs; - text = '' - set -euo pipefail - - repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" - cd "$repo_root" - - ${common.ensureRepoRoot} - ${env} - ${pathPrefix} - - ${command} - ''; - }; - in - { - type = "app"; - program = "${script}/bin/${name}"; - meta.description = description; - }; -in -{ - check = mkRepoApp { - name = "check"; - description = "Run cargo check across the radroots workspace"; - runtimeInputs = common.runtimeInputs.stable; - command = common.checkCommand; - }; - - contract = mkRepoApp { - name = "contract"; - description = "Run the sdk contract lane"; - runtimeInputs = common.runtimeInputs.stable; - command = common.contractCommand; - }; - - coverage-report = mkRepoApp { - name = "coverage-report"; - description = "Generate sdk coverage reports and blocking gate artifacts"; - runtimeInputs = common.runtimeInputs.coverage; - command = common.coverageReportCommand; - env = common.exportCoverageEnv; - pathPrefix = coveragePath; - }; - - guards = mkRepoApp { - name = "guards"; - description = "Run repository guard scripts"; - runtimeInputs = common.runtimeInputs.stable; - command = '' - ./scripts/ci/guard_no_legacy_identifiers.sh - ''; - }; - - fmt = mkRepoApp { - name = "fmt"; - description = "Format rust, nix, shell, and toml files"; - runtimeInputs = common.runtimeInputs.stable ++ [ - config.treefmt.build.wrapper - ]; - command = '' - cargo fmt --all - ${config.treefmt.build.wrapper}/bin/treefmt - ''; - }; - - release-preflight = mkRepoApp { - name = "release-preflight"; - description = "Run release coverage refresh and preflight validation"; - runtimeInputs = [ - pkgs.nix - ]; - command = coverageShellExec common.releasePreflightCommand; - env = common.exportCoverageEnv; - pathPrefix = coveragePath; - }; - -} diff --git a/nix/checks.nix b/nix/checks.nix @@ -1,47 +0,0 @@ -{ common, pkgs }: -let - cargoFmt = common.craneLib.cargoFmt common.commonCraneArgs; - cargoCheck = common.craneLib.mkCargoDerivation ( - common.commonCraneArgs - // { - inherit (common) cargoArtifacts; - pname = "radroots-cargo-check"; - doCheck = false; - buildPhaseCargoCommand = '' - cargo check ${common.sdkContractCargoArgs} - ''; - installPhaseCommand = "mkdir -p $out"; - } - ); - cargoTest = common.craneLib.mkCargoDerivation ( - common.commonCraneArgs - // { - inherit (common) cargoArtifacts; - pname = "radroots-cargo-test"; - doCheck = false; - buildPhaseCargoCommand = '' - cargo test ${common.sdkContractCargoArgs} - ''; - installPhaseCommand = "mkdir -p $out"; - } - ); -in -{ - cargo-fmt = cargoFmt; - cargo-check = cargoCheck; - cargo-test = cargoTest; - - guards = common.mkRepoCheck { - name = "repo-guards"; - runtimeInputs = [ - pkgs.coreutils - pkgs.gitMinimal - pkgs.gnugrep - pkgs.ripgrep - ]; - initGit = true; - command = '' - ./scripts/ci/guard_no_legacy_identifiers.sh - ''; - }; -} diff --git a/nix/common.nix b/nix/common.nix @@ -1,317 +0,0 @@ -{ - crane, - lib, - pkgs, - toolchains, -}: -let - root = ../.; - cargoToml = builtins.fromTOML (builtins.readFile ../Cargo.toml); - version = cargoToml.workspace.package.version; - darwinBuildInputs = lib.optionals pkgs.stdenv.isDarwin [ - pkgs.libiconv - ]; - repoSource = lib.sources.cleanSource root; - cargoSource = lib.fileset.toSource { - root = root; - fileset = lib.fileset.intersection (lib.fileset.fromSource repoSource) ( - lib.fileset.unions [ - ../Cargo.toml - ../Cargo.lock - ../README - ../rust-toolchain.toml - ../spec - ../policy - ../crates - ../scripts - ] - ); - }; - baseEnv = { - CARGO_TERM_COLOR = "always"; - LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; - } - // lib.optionalAttrs pkgs.stdenv.isDarwin { - CC = "clang"; - CXX = "clang++"; - SDKROOT = pkgs.apple-sdk_14.sdkroot; - MACOSX_DEPLOYMENT_TARGET = pkgs.stdenv.hostPlatform.darwinMinVersion; - }; - sharedEnv = - baseEnv - // { - PKG_CONFIG_PATH = lib.makeSearchPathOutput "dev" "lib/pkgconfig" stableRuntimeInputs; - } - // lib.optionalAttrs pkgs.stdenv.isDarwin { - LIBRARY_PATH = lib.makeLibraryPath darwinBuildInputs; - }; - coverageEnv = sharedEnv // { - RADROOTS_COVERAGE_CARGO = "${toolchains.coverage}/bin/cargo"; - }; - cargoLlvmCov = pkgs.cargo-llvm-cov.overrideAttrs (old: { - doCheck = false; - meta = old.meta // { - broken = false; - }; - }); - exportEnv = - env: - lib.concatStringsSep "\n" ( - lib.mapAttrsToList (name: value: "export ${name}=${lib.escapeShellArg value}") env - ); - stableRuntimeInputs = - with pkgs; - [ - toolchains.stable - clang - coreutils - curl - findutils - gawk - gitMinimal - gnugrep - gnumake - gnused - jq - libsodium - llvmPackages.llvm - llvmPackages.libclang - pkg-config - python3 - ] - ++ darwinBuildInputs; - coverageRuntimeInputs = stableRuntimeInputs ++ [ - toolchains.coverage - cargoLlvmCov - ]; - releaseRuntimeInputs = coverageRuntimeInputs; - sdkContractCrates = [ - "xtask" - "radroots_core" - "radroots_types" - "radroots_events" - "radroots_trade" - "radroots_identity" - "radroots_replica_db_schema" - "radroots_events_codec" - "radroots_nostr_connect" - "radroots_nostr_signer" - ]; - sdkContractCargoArgs = lib.concatStringsSep " " (map (crate: "-p ${crate}") sdkContractCrates); - craneLib = (crane.mkLib pkgs).overrideToolchain toolchains.stable; - commonCraneArgs = { - inherit version; - pname = "radroots"; - src = cargoSource; - strictDeps = true; - nativeBuildInputs = [ - pkgs.pkg-config - pkgs.clang - pkgs.llvmPackages.libclang - ]; - buildInputs = [ - pkgs.libsodium - ] - ++ darwinBuildInputs; - inherit (sharedEnv) - CARGO_TERM_COLOR - LIBCLANG_PATH - PKG_CONFIG_PATH - ; - }; - cargoArtifacts = craneLib.buildDepsOnly commonCraneArgs; - xtaskPackage = craneLib.buildPackage ( - commonCraneArgs - // { - inherit cargoArtifacts; - pname = "xtask"; - cargoExtraArgs = "-p xtask"; - doCheck = false; - } - ); - initGitRepo = '' - git init -q . - git config user.email "nix-check@example.invalid" - git config user.name "nix check" - git add -A . - ''; - mkRepoCheck = - { - name, - runtimeInputs, - command, - env ? sharedEnv, - initGit ? false, - linuxOnly ? false, - }: - if linuxOnly && !pkgs.stdenv.isLinux then - null - else - pkgs.runCommand name { nativeBuildInputs = runtimeInputs; } '' - export HOME="$TMPDIR/home" - mkdir -p "$HOME" - - cp -R ${repoSource} "$TMPDIR/repo" - chmod -R u+w "$TMPDIR/repo" - cd "$TMPDIR/repo" - export RADROOTS_WORKSPACE_ROOT="$PWD" - - ${exportEnv env} - ${lib.optionalString initGit initGitRepo} - - ${command} - - touch "$out" - ''; - ensureRepoRoot = '' - if [ ! -f Cargo.toml ] || [ ! -f flake.nix ]; then - echo "run this command from the radroots workspace checkout" >&2 - exit 1 - fi - export RADROOTS_WORKSPACE_ROOT="$PWD" - ''; - checkCommand = '' - cargo check --workspace - ''; - contractCommand = '' - ./scripts/ci/guard_no_legacy_identifiers.sh - cargo check -q ${sdkContractCargoArgs} - cargo test -q ${sdkContractCargoArgs} - cargo run -q -p xtask -- sdk validate - ''; - releasePreflightCommand = '' - ./scripts/ci/release_preflight.sh - ''; - coverageReportCommand = '' - rm -rf target/sdk-coverage - mkdir -p target/sdk-coverage - : > target/sdk-coverage/coverage-report-status.txt - - workspace_crates_file="$(mktemp)" - required_crates_file="$(mktemp)" - trap 'rm -f "$workspace_crates_file" "$required_crates_file"' EXIT - - cargo run -q -p xtask -- sdk coverage workspace-crates > "$workspace_crates_file" - while IFS= read -r crate; do - [ -n "''${crate}" ] || continue - safe_crate="''${crate//-/_}" - run_dir="target/sdk-coverage/''${safe_crate}" - mkdir -p "''${run_dir}" - status="ok" - - if ! cargo run -q -p xtask -- sdk coverage run-crate --crate "''${crate}" --out "''${run_dir}"; then - status="run-failed" - fi - - if [ "''${status}" = "ok" ] && ! cargo run -q -p xtask -- sdk coverage report \ - --scope "''${crate}" \ - --summary "''${run_dir}/coverage-summary.json" \ - --lcov "''${run_dir}/coverage-lcov.info" \ - --out "''${run_dir}/coverage-gate-summary.json" \ - --fail-under-exec-lines 0 \ - --fail-under-functions 0 \ - --fail-under-regions 0 \ - --fail-under-branches 0; then - status="report-failed" - fi - - if [ "''${status}" != "ok" ]; then - cat > "''${run_dir}/coverage-gate-summary.json" <<EOF - { - "scope": "''${crate}", - "thresholds": { - "executable_lines": 0, - "functions": 0, - "regions": 0, - "branches": 0, - "branches_required": false - }, - "measured": { - "executable_lines_percent": 0, - "executable_lines_source": "da", - "functions_percent": 0, - "branches_percent": null, - "branches_available": false, - "summary_lines_percent": 0, - "summary_regions_percent": 0 - }, - "counts": { - "executable_lines": { - "covered": 0, - "total": 0 - }, - "branches": { - "covered": 0, - "total": 0 - } - }, - "result": { - "pass": false, - "fail_reasons": [ - "''${status}" - ] - } - } - EOF - fi - - echo "''${crate}:''${status}" >> target/sdk-coverage/coverage-report-status.txt - done < "$workspace_crates_file" - - cargo run -q -p xtask -- sdk coverage required-crates > "$required_crates_file" - while IFS= read -r crate; do - [ -n "''${crate}" ] || continue - safe_crate="''${crate//-/_}" - crate_dir="target/sdk-coverage/''${safe_crate}" - crate_status="$(awk -F: -v crate="''${crate}" '$1 == crate { status = $2 } END { print status }' target/sdk-coverage/coverage-report-status.txt)" - - if [ ! -f "''${crate_dir}/coverage-summary.json" ] || [ ! -f "''${crate_dir}/coverage-lcov.info" ]; then - fail_reason="missing-coverage-artifacts" - if [ -n "''${crate_status}" ] && [ "''${crate_status}" != "ok" ]; then - fail_reason="''${crate_status}" - fi - - cargo run -q -p xtask -- sdk coverage report-missing \ - --scope "''${crate}" \ - --out "''${crate_dir}/coverage-gate-blocking.json" \ - --reason "''${fail_reason}" - continue - fi - - cargo run -q -p xtask -- sdk coverage report \ - --scope "''${crate}" \ - --summary "''${crate_dir}/coverage-summary.json" \ - --lcov "''${crate_dir}/coverage-lcov.info" \ - --out "''${crate_dir}/coverage-gate-blocking.json" \ - --policy-gate - done < "$required_crates_file" - ''; -in -{ - inherit - cargoLlvmCov - cargoArtifacts - checkCommand - commonCraneArgs - contractCommand - coverageEnv - coverageReportCommand - craneLib - ensureRepoRoot - mkRepoCheck - releasePreflightCommand - sdkContractCargoArgs - sharedEnv - version - xtaskPackage - ; - - exportCoverageEnv = exportEnv coverageEnv; - exportSharedEnv = exportEnv sharedEnv; - - runtimeInputs = { - stable = stableRuntimeInputs; - coverage = coverageRuntimeInputs; - release = releaseRuntimeInputs; - }; -} diff --git a/nix/devshells.nix b/nix/devshells.nix @@ -1,33 +0,0 @@ -{ - common, - pkgs, - toolchains, -}: -let - defaultHook = '' - ${common.exportSharedEnv} - export PATH=${toolchains.stable}/bin:$PATH - ''; - coverageHook = '' - ${common.exportCoverageEnv} - export PATH=${toolchains.stable}/bin:${toolchains.coverage}/bin:$PATH - ''; -in -{ - default = pkgs.mkShell { - packages = common.runtimeInputs.wasm ++ [ - common.cargoLlvmCov - ]; - shellHook = defaultHook; - }; - - coverage = pkgs.mkShell { - packages = common.runtimeInputs.release; - shellHook = coverageHook; - }; - - release = pkgs.mkShell { - packages = common.runtimeInputs.release; - shellHook = coverageHook; - }; -} diff --git a/nix/toolchains.nix b/nix/toolchains.nix @@ -1,20 +0,0 @@ -{ pkgs }: -let - toolchain = builtins.fromTOML (builtins.readFile ../rust-toolchain.toml); - stableVersion = toolchain.toolchain.channel; - stableTargets = toolchain.toolchain.targets or [ ]; - stableExtensions = [ - "clippy" - "rust-analyzer" - "rust-src" - "rustfmt" - ]; -in -{ - stable = pkgs.rust-bin.stable.${stableVersion}.default.override { - extensions = stableExtensions; - targets = stableTargets; - }; - - coverage = pkgs.rust-bin.fromRustupToolchainFile ../rust-toolchain-coverage.toml; -} diff --git a/scripts/ci/guard_no_legacy_identifiers.sh b/scripts/ci/guard_no_legacy_identifiers.sh @@ -1,68 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -scan_forbidden() { - local label="$1" - local pattern="$2" - shift 2 - - local matches - matches="$( - rg -nI \ - --glob '!AGENTS.md' \ - --glob '!scripts/ci/guard_no_legacy_identifiers.sh' \ - --glob '!crates/xtask/src/phase1_1.rs' \ - -- "$pattern" "$@" || - true - )" - - if [[ -n $matches ]]; then - echo "$label is forbidden in oss source files" - echo "$matches" - exit 1 - fi -} - -scan_raw_commercial_identifier_fields() { - local files - files="$(rg --files crates/events/src crates/events_codec/src crates/trade/src -g '*.rs')" - - local matches - matches="$( - awk ' - /^pub struct / { - struct_name = $3 - sub(/\{.*/, "", struct_name) - sub(/<.*/, "", struct_name) - } - /^}/ { struct_name = "" } - /^[[:space:]]*pub (order_id|listing_addr|revision_id|quote_id|primary_bin_id|bin_id|economics_digest): String,/ { - if (struct_name != "RadrootsOrderEnvelope" && struct_name != "BinDraft" && struct_name !~ /Projection|Accounting|Availability|Reservation|Issue|NormalizedInventoryCount|TradeListing|ValidationReceiptTags/) { - print FILENAME ":" FNR ":" $0 - } - } - ' $files || true - )" - - if [[ -n $matches ]]; then - echo "raw commercial protocol identifier String fields are forbidden in active payload structs" - echo "$matches" - exit 1 - fi -} - -scan_forbidden "legacy identifier 'tangle'" "tangle" . - -scan_forbidden \ - "legacy broad trade event identifier" \ - "RadrootsTradeMessageType|RadrootsTradeEnvelope|RadrootsTradeMessagePayload|RadrootsTradeQuestion|RadrootsTradeAnswer|RadrootsTradeDiscount|RadrootsTradeOrder|RadrootsActiveOrder|RadrootsActiveTrade|RadrootsTradeListingParseError|RadrootsTradeDomain|TradeListingParseError|TradeListingEnvelope|TradeListingMessage|KIND_TRADE_ORDER|TRADE_LISTING_KINDS|build_envelope_draft|parse_envelope|public_trade|events::trade::|events_codec::trade::|radroots_sdk::trade::|trade_order_economics_digest|trade_revision|trade_lifecycle|reduce_active_order|canonicalize_active_order|active_trade_|ActiveOrder|active_order|active order|active trade|RADROOTS_TRADE_(LISTING_DOMAIN|ENVELOPE_VERSION)" \ - crates spec scripts - -scan_forbidden \ - "legacy broad trade listing kind constant" \ - "KIND_TRADE_LISTING_(ORDER|QUESTION|ANSWER|DISCOUNT|CANCEL|FULFILLMENT|RECEIPT)" \ - crates spec scripts - -scan_raw_commercial_identifier_fields - -echo "no legacy identifiers found in oss source files" diff --git a/scripts/ci/release_preflight.sh b/scripts/ci/release_preflight.sh @@ -1,41 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -root_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -cd "$root_dir" - -cargo check -q -# Keep synthetic xtask test workspaces isolated from any root release-policy env -# that may be set by an outer monorepo preflight wrapper. -env -u RADROOTS_MOUNTED_RUST_CRATE_PUBLISH_POLICY cargo test -q -p xtask -cargo run -q -p xtask -- sdk validate - -required_file="$(mktemp)" -trap 'rm -f "$required_file"' EXIT -cargo run -q -p xtask -- sdk coverage required-crates >"$required_file" - -rm -rf target/coverage -mkdir -p target/coverage - -while IFS= read -r crate; do - [ -n "$crate" ] || continue - safe_crate="${crate//-/_}" - out_dir="target/coverage/${safe_crate}" - mkdir -p "$out_dir" - - cargo run -q -p xtask -- sdk coverage run-crate --crate "$crate" --out "$out_dir" - cargo run -q -p xtask -- sdk coverage report \ - --scope "${crate}" \ - --summary "${out_dir}/coverage-summary.json" \ - --lcov "${out_dir}/coverage-lcov.info" \ - --out "${out_dir}/gate-report.json" \ - --policy-gate -done <"$required_file" - -cargo run -q -p xtask -- sdk coverage refresh-summary \ - --reports-root target/coverage \ - --out target/coverage/coverage-refresh.tsv \ - --status-out target/coverage/coverage-refresh-status.tsv - -cargo run -q -p xtask -- sdk release preflight -echo "release preflight complete" diff --git a/tools/xtask/Cargo.toml b/tools/xtask/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "xtask" +version = "0.1.0-alpha.2" +edition.workspace = true +rust-version.workspace = true +description = "Internal workspace automation tasks" +readme = "README" +license.workspace = true +publish = false +authors = ["Tyson Lupul <tyson@radroots.org>"] + +[dependencies] +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +sha2 = { workspace = true } +hex = { workspace = true } +toml = { workspace = true } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage_nightly)'] } diff --git a/tools/xtask/README b/tools/xtask/README @@ -0,0 +1,24 @@ +# xtask + +This is the README for `xtask`, which provides internal workspace automation +tasks for the `radroots` core libraries. + +## Overview + + * an internal `cargo xtask` runner for workspace-oriented development and + release workflows; + * `contract`, `coverage`, `release`, and `hygiene` command families for + core-library governance; + * command-dispatch code used for contract, coverage, hygiene, and release + paths inside the workspace; + * a non-published binary crate used as tooling rather than as a library + dependency. + +## Copyright + +Except as otherwise noted, all files in the `xtask` distribution are + + Copyright (c) 2020-2026 Tyson Lupul + +For information on usage and redistribution, and for a DISCLAIMER OF ALL +WARRANTIES, see LICENSE included in the `xtask` distribution. diff --git a/crates/xtask/src/contract.rs b/tools/xtask/src/contract.rs diff --git a/tools/xtask/src/coverage.rs b/tools/xtask/src/coverage.rs @@ -0,0 +1,4851 @@ +#![forbid(unsafe_code)] + +use std::ffi::OsString; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; +use std::{collections::BTreeMap, collections::BTreeSet, io::Write}; + +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Clone)] +pub struct CoverageSummary { + pub functions_percent: f64, + pub summary_lines_percent: f64, + pub summary_regions_percent: f64, +} + +#[derive(Debug, Clone, Copy)] +struct DetailedCoverageSummary { + functions_percent: f64, + regions_percent: f64, +} + +#[derive(Debug, Clone, Copy)] +pub enum ExecutableSource { + Da, + LfLh, +} + +#[derive(Debug, Clone)] +pub struct LcovCoverage { + pub executable_total: u64, + pub executable_covered: u64, + pub executable_percent: f64, + pub executable_source: ExecutableSource, + pub branch_total: u64, + pub branch_covered: u64, + pub branches_available: bool, + pub branch_percent: Option<f64>, +} + +#[derive(Debug, Clone, Copy)] +pub struct CoverageThresholds { + pub fail_under_exec_lines: f64, + pub fail_under_functions: f64, + pub fail_under_regions: f64, + pub fail_under_branches: f64, + pub require_branches: bool, +} + +#[derive(Debug, Clone)] +pub struct CoverageGateResult { + pub pass: bool, + pub fail_reasons: Vec<String>, +} + +#[derive(Debug, Serialize, Deserialize)] +struct CoverageGateReport { + scope: String, + thresholds: CoverageGateReportThresholds, + measured: CoverageGateReportMeasured, + counts: CoverageGateReportCounts, + result: CoverageGateReportResult, +} + +#[derive(Debug, Serialize, Deserialize)] +struct CoverageGateReportThresholds { + executable_lines: f64, + functions: f64, + regions: f64, + branches: f64, + branches_required: bool, +} + +#[derive(Debug, Serialize, Deserialize)] +struct CoverageGateReportMeasured { + executable_lines_percent: f64, + executable_lines_source: String, + functions_percent: f64, + branches_percent: Option<f64>, + branches_available: bool, + summary_lines_percent: f64, + summary_regions_percent: f64, +} + +#[derive(Debug, Serialize, Deserialize)] +struct CoverageGateReportCounts { + executable_lines: CoverageCount, + branches: CoverageCount, +} + +#[derive(Debug, Serialize, Deserialize)] +struct CoverageCount { + covered: u64, + total: u64, +} + +#[derive(Debug, Serialize, Deserialize)] +struct CoverageGateReportResult { + pass: bool, + fail_reasons: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovSummaryRoot { + data: Vec<LlvmCovSummaryData>, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovSummaryData { + totals: LlvmCovSummaryTotals, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovSummaryTotals { + functions: LlvmCovSummaryMetric, + lines: LlvmCovSummaryMetric, + regions: LlvmCovSummaryMetric, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovSummaryMetric { + percent: f64, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovDetailsRoot { + data: Vec<LlvmCovDetailsData>, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovDetailsData { + #[serde(default)] + functions: Vec<LlvmCovFunction>, +} + +#[derive(Debug, Deserialize)] +struct LlvmCovFunction { + count: u64, + #[serde(default)] + filenames: Vec<String>, + #[serde(default)] + regions: Vec<[u64; 8]>, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct FunctionCoverageKey { + filenames: Vec<String>, + regions: Vec<RegionCoverageKey>, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct RegionCoverageKey { + line_start: u64, + column_start: u64, + line_end: u64, + column_end: u64, + kind: u64, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CoveragePolicyFile { + gate: CoveragePolicyGate, + required: CoverageRequiredList, + #[serde(default)] + overrides: BTreeMap<String, CoveragePolicyOverride>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CoveragePolicyGate { + fail_under_exec_lines: f64, + fail_under_functions: f64, + fail_under_regions: f64, + fail_under_branches: f64, + require_branches: bool, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CoverageRequiredList { + crates: Vec<String>, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct CoveragePolicyOverride { + fail_under_exec_lines: Option<f64>, + fail_under_functions: Option<f64>, + fail_under_regions: Option<f64>, + fail_under_branches: Option<f64>, + require_branches: Option<bool>, + temporary: bool, + reason: String, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceManifest { + workspace: WorkspaceMembers, +} + +#[derive(Debug, Deserialize)] +struct WorkspaceMembers { + members: Vec<String>, +} + +#[derive(Debug, Deserialize)] +struct PackageManifest { + package: PackageSection, +} + +#[derive(Debug, Deserialize)] +struct PackageSection { + name: String, +} + +#[derive(Debug, Deserialize, Default)] +struct CoverageProfilesFile { + #[serde(default)] + profiles: CoverageProfilesSection, +} + +#[derive(Debug, Deserialize, Default)] +struct CoverageProfilesSection { + #[serde(default)] + default: CoverageProfileRaw, + #[serde(default)] + crates: BTreeMap<String, CoverageProfileRaw>, +} + +#[derive(Debug, Deserialize, Default, Clone)] +struct CoverageProfileRaw { + no_default_features: Option<bool>, + features: Option<Vec<String>>, + test_threads: Option<u32>, +} + +#[derive(Debug, Clone)] +struct CoverageProfile { + no_default_features: bool, + features: Vec<String>, + test_threads: Option<u32>, +} + +#[cfg_attr(not(test), allow(dead_code))] +pub fn read_summary(path: &Path) -> Result<CoverageSummary, String> { + read_summary_for_scope(path, None) +} + +fn read_summary_for_scope(path: &Path, scope: Option<&str>) -> Result<CoverageSummary, String> { + let raw = match fs::read_to_string(path) { + Ok(raw) => raw, + Err(err) => return Err(format!("failed to read summary {}: {err}", path.display())), + }; + let parsed: LlvmCovSummaryRoot = match serde_json::from_str(&raw) { + Ok(parsed) => parsed, + Err(err) => return Err(format!("failed to parse summary {}: {err}", path.display())), + }; + let totals = match parsed.data.first() { + Some(entry) => &entry.totals, + None => return Err(format!("summary data is empty in {}", path.display())), + }; + + let mut summary = CoverageSummary { + functions_percent: totals.functions.percent, + summary_lines_percent: totals.lines.percent, + summary_regions_percent: totals.regions.percent, + }; + + let details_path = coverage_details_path(path); + if details_path.exists() { + let normalized = read_detailed_summary(&details_path, scope)?; + if (summary.functions_percent - 100.0).abs() < f64::EPSILON { + summary.summary_regions_percent = normalized.regions_percent; + } + } + + Ok(summary) +} + +fn coverage_details_path(summary_path: &Path) -> PathBuf { + summary_path + .parent() + .unwrap_or_else(|| Path::new(".")) + .join("coverage-details.json") +} + +fn read_detailed_summary( + path: &Path, + scope: Option<&str>, +) -> Result<DetailedCoverageSummary, String> { + let raw = match fs::read_to_string(path) { + Ok(raw) => raw, + Err(err) => { + return Err(format!( + "failed to read coverage details {}: {err}", + path.display() + )); + } + }; + let parsed: LlvmCovDetailsRoot = match serde_json::from_str(&raw) { + Ok(parsed) => parsed, + Err(err) => { + return Err(format!( + "failed to parse coverage details {}: {err}", + path.display() + )); + } + }; + let Some(entry) = parsed.data.first() else { + return Err(format!( + "coverage details data is empty in {}", + path.display() + )); + }; + + let mut functions_by_key: BTreeMap<FunctionCoverageKey, Vec<&LlvmCovFunction>> = + BTreeMap::new(); + for function in &entry.functions { + if function.filenames.is_empty() || function.regions.is_empty() { + continue; + } + let key = FunctionCoverageKey { + filenames: function.filenames.clone(), + regions: function + .regions + .iter() + .map(|region| RegionCoverageKey { + line_start: region[0], + column_start: region[1], + line_end: region[2], + column_end: region[3], + kind: region[7], + }) + .collect(), + }; + functions_by_key.entry(key).or_default().push(function); + } + + if functions_by_key.is_empty() { + return Err(format!( + "coverage details functions are empty in {}", + path.display() + )); + } + + let mut regions_total = 0_u64; + let mut regions_covered = 0_u64; + let mut functions_total = 0_u64; + let mut functions_covered = 0_u64; + let mut source_cache: BTreeMap<String, Option<String>> = BTreeMap::new(); + let scope_filter = scope.map(scope_path_fragment); + for variants in functions_by_key.values() { + if let Some(scope_filter) = scope_filter.as_deref() + && !variants.iter().any(|function| { + function + .filenames + .iter() + .any(|filename| filename.contains(scope_filter)) + }) + { + continue; + } + functions_total = functions_total.saturating_add(1); + if variants.iter().any(|function| function.count > 0) { + functions_covered = functions_covered.saturating_add(1); + } + let mut group_regions: BTreeMap<RegionCoverageKey, bool> = BTreeMap::new(); + for function in variants { + for region in &function.regions { + let key = RegionCoverageKey { + line_start: region[0], + column_start: region[1], + line_end: region[2], + column_end: region[3], + kind: region[7], + }; + let covered = region[4] > 0; + group_regions + .entry(key) + .and_modify(|existing| *existing |= covered) + .or_insert(covered); + } + } + let primary_filename = variants + .first() + .and_then(|function| function.filenames.first()) + .map(String::as_str); + for (region, covered) in group_regions { + if !covered + && primary_filename.is_some_and(|filename| { + is_ignorable_synthetic_region(filename, &region, &mut source_cache) + }) + { + continue; + } + regions_total = regions_total.saturating_add(1); + if covered { + regions_covered = regions_covered.saturating_add(1); + } + } + } + + Ok(DetailedCoverageSummary { + functions_percent: percentage(functions_covered, functions_total), + regions_percent: percentage(regions_covered, regions_total), + }) +} + +fn scope_path_fragment(scope: &str) -> String { + let crate_dir = scope.strip_prefix("radroots_").unwrap_or(scope); + format!("/crates/{crate_dir}/src/") +} + +fn percentage(covered: u64, total: u64) -> f64 { + if total == 0 { + 100.0 + } else { + (covered as f64 / total as f64) * 100.0 + } +} + +fn is_ignorable_synthetic_region( + filename: &str, + region: &RegionCoverageKey, + source_cache: &mut BTreeMap<String, Option<String>>, +) -> bool { + if region.line_start != region.line_end { + return false; + } + let source = source_cache + .entry(filename.to_string()) + .or_insert_with(|| fs::read_to_string(filename).ok()); + let Some(source) = source.as_ref() else { + return false; + }; + let Some(line) = source + .lines() + .nth(region.line_start.saturating_sub(1) as usize) + else { + return false; + }; + let start = region.column_start.saturating_sub(1) as usize; + let end = region.column_end.saturating_sub(1) as usize; + let slice = line.get(start..end); + if region.column_end == region.column_start + 1 && slice == Some("?") { + return true; + } + + filename.ends_with("/tests.rs") + && line.contains("panic!(\"unexpected") + && matches!(slice, Some("other") | Some("panic!")) +} + +impl CoveragePolicyFile { + pub(crate) fn thresholds(&self) -> CoverageThresholds { + CoverageThresholds { + fail_under_exec_lines: self.gate.fail_under_exec_lines, + fail_under_functions: self.gate.fail_under_functions, + fail_under_regions: self.gate.fail_under_regions, + fail_under_branches: self.gate.fail_under_branches, + require_branches: self.gate.require_branches, + } + } + + pub(crate) fn thresholds_for_scope(&self, scope: &str) -> CoverageThresholds { + let base = self.thresholds(); + let Some(override_policy) = self.overrides.get(scope) else { + return base; + }; + CoverageThresholds { + fail_under_exec_lines: override_policy + .fail_under_exec_lines + .unwrap_or(base.fail_under_exec_lines), + fail_under_functions: override_policy + .fail_under_functions + .unwrap_or(base.fail_under_functions), + fail_under_regions: override_policy + .fail_under_regions + .unwrap_or(base.fail_under_regions), + fail_under_branches: override_policy + .fail_under_branches + .unwrap_or(base.fail_under_branches), + require_branches: override_policy + .require_branches + .unwrap_or(base.require_branches), + } + } + + pub(crate) fn required_crates(&self) -> Result<Vec<String>, String> { + if self.required.crates.is_empty() { + return Err("coverage required crates list must not be empty".to_string()); + } + let mut seen = BTreeSet::new(); + for crate_name in &self.required.crates { + if crate_name.trim().is_empty() { + return Err( + "coverage required crates list includes an empty crate name".to_string() + ); + } + if !seen.insert(crate_name.clone()) { + return Err(format!( + "coverage required crates list includes duplicate crate {crate_name}" + )); + } + } + Ok(self.required.crates.clone()) + } + + fn validate_overrides(&self) -> Result<(), String> { + let required_crates = self.required_crates()?; + let required_set: BTreeSet<_> = required_crates.into_iter().collect(); + let base = self.thresholds(); + for (crate_name, override_policy) in &self.overrides { + if !required_set.contains(crate_name) { + return Err(format!( + "coverage override {crate_name} must target a required crate" + )); + } + if !override_policy.temporary { + return Err(format!( + "coverage override {crate_name} must set temporary = true" + )); + } + if override_policy.reason.trim().is_empty() { + return Err(format!( + "coverage override {crate_name} must include a non-empty reason" + )); + } + validate_override_threshold( + crate_name, + "fail_under_exec_lines", + override_policy.fail_under_exec_lines, + base.fail_under_exec_lines, + )?; + validate_override_threshold( + crate_name, + "fail_under_functions", + override_policy.fail_under_functions, + base.fail_under_functions, + )?; + validate_override_threshold( + crate_name, + "fail_under_regions", + override_policy.fail_under_regions, + base.fail_under_regions, + )?; + validate_override_threshold( + crate_name, + "fail_under_branches", + override_policy.fail_under_branches, + base.fail_under_branches, + )?; + if override_policy.require_branches == Some(true) && !base.require_branches { + return Err(format!( + "coverage override {crate_name} require_branches cannot be stricter than the global gate" + )); + } + } + Ok(()) + } + + pub(crate) fn required_crate_entries(&self) -> &[String] { + &self.required.crates + } +} + +fn validate_override_threshold( + crate_name: &str, + label: &str, + value: Option<f64>, + global: f64, +) -> Result<(), String> { + let Some(value) = value else { + return Ok(()); + }; + if !value.is_finite() { + return Err(format!( + "coverage override {crate_name} {label} must be finite" + )); + } + if !(0.0..=100.0).contains(&value) { + return Err(format!( + "coverage override {crate_name} {label} must be within 0..=100" + )); + } + if value > global { + return Err(format!( + "coverage override {crate_name} {label} must not exceed the global gate" + )); + } + Ok(()) +} + +pub(crate) fn coverage_policy_path(root: &Path) -> PathBuf { + root.join("contracts").join("coverage.toml") +} + +pub(crate) fn read_coverage_policy(path: &Path) -> Result<CoveragePolicyFile, String> { + let raw = match fs::read_to_string(path) { + Ok(raw) => raw, + Err(err) => { + return Err(format!( + "failed to read coverage policy {}: {err}", + path.display() + )); + } + }; + let parsed: CoveragePolicyFile = match toml::from_str(&raw) { + Ok(parsed) => parsed, + Err(err) => { + return Err(format!( + "failed to parse coverage policy {}: {err}", + path.display() + )); + } + }; + let thresholds = parsed.thresholds(); + for (label, value) in [ + ("fail_under_exec_lines", thresholds.fail_under_exec_lines), + ("fail_under_functions", thresholds.fail_under_functions), + ("fail_under_regions", thresholds.fail_under_regions), + ("fail_under_branches", thresholds.fail_under_branches), + ] { + if !value.is_finite() { + return Err(format!("coverage policy {label} must be finite")); + } + if !(0.0..=100.0).contains(&value) { + return Err(format!("coverage policy {label} must be within 0..=100")); + } + } + parsed.required_crates()?; + parsed.validate_overrides()?; + Ok(parsed) +} + +fn read_required_crates(path: &Path) -> Result<Vec<String>, String> { + read_coverage_policy(path)?.required_crates() +} + +fn read_workspace_crates(workspace_root: &Path) -> Result<Vec<String>, String> { + let packages = read_workspace_packages(workspace_root)?; + Ok(packages.into_iter().map(|(name, _)| name).collect()) +} + +fn read_workspace_packages(workspace_root: &Path) -> Result<Vec<(String, PathBuf)>, String> { + let workspace_manifest = parse_toml::<WorkspaceManifest>(&workspace_root.join("Cargo.toml"))?; + if workspace_manifest.workspace.members.is_empty() { + return Err("workspace members list must not be empty".to_string()); + } + let mut packages = Vec::with_capacity(workspace_manifest.workspace.members.len()); + let mut seen = BTreeSet::new(); + for member in workspace_manifest.workspace.members { + let package_manifest = + parse_toml::<PackageManifest>(&workspace_root.join(&member).join("Cargo.toml"))?; + let package_name = package_manifest.package.name; + if package_name.trim().is_empty() { + return Err("workspace includes an empty package name".to_string()); + } + if !seen.insert(package_name.clone()) { + return Err(format!( + "workspace includes duplicate package name {}", + package_name + )); + } + packages.push((package_name, PathBuf::from(member))); + } + Ok(packages) +} + +fn parse_toml<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> { + let raw = match fs::read_to_string(path) { + Ok(raw) => raw, + Err(err) => return Err(format!("failed to read {}: {err}", path.display())), + }; + match toml::from_str::<T>(&raw) { + Ok(parsed) => Ok(parsed), + Err(err) => Err(format!("failed to parse {}: {err}", path.display())), + } +} + +fn merge_coverage_profile( + base: CoverageProfileRaw, + overlay: CoverageProfileRaw, +) -> CoverageProfile { + CoverageProfile { + no_default_features: overlay + .no_default_features + .unwrap_or(base.no_default_features.unwrap_or(false)), + features: overlay + .features + .unwrap_or_else(|| base.features.unwrap_or_default()), + test_threads: overlay.test_threads.or(base.test_threads), + } +} + +fn read_coverage_profile( + workspace_root: &Path, + crate_name: &str, +) -> Result<CoverageProfile, String> { + let path = workspace_root + .join("contracts") + .join("coverage-profiles.toml"); + if !path.exists() { + return Ok(CoverageProfile { + no_default_features: false, + features: Vec::new(), + test_threads: None, + }); + } + let parsed = parse_toml::<CoverageProfilesFile>(&path)?; + let base = parsed.profiles.default; + let overlay = parsed + .profiles + .crates + .get(crate_name) + .cloned() + .unwrap_or_default(); + let resolved = merge_coverage_profile(base, overlay); + if resolved + .features + .iter() + .any(|feature| feature.trim().is_empty()) + { + return Err(format!( + "coverage profile for {crate_name} includes an empty feature value" + )); + } + if resolved.test_threads == Some(0) { + return Err(format!( + "coverage profile for {crate_name} must set test_threads > 0" + )); + } + Ok(resolved) +} + +pub fn read_lcov(path: &Path) -> Result<LcovCoverage, String> { + let raw = match fs::read_to_string(path) { + Ok(raw) => raw, + Err(err) => return Err(format!("failed to read lcov {}: {err}", path.display())), + }; + + let mut da_total: u64 = 0; + let mut da_covered: u64 = 0; + let mut executable_total: u64 = 0; + let mut executable_covered: u64 = 0; + let mut branch_total_lcov: u64 = 0; + let mut branch_covered_lcov: u64 = 0; + let mut branch_total_brda: u64 = 0; + let mut branch_covered_brda: u64 = 0; + + for line in raw.lines() { + if let Some(value) = line.strip_prefix("DA:") { + let Some((_, hit)) = value.split_once(',') else { + return Err(format!("invalid DA record in {}", path.display())); + }; + let hit_count: u64 = match hit.parse() { + Ok(hit_count) => hit_count, + Err(err) => { + return Err(format!( + "invalid DA hit count `{hit}` in {}: {err}", + path.display() + )); + } + }; + da_total = da_total.saturating_add(1); + if hit_count > 0 { + da_covered = da_covered.saturating_add(1); + } + continue; + } + if let Some(value) = line.strip_prefix("LF:") { + let parsed: u64 = match value.parse() { + Ok(parsed) => parsed, + Err(err) => { + return Err(format!( + "invalid LF value `{value}` in {}: {err}", + path.display() + )); + } + }; + executable_total = executable_total.saturating_add(parsed); + continue; + } + if let Some(value) = line.strip_prefix("LH:") { + let parsed: u64 = match value.parse() { + Ok(parsed) => parsed, + Err(err) => { + return Err(format!( + "invalid LH value `{value}` in {}: {err}", + path.display() + )); + } + }; + executable_covered = executable_covered.saturating_add(parsed); + continue; + } + if let Some(value) = line.strip_prefix("BRF:") { + let parsed: u64 = match value.parse() { + Ok(parsed) => parsed, + Err(err) => { + return Err(format!( + "invalid BRF value `{value}` in {}: {err}", + path.display() + )); + } + }; + branch_total_lcov = branch_total_lcov.saturating_add(parsed); + continue; + } + if let Some(value) = line.strip_prefix("BRH:") { + let parsed: u64 = match value.parse() { + Ok(parsed) => parsed, + Err(err) => { + return Err(format!( + "invalid BRH value `{value}` in {}: {err}", + path.display() + )); + } + }; + branch_covered_lcov = branch_covered_lcov.saturating_add(parsed); + continue; + } + if let Some(value) = line.strip_prefix("BRDA:") { + let fields = value.split(',').collect::<Vec<_>>(); + if fields.len() != 4 { + return Err(format!("invalid BRDA record in {}", path.display())); + } + let taken = fields[3]; + if taken == "-" { + continue; + } + let hit_count: u64 = match taken.parse() { + Ok(hit_count) => hit_count, + Err(err) => { + return Err(format!( + "invalid BRDA taken count `{taken}` in {}: {err}", + path.display() + )); + } + }; + branch_total_brda = branch_total_brda.saturating_add(1); + if hit_count > 0 { + branch_covered_brda = branch_covered_brda.saturating_add(1); + } + } + } + + let mut executable_source = ExecutableSource::Da; + let mut executable_percent = 100.0_f64; + + if da_total > 0 { + executable_total = da_total; + executable_covered = da_covered; + executable_percent = (da_covered as f64 / da_total as f64) * 100.0_f64; + } else if executable_total > 0 { + executable_source = ExecutableSource::LfLh; + executable_percent = (executable_covered as f64 / executable_total as f64) * 100.0_f64; + } + + let (branch_total, branch_covered) = if branch_total_brda > 0 { + (branch_total_brda, branch_covered_brda) + } else { + (branch_total_lcov, branch_covered_lcov) + }; + let branches_available = branch_total > 0; + let branch_percent = if branches_available { + Some((branch_covered as f64 / branch_total as f64) * 100.0_f64) + } else { + None + }; + + Ok(LcovCoverage { + executable_total, + executable_covered, + executable_percent, + executable_source, + branch_total, + branch_covered, + branches_available, + branch_percent, + }) +} + +pub fn evaluate_gate( + summary: &CoverageSummary, + lcov: &LcovCoverage, + thresholds: CoverageThresholds, +) -> CoverageGateResult { + let exec_ok = lcov.executable_percent >= thresholds.fail_under_exec_lines; + let functions_ok = summary.functions_percent >= thresholds.fail_under_functions; + let regions_ok = summary.summary_regions_percent >= thresholds.fail_under_regions; + let branch_presence_ok = !thresholds.require_branches || lcov.branches_available; + let branch_ok = lcov + .branch_percent + .is_none_or(|branch_percent| branch_percent >= thresholds.fail_under_branches); + + let pass = [ + exec_ok, + functions_ok, + regions_ok, + branch_presence_ok, + branch_ok, + ] + .into_iter() + .all(|flag| flag); + let mut fail_reasons: Vec<String> = Vec::new(); + + if !exec_ok { + fail_reasons.push(format!( + "executable_lines={:.6} < {:.6}", + lcov.executable_percent, thresholds.fail_under_exec_lines + )); + } + + if !functions_ok { + fail_reasons.push(format!( + "functions={:.6} < {:.6}", + summary.functions_percent, thresholds.fail_under_functions + )); + } + + if !regions_ok { + fail_reasons.push(format!( + "regions={:.6} < {:.6}", + summary.summary_regions_percent, thresholds.fail_under_regions + )); + } + + if thresholds.require_branches && !lcov.branches_available { + fail_reasons.push("branches=unavailable".to_string()); + } + + if lcov.branches_available && !branch_ok { + fail_reasons.push(format!( + "branches={:.6} < {:.6}", + lcov.branch_percent.unwrap_or(0.0), + thresholds.fail_under_branches + )); + } + + CoverageGateResult { pass, fail_reasons } +} + +fn executable_source_label(source: ExecutableSource) -> &'static str { + match source { + ExecutableSource::Da => "da", + ExecutableSource::LfLh => "lf_lh", + } +} + +fn parse_string_arg(args: &[String], name: &str) -> Result<String, String> { + let flag = format!("--{name}"); + let mut index = 0usize; + while index < args.len() { + if args[index] == flag { + let Some(value) = args.get(index + 1) else { + return Err(format!("missing value for --{name}")); + }; + return Ok(value.clone()); + } + index += 1; + } + Err(format!("missing --{name}")) +} + +fn parse_optional_string_arg(args: &[String], name: &str) -> Option<String> { + let flag = format!("--{name}"); + let mut index = 0usize; + while index < args.len() { + if args[index] == flag { + return args.get(index + 1).cloned(); + } + index += 1; + } + None +} + +fn parse_optional_f64_arg(args: &[String], name: &str) -> Result<Option<f64>, String> { + if let Some(raw) = parse_optional_string_arg(args, name) { + let parsed = raw + .parse::<f64>() + .map_err(|err| format!("invalid --{name} value `{raw}`: {err}"))?; + if !parsed.is_finite() { + return Err(format!("invalid --{name} value `{raw}`: must be finite")); + } + return Ok(Some(parsed)); + } + Ok(None) +} + +#[cfg_attr(not(test), allow(dead_code))] +fn parse_f64_arg(args: &[String], name: &str, default: f64) -> Result<f64, String> { + if let Some(raw) = parse_optional_string_arg(args, name) { + return raw + .parse::<f64>() + .map_err(|err| format!("invalid --{name} value `{raw}`: {err}")); + } + Ok(default) +} + +fn parse_optional_u32_arg(args: &[String], name: &str) -> Result<Option<u32>, String> { + if let Some(raw) = parse_optional_string_arg(args, name) { + let parsed = raw + .parse::<u32>() + .map_err(|err| format!("invalid --{name} value `{raw}`: {err}"))?; + return Ok(Some(parsed)); + } + Ok(None) +} + +fn parse_bool_flag(args: &[String], name: &str) -> bool { + let flag = format!("--{name}"); + args.iter().any(|arg| arg == &flag) +} + +fn has_flag(args: &[String], name: &str) -> bool { + let flag = format!("--{name}"); + args.iter().any(|arg| arg == &flag) +} + +fn workspace_root_with_override(override_root: Option<&str>) -> PathBuf { + if let Some(raw) = override_root { + let trimmed = raw.trim(); + if !trimmed.is_empty() { + return PathBuf::from(trimmed); + } + } + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let crates_dir = manifest_dir.parent().unwrap_or(manifest_dir); + let root = crates_dir.parent().unwrap_or(crates_dir); + root.to_path_buf() +} + +fn workspace_root() -> PathBuf { + let override_root = std::env::var("RADROOTS_WORKSPACE_ROOT").ok(); + workspace_root_with_override(override_root.as_deref()) +} + +fn run_command(mut command: Command, name: &str) -> Result<(), String> { + let status = match command.status() { + Ok(status) => status, + Err(err) => return Err(format!("failed to run {name}: {err}")), + }; + if !status.success() { + return Err(format!("{name} failed with status {status}")); + } + Ok(()) +} + +fn apply_coverage_profile_flags(command: &mut Command, profile: &CoverageProfile) { + if profile.no_default_features { + command.arg("--no-default-features"); + } + if !profile.features.is_empty() { + command.arg("--features").arg(profile.features.join(",")); + } +} + +fn prepend_toolchain_bin_to_path( + toolchain_bin: &Path, + existing_path: Option<OsString>, +) -> OsString { + match existing_path { + Some(existing) => std::env::join_paths( + std::iter::once(toolchain_bin.to_path_buf()).chain(std::env::split_paths(&existing)), + ) + .expect("joining PATH entries for coverage toolchain should succeed"), + None => OsString::from(toolchain_bin), + } +} + +fn configure_coverage_toolchain_env(command: &mut Command, toolchain_bin: &Path) { + let joined_path = prepend_toolchain_bin_to_path(toolchain_bin, std::env::var_os("PATH")); + command.env("PATH", joined_path); + + for (env_name, binary_name) in [ + ("RUSTC", "rustc"), + ("RUSTDOC", "rustdoc"), + ("LLVM_COV", "llvm-cov"), + ("LLVM_PROFDATA", "llvm-profdata"), + ] { + let binary_path = toolchain_bin.join(binary_name); + if binary_path.exists() { + command.env(env_name, binary_path); + } + } +} + +fn coverage_cargo_command_with_override(override_binary: Option<&str>) -> Command { + if let Some(binary) = override_binary { + let mut cmd = Command::new(binary); + if let Some(toolchain_bin) = Path::new(binary).parent() { + configure_coverage_toolchain_env(&mut cmd, toolchain_bin); + } + return cmd; + } + + let mut cmd = Command::new("rustup"); + cmd.arg("run").arg("nightly").arg("cargo"); + cmd +} + +fn normalized_coverage_cargo_override(raw: Option<String>) -> Option<String> { + raw.map(|raw| raw.trim().to_string()) + .filter(|raw| !raw.is_empty()) +} + +fn coverage_cargo_command() -> Command { + let override_binary = + normalized_coverage_cargo_override(std::env::var("RADROOTS_COVERAGE_CARGO").ok()); + coverage_cargo_command_with_override(override_binary.as_deref()) +} + +fn coverage_llvm_cov_command() -> Command { + let mut cmd = coverage_cargo_command(); + cmd.arg("llvm-cov"); + cmd +} + +const COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX: &str = + r"(/\.cargo/registry/|/lib/rustlib/src/rust/)"; + +fn escape_regex_literal(raw: &str) -> String { + let mut escaped = String::with_capacity(raw.len()); + for ch in raw.chars() { + match ch { + '\\' | '.' | '+' | '*' | '?' | '(' | ')' | '|' | '[' | ']' | '{' | '}' | '^' | '$' => { + escaped.push('\\'); + escaped.push(ch); + } + _ => escaped.push(ch), + } + } + escaped +} + +fn coverage_ignore_filename_regex( + workspace_root: &Path, + crate_name: &str, +) -> Result<String, String> { + let mut patterns = vec![COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX.to_string()]; + let mut found_target = false; + + for (package_name, member_path) in read_workspace_packages(workspace_root)? { + let absolute_member = workspace_root.join(member_path); + if package_name == crate_name { + found_target = true; + patterns.push(format!( + "^{}/", + escape_regex_literal(&absolute_member.join("tests").display().to_string()) + )); + continue; + } + patterns.push(format!( + "^{}/", + escape_regex_literal(&absolute_member.display().to_string()) + )); + } + + if !found_target { + return Err(format!( + "workspace coverage filters could not resolve crate directory for {crate_name}" + )); + } + + Ok(format!("({})", patterns.join("|"))) +} + +fn apply_coverage_report_filters(command: &mut Command, ignore_regex: &str) { + command.arg("--ignore-filename-regex").arg(ignore_regex); +} + +fn run_crate_with_runner_at_root( + args: &[String], + workspace_root: &Path, + runner: &mut dyn FnMut(Command, &str) -> Result<(), String>, +) -> Result<(), String> { + let crate_name = parse_string_arg(args, "crate")?; + let profile = read_coverage_profile(workspace_root, &crate_name)?; + let out_dir = if let Some(raw) = parse_optional_string_arg(args, "out") { + PathBuf::from(raw) + } else { + workspace_root + .join("target") + .join("coverage") + .join(crate_name.replace('-', "_")) + }; + let test_threads = parse_optional_u32_arg(args, "test-threads")? + .or(profile.test_threads) + .unwrap_or(1); + let ignore_regex = coverage_ignore_filename_regex(workspace_root, &crate_name)?; + + if let Err(err) = fs::create_dir_all(&out_dir) { + return Err(format!("failed to create {}: {err}", out_dir.display())); + } + + runner( + { + let mut cmd = coverage_llvm_cov_command(); + cmd.arg("clean") + .arg("--workspace") + .current_dir(workspace_root); + cmd + }, + "cargo llvm-cov clean --workspace", + )?; + + runner( + { + let mut cmd = coverage_llvm_cov_command(); + cmd.arg("-p").arg(&crate_name); + apply_coverage_profile_flags(&mut cmd, &profile); + cmd.arg("--no-report") + .arg("--branch") + .arg("--") + .arg(format!("--test-threads={test_threads}")) + .current_dir(workspace_root); + cmd + }, + "cargo llvm-cov --no-report", + )?; + + let summary_path = out_dir.join("coverage-summary.json"); + runner( + { + let mut cmd = coverage_llvm_cov_command(); + cmd.arg("report").arg("-p").arg(&crate_name); + apply_coverage_report_filters(&mut cmd, &ignore_regex); + cmd.arg("--json") + .arg("--summary-only") + .arg("--branch") + .arg("--output-path") + .arg(&summary_path) + .current_dir(workspace_root); + cmd + }, + "cargo llvm-cov report --json --summary-only", + )?; + + let details_path = out_dir.join("coverage-details.json"); + runner( + { + let mut cmd = coverage_llvm_cov_command(); + cmd.arg("report").arg("-p").arg(&crate_name); + apply_coverage_report_filters(&mut cmd, &ignore_regex); + cmd.arg("--json") + .arg("--branch") + .arg("--output-path") + .arg(&details_path) + .current_dir(workspace_root); + cmd + }, + "cargo llvm-cov report --json", + )?; + + let lcov_path = out_dir.join("coverage-lcov.info"); + runner( + { + let mut cmd = coverage_llvm_cov_command(); + cmd.arg("report").arg("-p").arg(&crate_name); + apply_coverage_report_filters(&mut cmd, &ignore_regex); + cmd.arg("--lcov") + .arg("--branch") + .arg("--output-path") + .arg(&lcov_path) + .current_dir(workspace_root); + cmd + }, + "cargo llvm-cov report --lcov", + )?; + + eprintln!("coverage summary: {}", summary_path.display()); + eprintln!("coverage details: {}", details_path.display()); + eprintln!("coverage lcov: {}", lcov_path.display()); + Ok(()) +} + +fn run_crate_with_runner( + args: &[String], + runner: &mut dyn FnMut(Command, &str) -> Result<(), String>, +) -> Result<(), String> { + let root = workspace_root(); + run_crate_with_runner_at_root(args, &root, runner) +} + +fn run_crate(args: &[String]) -> Result<(), String> { + let mut runner = run_command; + run_crate_with_runner(args, &mut runner) +} + +fn report_gate_with_root(args: &[String], root: &Path) -> Result<(), String> { + let scope = parse_string_arg(args, "scope")?; + let summary_path = PathBuf::from(parse_string_arg(args, "summary")?); + let lcov_path = PathBuf::from(parse_string_arg(args, "lcov")?); + let out_path = PathBuf::from(parse_string_arg(args, "out")?); + let policy_gate = parse_bool_flag(args, "policy-gate"); + let explicit_exec = parse_optional_f64_arg(args, "fail-under-exec-lines")?; + let explicit_functions = parse_optional_f64_arg(args, "fail-under-functions")?; + let explicit_regions = parse_optional_f64_arg(args, "fail-under-regions")?; + let explicit_branches = parse_optional_f64_arg(args, "fail-under-branches")?; + let explicit_require_branches = has_flag(args, "require-branches"); + let any_explicit_threshold = explicit_exec.is_some() + || explicit_functions.is_some() + || explicit_regions.is_some() + || explicit_branches.is_some(); + let thresholds = if policy_gate { + if any_explicit_threshold || explicit_require_branches { + return Err( + "--policy-gate cannot be combined with explicit threshold or branch flags" + .to_string(), + ); + } + let policy = read_coverage_policy(&coverage_policy_path(root))?; + policy.thresholds_for_scope(&scope) + } else { + let Some(fail_under_exec_lines) = explicit_exec else { + return Err( + "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values" + .to_string(), + ); + }; + let Some(fail_under_functions) = explicit_functions else { + return Err( + "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values" + .to_string(), + ); + }; + let Some(fail_under_regions) = explicit_regions else { + return Err( + "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values" + .to_string(), + ); + }; + let Some(fail_under_branches) = explicit_branches else { + return Err( + "missing coverage thresholds; pass --policy-gate or explicit --fail-under-* values" + .to_string(), + ); + }; + CoverageThresholds { + fail_under_exec_lines, + fail_under_functions, + fail_under_regions, + fail_under_branches, + require_branches: explicit_require_branches, + } + }; + + let mut summary = read_summary_for_scope(&summary_path, Some(&scope))?; + let lcov = read_lcov(&lcov_path)?; + normalize_summary_for_gate(&scope, &summary_path, &lcov, &mut summary)?; + let gate = evaluate_gate(&summary, &lcov, thresholds); + + let report = CoverageGateReport { + scope: scope.clone(), + thresholds: CoverageGateReportThresholds { + executable_lines: thresholds.fail_under_exec_lines, + functions: thresholds.fail_under_functions, + regions: thresholds.fail_under_regions, + branches: thresholds.fail_under_branches, + branches_required: thresholds.require_branches, + }, + measured: CoverageGateReportMeasured { + executable_lines_percent: lcov.executable_percent, + executable_lines_source: executable_source_label(lcov.executable_source).to_string(), + functions_percent: summary.functions_percent, + branches_percent: lcov.branch_percent, + branches_available: lcov.branches_available, + summary_lines_percent: summary.summary_lines_percent, + summary_regions_percent: summary.summary_regions_percent, + }, + counts: CoverageGateReportCounts { + executable_lines: CoverageCount { + covered: lcov.executable_covered, + total: lcov.executable_total, + }, + branches: CoverageCount { + covered: lcov.branch_covered, + total: lcov.branch_total, + }, + }, + result: CoverageGateReportResult { + pass: gate.pass, + fail_reasons: gate.fail_reasons.clone(), + }, + }; + write_gate_report(&out_path, &report)?; + + if lcov.branches_available { + eprintln!( + "{} coverage: executable_lines={:.6} functions={:.6} regions={:.6} branches={:.6}", + scope, + lcov.executable_percent, + summary.functions_percent, + summary.summary_regions_percent, + lcov.branch_percent.unwrap_or(0.0) + ); + } else { + eprintln!( + "{} coverage: executable_lines={:.6} functions={:.6} regions={:.6} branches=unavailable", + scope, + lcov.executable_percent, + summary.functions_percent, + summary.summary_regions_percent + ); + } + + eprintln!( + "{} summary (informational): lines={:.6} regions={:.6}", + scope, summary.summary_lines_percent, summary.summary_regions_percent + ); + + if !gate.pass { + for reason in &gate.fail_reasons { + eprintln!("{scope} gate fail: {reason}"); + } + return Err("coverage gate failed".to_string()); + } + + Ok(()) +} + +#[cfg_attr(coverage_nightly, coverage(off))] +fn normalize_summary_for_gate( + scope: &str, + summary_path: &Path, + _lcov: &LcovCoverage, + summary: &mut CoverageSummary, +) -> Result<(), String> { + let details_path = coverage_details_path(summary_path); + if !details_path.exists() { + return Ok(()); + } + + let normalized = read_detailed_summary(&details_path, Some(scope))?; + summary.functions_percent = normalized.functions_percent; + summary.summary_regions_percent = normalized.regions_percent; + Ok(()) +} + +#[cfg_attr(not(test), allow(dead_code))] +fn report_gate(args: &[String]) -> Result<(), String> { + let root = workspace_root(); + report_gate_with_root(args, &root) +} + +fn report_missing_gate_with_root(args: &[String], root: &Path) -> Result<(), String> { + let scope = parse_string_arg(args, "scope")?; + let out_path = PathBuf::from(parse_string_arg(args, "out")?); + let reason = parse_string_arg(args, "reason")?; + let policy = read_coverage_policy(&coverage_policy_path(root))?; + let thresholds = policy.thresholds_for_scope(&scope); + + let report = CoverageGateReport { + scope: scope.clone(), + thresholds: CoverageGateReportThresholds { + executable_lines: thresholds.fail_under_exec_lines, + functions: thresholds.fail_under_functions, + regions: thresholds.fail_under_regions, + branches: thresholds.fail_under_branches, + branches_required: thresholds.require_branches, + }, + measured: CoverageGateReportMeasured { + executable_lines_percent: 0.0, + executable_lines_source: executable_source_label(ExecutableSource::Da).to_string(), + functions_percent: 0.0, + branches_percent: None, + branches_available: false, + summary_lines_percent: 0.0, + summary_regions_percent: 0.0, + }, + counts: CoverageGateReportCounts { + executable_lines: CoverageCount { + covered: 0, + total: 0, + }, + branches: CoverageCount { + covered: 0, + total: 0, + }, + }, + result: CoverageGateReportResult { + pass: false, + fail_reasons: vec![reason.clone()], + }, + }; + write_gate_report(&out_path, &report)?; + eprintln!("{scope} gate fail: {reason}"); + Ok(()) +} + +fn write_gate_report(out_path: &Path, report: &CoverageGateReport) -> Result<(), String> { + let json = serde_json::to_string_pretty(report) + .expect("serializing coverage gate report should succeed"); + if let Err(err) = fs::write(out_path, format!("{json}\n")) { + return Err(format!("failed to write {}: {err}", out_path.display())); + } + Ok(()) +} + +fn coverage_report_path(reports_root: &Path, crate_name: &str) -> PathBuf { + reports_root + .join(crate_name.replace('-', "_")) + .join("gate-report.json") +} + +fn read_gate_report(path: &Path) -> Result<CoverageGateReport, String> { + let raw = match fs::read_to_string(path) { + Ok(raw) => raw, + Err(err) => { + return Err(format!( + "failed to read gate report {}: {err}", + path.display() + )); + } + }; + match serde_json::from_str::<CoverageGateReport>(&raw) { + Ok(report) => Ok(report), + Err(err) => Err(format!( + "failed to parse gate report {}: {err}", + path.display() + )), + } +} + +fn list_required_crates_with_root(root: &Path, writer: &mut dyn Write) -> Result<(), String> { + let required_path = coverage_policy_path(root); + let crates = read_required_crates(&required_path)?; + write_crate_names_output(writer, crates, "required crates") +} + +fn list_workspace_crates_with_root(root: &Path, writer: &mut dyn Write) -> Result<(), String> { + let crates = read_workspace_crates(root)?; + write_crate_names_output(writer, crates, "workspace crates") +} + +fn write_crate_names_output( + writer: &mut dyn Write, + crates: Vec<String>, + label: &str, +) -> Result<(), String> { + for crate_name in crates { + if let Err(err) = writeln!(writer, "{crate_name}") { + return Err(format!("failed to write {label} output: {err}")); + } + } + Ok(()) +} + +fn run_with_root(args: &[String], root: &Path) -> Result<(), String> { + match args.first().map(String::as_str) { + Some("help") => Ok(()), + Some("run-crate") => run_crate(&args[1..]), + Some("report") => report_gate_with_root(&args[1..], root), + Some("report-missing") => report_missing_gate_with_root(&args[1..], root), + Some("refresh-summary") => { + let reports_root = match parse_optional_string_arg(&args[1..], "reports-root") { + Some(raw) => PathBuf::from(raw), + None => PathBuf::from("target/coverage"), + }; + let out_path = match parse_optional_string_arg(&args[1..], "out") { + Some(raw) => PathBuf::from(raw), + None => PathBuf::from("target/coverage/coverage-refresh.tsv"), + }; + let status_out_path = + parse_optional_string_arg(&args[1..], "status-out").map(PathBuf::from); + let required_crates = read_required_crates(&coverage_policy_path(root))?; + + let mut refresh_rows = + String::from("crate\tstatus\texec\tfunc\tbranch\tregion\treport\n"); + let mut status_rows = String::from("crate\tstatus\n"); + + for crate_name in required_crates { + let report_path = coverage_report_path(&reports_root, &crate_name); + let report = read_gate_report(&report_path)?; + let status = if report.result.pass { "pass" } else { "fail" }; + let branch = report + .measured + .branches_percent + .map(|value| format!("{value:.6}")) + .unwrap_or_else(|| "unavailable".to_string()); + refresh_rows.push_str(&format!( + "{}\t{}\t{:.6}\t{:.6}\t{}\t{:.6}\t{}\n", + crate_name, + status, + report.measured.executable_lines_percent, + report.measured.functions_percent, + branch, + report.measured.summary_regions_percent, + report_path.display() + )); + status_rows.push_str(&format!("{}\t{}\n", crate_name, status)); + } + + if let Some(parent) = out_path.parent() + && !parent.as_os_str().is_empty() + && let Err(err) = fs::create_dir_all(parent) + { + return Err(format!("failed to create {}: {err}", parent.display())); + } + fs::write(&out_path, refresh_rows) + .map_err(|err| format!("failed to write {}: {err}", out_path.display()))?; + + if let Some(status_out_path) = status_out_path { + if let Some(parent) = status_out_path.parent() + && !parent.as_os_str().is_empty() + { + fs::create_dir_all(parent) + .map_err(|err| format!("failed to create {}: {err}", parent.display()))?; + } + fs::write(&status_out_path, status_rows).map_err(|err| { + format!("failed to write {}: {err}", status_out_path.display()) + })?; + } + + Ok(()) + } + Some("required-crates") => { + let mut stdout = std::io::stdout().lock(); + list_required_crates_with_root(root, &mut stdout) + } + Some("workspace-crates") => { + let mut stdout = std::io::stdout().lock(); + list_workspace_crates_with_root(root, &mut stdout) + } + Some(_) => Err("unknown coverage subcommand".to_string()), + None => Err("missing coverage subcommand".to_string()), + } +} + +pub fn run(args: &[String]) -> Result<(), String> { + let root = workspace_root(); + run_with_root(args, &root) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::io::{self, Write}; + use std::path::Path; + use std::sync::{Mutex, MutexGuard, OnceLock}; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_file_path(prefix: &str) -> PathBuf { + let ns = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time") + .as_nanos(); + std::env::temp_dir().join(format!("radroots_xtask_coverage_{prefix}_{ns}.tmp")) + } + + fn temp_dir_path(prefix: &str) -> PathBuf { + let ns = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time") + .as_nanos(); + std::env::temp_dir().join(format!("radroots_xtask_coverage_{prefix}_{ns}")) + } + + fn write_file(path: &Path, content: &str) { + let _ = fs::create_dir_all(path.parent().unwrap_or(Path::new(""))); + fs::write(path, content).expect("write file"); + } + + fn cwd_lock() -> &'static Mutex<()> { + static LOCK: OnceLock<Mutex<()>> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) + } + + fn recover_lock(lock: &'static Mutex<()>) -> MutexGuard<'static, ()> { + match lock.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } + } + + fn lock_cwd() -> MutexGuard<'static, ()> { + recover_lock(cwd_lock()) + } + + fn collect_command_envs(cmd: &Command) -> BTreeMap<String, Option<String>> { + let mut envs = BTreeMap::new(); + for (key, value) in cmd.get_envs() { + envs.insert( + key.to_string_lossy().to_string(), + value.map(|raw| raw.to_string_lossy().to_string()), + ); + } + envs + } + + fn ok_runner(_cmd: Command, _name: &str) -> Result<(), String> { + Ok(()) + } + + struct FailingWriter; + + impl Write for FailingWriter { + fn write(&mut self, _buf: &[u8]) -> io::Result<usize> { + Err(io::Error::other("forced write failure")) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + #[test] + fn reads_summary_totals_from_llvm_cov_json() { + let path = temp_file_path("summary"); + fs::write( + &path, + r#"{ + "data": [ + { + "totals": { + "functions": {"percent": 91.25}, + "lines": {"percent": 88.5}, + "regions": {"percent": 86.75} + } + } + ] +}"#, + ) + .expect("write summary"); + + let summary = read_summary(&path).expect("parse summary"); + assert_eq!(summary.functions_percent, 91.25); + assert_eq!(summary.summary_lines_percent, 88.5); + assert_eq!(summary.summary_regions_percent, 86.75); + + fs::remove_file(path).expect("remove summary"); + } + + #[test] + fn read_summary_normalizes_duplicate_generic_detail_records() { + let root = temp_dir_path("summary_details_normalized"); + let summary_path = root.join("coverage-summary.json"); + write_file( + &summary_path, + r#"{ + "data": [ + { + "totals": { + "functions": {"percent": 100.0}, + "lines": {"percent": 88.5}, + "regions": {"percent": 22.0} + } + } + ] +}"#, + ); + write_file( + &root.join("coverage-details.json"), + r#"{ + "data": [ + { + "functions": [ + { + "count": 4, + "filenames": ["/tmp/lib.rs"], + "regions": [ + [10, 1, 12, 2, 4, 0, 0, 0], + [13, 1, 13, 8, 4, 0, 0, 0] + ] + }, + { + "count": 0, + "filenames": ["/tmp/lib.rs"], + "regions": [ + [10, 1, 12, 2, 0, 0, 0, 0], + [13, 1, 13, 8, 0, 0, 0, 0] + ] + } + ] + } + ] +}"#, + ); + + let summary = read_summary(&summary_path).expect("parse normalized summary"); + assert_eq!(summary.functions_percent, 100.0); + assert_eq!(summary.summary_lines_percent, 88.5); + assert_eq!(summary.summary_regions_percent, 100.0); + + fs::remove_dir_all(root).expect("remove summary details root"); + } + + #[test] + fn read_summary_keeps_original_regions_when_functions_are_not_perfect() { + let root = temp_dir_path("summary_details_not_applied"); + let summary_path = root.join("coverage-summary.json"); + write_file( + &summary_path, + r#"{ + "data": [ + { + "totals": { + "functions": {"percent": 95.0}, + "lines": {"percent": 88.5}, + "regions": {"percent": 22.0} + } + } + ] +}"#, + ); + write_file( + &root.join("coverage-details.json"), + r#"{ + "data": [ + { + "functions": [ + { + "count": 4, + "filenames": ["/tmp/lib.rs"], + "regions": [ + [10, 1, 12, 2, 4, 0, 0, 0] + ] + } + ] + } + ] +}"#, + ); + + let summary = read_summary(&summary_path).expect("parse preserved summary"); + assert_eq!(summary.functions_percent, 95.0); + assert_eq!(summary.summary_regions_percent, 22.0); + + fs::remove_dir_all(root).expect("remove summary preserve root"); + } + + #[test] + fn read_summary_for_scope_ignores_other_crate_detail_records() { + let root = temp_dir_path("summary_details_scope_filtered"); + let summary_path = root.join("coverage-summary.json"); + write_file( + &summary_path, + r#"{ + "data": [ + { + "totals": { + "functions": {"percent": 100.0}, + "lines": {"percent": 88.5}, + "regions": {"percent": 22.0} + } + } + ] +}"#, + ); + write_file( + &root.join("coverage-details.json"), + r#"{ + "data": [ + { + "functions": [ + { + "count": 4, + "filenames": ["/workspace/crates/a/src/lib.rs"], + "regions": [ + [10, 1, 12, 2, 4, 0, 0, 0] + ] + }, + { + "count": 9, + "filenames": ["/workspace/crates/b/src/lib.rs"], + "regions": [ + [20, 1, 20, 6, 0, 0, 0, 0] + ] + } + ] + } + ] +}"#, + ); + + let summary = + read_summary_for_scope(&summary_path, Some("radroots_a")).expect("parse scope summary"); + assert_eq!(summary.functions_percent, 100.0); + assert_eq!(summary.summary_lines_percent, 88.5); + assert_eq!(summary.summary_regions_percent, 100.0); + + fs::remove_dir_all(root).expect("remove summary scope root"); + } + + #[test] + fn coverage_details_path_uses_summary_parent() { + let summary_path = Path::new("target/coverage/radroots_a/coverage-summary.json"); + assert_eq!( + coverage_details_path(summary_path), + Path::new("target/coverage/radroots_a/coverage-details.json") + ); + } + + #[test] + fn read_detailed_summary_covers_empty_skip_and_filter_paths() { + let root = temp_dir_path("details_empty_skip_filter"); + let missing = root.join("missing-details.json"); + let err = read_detailed_summary(&missing, None).expect_err("missing details"); + assert!(err.contains("failed to read coverage details")); + + let empty = root.join("empty-details.json"); + write_file(&empty, r#"{"data":[]}"#); + let err = read_detailed_summary(&empty, None).expect_err("empty details"); + assert!(err.contains("coverage details data is empty")); + + let skipped = root.join("skipped-details.json"); + write_file( + &skipped, + r#"{ + "data": [ + { + "functions": [ + { + "count": 1, + "filenames": [], + "regions": [[10, 1, 10, 2, 1, 0, 0, 0]] + }, + { + "count": 1, + "filenames": ["/workspace/crates/a/src/lib.rs"], + "regions": [] + } + ] + } + ] +}"#, + ); + let err = read_detailed_summary(&skipped, None).expect_err("skipped details"); + assert!(err.contains("coverage details functions are empty")); + + let filtered = root.join("filtered-details.json"); + write_file( + &filtered, + r#"{ + "data": [ + { + "functions": [ + { + "count": 0, + "filenames": ["/workspace/crates/a/src/lib.rs"], + "regions": [[10, 1, 10, 2, 0, 0, 0, 0]] + }, + { + "count": 1, + "filenames": ["/workspace/crates/b/src/lib.rs"], + "regions": [[20, 1, 20, 2, 1, 0, 0, 0]] + } + ] + } + ] +}"#, + ); + let summary = + read_detailed_summary(&filtered, Some("radroots_a")).expect("filtered summary"); + assert_eq!(summary.functions_percent, 0.0); + assert_eq!(summary.regions_percent, 0.0); + + fs::remove_dir_all(root).expect("remove detail edge root"); + } + + #[test] + fn read_detailed_summary_ignores_synthetic_regions_from_source() { + let root = temp_dir_path("details_synthetic_regions"); + let source_path = root + .join("crates") + .join("radroots_a") + .join("src") + .join("lib.rs"); + write_file(&source_path, "pub fn load() { let _value = call()?; }\n"); + let details_path = root.join("coverage-details.json"); + let raw = serde_json::json!({ + "data": [ + { + "functions": [ + { + "count": 1, + "filenames": [source_path.display().to_string()], + "regions": [ + [1, 1, 1, 37, 1, 0, 0, 0], + [1, 34, 1, 35, 0, 0, 0, 0] + ] + } + ] + } + ] + }); + write_file(&details_path, &raw.to_string()); + + let summary = + read_detailed_summary(&details_path, Some("radroots_a")).expect("synthetic summary"); + assert_eq!(summary.functions_percent, 100.0); + assert_eq!(summary.regions_percent, 100.0); + + fs::remove_dir_all(root).expect("remove synthetic details root"); + } + + #[test] + fn read_summary_reports_read_and_parse_errors() { + let missing = temp_file_path("summary_missing"); + let read_err = read_summary(&missing).expect_err("missing summary should fail"); + assert!(read_err.contains("failed to read summary")); + + let invalid = temp_file_path("summary_invalid"); + write_file(&invalid, "{not-json"); + let parse_err = read_summary(&invalid).expect_err("invalid summary should fail"); + assert!(parse_err.contains("failed to parse summary")); + fs::remove_file(invalid).expect("remove invalid summary"); + } + + #[test] + fn read_summary_reports_detail_parse_errors() { + let root = temp_dir_path("summary_invalid_details"); + let summary_path = root.join("coverage-summary.json"); + write_file( + &summary_path, + r#"{ + "data": [ + { + "totals": { + "functions": {"percent": 91.25}, + "lines": {"percent": 88.5}, + "regions": {"percent": 86.75} + } + } + ] +}"#, + ); + write_file(&root.join("coverage-details.json"), "{not-json"); + + let err = read_summary(&summary_path).expect_err("invalid details should fail"); + assert!(err.contains("failed to parse coverage details")); + + fs::remove_dir_all(root).expect("remove invalid details root"); + } + + #[test] + fn ignorable_question_mark_regions_require_single_char_question_mark() { + let path = temp_file_path("coverage_question_mark_region"); + write_file(&path, "let value = call()?;\nreturn Err(());\n"); + let mut cache = BTreeMap::new(); + + let question_mark = RegionCoverageKey { + line_start: 1, + column_start: 19, + line_end: 1, + column_end: 20, + kind: 0, + }; + assert!(is_ignorable_synthetic_region( + path.to_str().expect("utf-8 path"), + &question_mark, + &mut cache, + )); + + let not_question_mark = RegionCoverageKey { + line_start: 2, + column_start: 8, + line_end: 2, + column_end: 15, + kind: 0, + }; + assert!(!is_ignorable_synthetic_region( + path.to_str().expect("utf-8 path"), + &not_question_mark, + &mut cache, + )); + + let single_char_not_question_mark = RegionCoverageKey { + line_start: 2, + column_start: 1, + line_end: 2, + column_end: 2, + kind: 0, + }; + assert!(!is_ignorable_synthetic_region( + path.to_str().expect("utf-8 path"), + &single_char_not_question_mark, + &mut cache, + )); + + fs::remove_file(path).expect("remove question mark source"); + } + + #[test] + fn ignorable_unexpected_panic_regions_require_test_fallback_lines() { + let root = temp_dir_path("coverage_unexpected_panic_region"); + let path = root.join("tests.rs"); + write_file( + &path, + "match &err {\n RuntimeProtectedFileError::Io { .. } => {}\n other => panic!(\"unexpected io error: {other}\"),\n}\n", + ); + let mut cache = BTreeMap::new(); + + let other_region = RegionCoverageKey { + line_start: 3, + column_start: 9, + line_end: 3, + column_end: 14, + kind: 0, + }; + assert!(is_ignorable_synthetic_region( + path.to_str().expect("utf-8 path"), + &other_region, + &mut cache, + )); + + let panic_region = RegionCoverageKey { + line_start: 3, + column_start: 18, + line_end: 3, + column_end: 24, + kind: 0, + }; + assert!(is_ignorable_synthetic_region( + path.to_str().expect("utf-8 path"), + &panic_region, + &mut cache, + )); + + let non_test_path = root.join("source.rs"); + write_file( + &non_test_path, + "match &err {\n RuntimeProtectedFileError::Io { .. } => {}\n other => panic!(\"unexpected io error: {other}\"),\n}\n", + ); + assert!(!is_ignorable_synthetic_region( + non_test_path.to_str().expect("utf-8 path"), + &other_region, + &mut cache, + )); + + let multiline = RegionCoverageKey { + line_start: 1, + column_start: 1, + line_end: 2, + column_end: 1, + kind: 0, + }; + assert!(!is_ignorable_synthetic_region( + path.to_str().expect("utf-8 path"), + &multiline, + &mut cache, + )); + + let missing_file = root.join("missing.rs"); + assert!(!is_ignorable_synthetic_region( + missing_file.to_str().expect("utf-8 path"), + &other_region, + &mut cache, + )); + + let out_of_range = RegionCoverageKey { + line_start: 99, + column_start: 1, + line_end: 99, + column_end: 2, + kind: 0, + }; + assert!(!is_ignorable_synthetic_region( + path.to_str().expect("utf-8 path"), + &out_of_range, + &mut cache, + )); + + let non_panic_test_path = root.join("non_panic_tests.rs"); + write_file(&non_panic_test_path, " other => Ok(()),\n"); + let non_panic_other_region = RegionCoverageKey { + line_start: 1, + column_start: 9, + line_end: 1, + column_end: 14, + kind: 0, + }; + assert!(!is_ignorable_synthetic_region( + non_panic_test_path.to_str().expect("utf-8 path"), + &non_panic_other_region, + &mut cache, + )); + + let non_fallback_region = RegionCoverageKey { + line_start: 3, + column_start: 39, + line_end: 3, + column_end: 44, + kind: 0, + }; + assert!(!is_ignorable_synthetic_region( + path.to_str().expect("utf-8 path"), + &non_fallback_region, + &mut cache, + )); + + fs::remove_dir_all(root).expect("remove unexpected panic source"); + } + + #[test] + fn read_coverage_policy_rejects_non_finite_and_out_of_range_thresholds() { + let non_finite = temp_file_path("coverage_policy_non_finite"); + write_file( + &non_finite, + "[gate]\nfail_under_exec_lines = inf\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", + ); + let non_finite_err = + read_coverage_policy(&non_finite).expect_err("non-finite threshold should fail"); + assert!(non_finite_err.contains("must be finite")); + fs::remove_file(non_finite).expect("remove non-finite policy"); + + let out_of_range = temp_file_path("coverage_policy_out_of_range"); + write_file( + &out_of_range, + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 101.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", + ); + let out_of_range_err = + read_coverage_policy(&out_of_range).expect_err("out-of-range threshold should fail"); + assert!(out_of_range_err.contains("must be within 0..=100")); + fs::remove_file(out_of_range).expect("remove out-of-range policy"); + } + + #[test] + fn coverage_policy_resolves_scope_specific_temporary_overrides() { + let path = temp_file_path("coverage_policy_override_scope"); + write_file( + &path, + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\", \"radroots_b\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 88.5\nfail_under_functions = 77.5\nfail_under_regions = 66.5\nfail_under_branches = 55.5\nrequire_branches = false\ntemporary = true\nreason = \"temporary publish unblocker\"\n", + ); + let policy = read_coverage_policy(&path).expect("parse scoped override policy"); + let override_thresholds = policy.thresholds_for_scope("radroots_a"); + assert_eq!(override_thresholds.fail_under_exec_lines, 88.5); + assert_eq!(override_thresholds.fail_under_functions, 77.5); + assert_eq!(override_thresholds.fail_under_regions, 66.5); + assert_eq!(override_thresholds.fail_under_branches, 55.5); + assert!(!override_thresholds.require_branches); + + let default_thresholds = policy.thresholds_for_scope("radroots_b"); + assert_eq!(default_thresholds.fail_under_exec_lines, 100.0); + assert_eq!(default_thresholds.fail_under_functions, 100.0); + assert_eq!(default_thresholds.fail_under_regions, 100.0); + assert_eq!(default_thresholds.fail_under_branches, 100.0); + assert!(default_thresholds.require_branches); + + fs::remove_file(path).expect("remove override scope policy"); + } + + #[test] + fn read_coverage_policy_rejects_invalid_override_shapes() { + let non_required = temp_file_path("coverage_policy_override_non_required"); + write_file( + &non_required, + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_b]\nfail_under_exec_lines = 90.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n", + ); + let non_required_err = + read_coverage_policy(&non_required).expect_err("non-required override should fail"); + assert!(non_required_err.contains("must target a required crate")); + fs::remove_file(non_required).expect("remove non-required override policy"); + + let missing_temporary = temp_file_path("coverage_policy_override_missing_temporary"); + write_file( + &missing_temporary, + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 90.0\ntemporary = false\nreason = \"temporary publish unblocker\"\n", + ); + let missing_temporary_err = read_coverage_policy(&missing_temporary) + .expect_err("override without temporary=true should fail"); + assert!(missing_temporary_err.contains("temporary = true")); + fs::remove_file(missing_temporary).expect("remove temporary override policy"); + + let missing_reason = temp_file_path("coverage_policy_override_missing_reason"); + write_file( + &missing_reason, + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 90.0\ntemporary = true\nreason = \" \"\n", + ); + let missing_reason_err = + read_coverage_policy(&missing_reason).expect_err("blank override reason should fail"); + assert!(missing_reason_err.contains("non-empty reason")); + fs::remove_file(missing_reason).expect("remove missing reason policy"); + + let stricter = temp_file_path("coverage_policy_override_stricter"); + write_file( + &stricter, + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 100.1\ntemporary = true\nreason = \"temporary publish unblocker\"\n", + ); + let stricter_err = + read_coverage_policy(&stricter).expect_err("stricter override should fail"); + assert!(stricter_err.contains("must be within 0..=100")); + fs::remove_file(stricter).expect("remove stricter override policy"); + + let above_global = temp_file_path("coverage_policy_override_above_global"); + write_file( + &above_global, + "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 99.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n", + ); + let above_global_err = + read_coverage_policy(&above_global).expect_err("above-global override should fail"); + assert!(above_global_err.contains("must not exceed the global gate")); + fs::remove_file(above_global).expect("remove above-global override policy"); + + let above_global_functions = temp_file_path("coverage_policy_override_above_global_fn"); + write_file( + &above_global_functions, + "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_functions = 99.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n", + ); + let above_global_functions_err = read_coverage_policy(&above_global_functions) + .expect_err("above-global function override should fail"); + assert!(above_global_functions_err.contains("must not exceed the global gate")); + fs::remove_file(above_global_functions).expect("remove above-global function policy"); + + let above_global_regions = temp_file_path("coverage_policy_override_above_global_regions"); + write_file( + &above_global_regions, + "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_regions = 99.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n", + ); + let above_global_regions_err = read_coverage_policy(&above_global_regions) + .expect_err("above-global region override should fail"); + assert!(above_global_regions_err.contains("must not exceed the global gate")); + fs::remove_file(above_global_regions).expect("remove above-global region policy"); + + let above_global_branches = + temp_file_path("coverage_policy_override_above_global_branches"); + write_file( + &above_global_branches, + "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_branches = 99.0\ntemporary = true\nreason = \"temporary publish unblocker\"\n", + ); + let above_global_branches_err = read_coverage_policy(&above_global_branches) + .expect_err("above-global branch override should fail"); + assert!(above_global_branches_err.contains("must not exceed the global gate")); + fs::remove_file(above_global_branches).expect("remove above-global branch policy"); + + let non_finite_override = temp_file_path("coverage_policy_override_non_finite"); + write_file( + &non_finite_override, + "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = inf\ntemporary = true\nreason = \"temporary publish unblocker\"\n", + ); + let non_finite_override_err = read_coverage_policy(&non_finite_override) + .expect_err("non-finite override should fail"); + assert!(non_finite_override_err.contains("must be finite")); + fs::remove_file(non_finite_override).expect("remove non-finite override policy"); + + let stricter_branch_presence = temp_file_path("coverage_policy_override_branch_required"); + write_file( + &stricter_branch_presence, + "[gate]\nfail_under_exec_lines = 98.0\nfail_under_functions = 98.0\nfail_under_regions = 98.0\nfail_under_branches = 98.0\nrequire_branches = false\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nrequire_branches = true\ntemporary = true\nreason = \"temporary publish unblocker\"\n", + ); + let branch_presence_err = read_coverage_policy(&stricter_branch_presence) + .expect_err("stricter branch presence should fail"); + assert!(branch_presence_err.contains("require_branches cannot be stricter")); + fs::remove_file(stricter_branch_presence).expect("remove branch presence policy"); + } + + #[test] + fn report_missing_gate_uses_policy_thresholds() { + let root = temp_dir_path("report_missing_gate_root"); + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + write_file( + &coverage_dir.join("coverage.toml"), + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", + ); + let out_path = root.join("gate-report.json"); + + report_missing_gate_with_root( + &[ + "--scope".to_string(), + "radroots_a_blocking".to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--reason".to_string(), + "missing-coverage-artifacts".to_string(), + ], + &root, + ) + .expect("report missing gate"); + + let report_raw = fs::read_to_string(&out_path).expect("read gate report"); + let report_json: serde_json::Value = + serde_json::from_str(&report_raw).expect("parse gate report json"); + assert_eq!( + report_json["thresholds"]["executable_lines"], + serde_json::json!(100.0) + ); + assert_eq!( + report_json["thresholds"]["branches_required"], + serde_json::json!(true) + ); + assert_eq!(report_json["result"]["pass"], serde_json::json!(false)); + assert_eq!( + report_json["result"]["fail_reasons"], + serde_json::json!(["missing-coverage-artifacts"]) + ); + + fs::remove_dir_all(root).expect("remove root"); + } + + #[test] + fn report_missing_gate_uses_scope_specific_override_thresholds() { + let root = temp_dir_path("report_missing_gate_override_root"); + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + write_file( + &coverage_dir.join("coverage.toml"), + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 88.5\nfail_under_functions = 77.5\nfail_under_regions = 66.5\nfail_under_branches = 55.5\nrequire_branches = false\ntemporary = true\nreason = \"temporary publish unblocker\"\n", + ); + let out_path = root.join("gate-report.json"); + + report_missing_gate_with_root( + &[ + "--scope".to_string(), + "radroots_a".to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--reason".to_string(), + "missing-coverage-artifacts".to_string(), + ], + &root, + ) + .expect("report missing gate with override"); + + let report_raw = fs::read_to_string(&out_path).expect("read gate report"); + let report_json: serde_json::Value = + serde_json::from_str(&report_raw).expect("parse gate report json"); + assert_eq!( + report_json["thresholds"]["executable_lines"], + serde_json::json!(88.5) + ); + assert_eq!( + report_json["thresholds"]["functions"], + serde_json::json!(77.5) + ); + assert_eq!( + report_json["thresholds"]["regions"], + serde_json::json!(66.5) + ); + assert_eq!( + report_json["thresholds"]["branches"], + serde_json::json!(55.5) + ); + assert_eq!( + report_json["thresholds"]["branches_required"], + serde_json::json!(false) + ); + + fs::remove_dir_all(root).expect("remove override root"); + } + + #[test] + fn report_missing_gate_reports_argument_policy_and_write_errors() { + let root = temp_dir_path("report_missing_gate_error_root"); + let missing_scope = + report_missing_gate_with_root(&[], &root).expect_err("missing scope should fail"); + assert!(missing_scope.contains("missing --scope")); + + let missing_out = report_missing_gate_with_root( + &[ + "--scope".to_string(), + "radroots_a_blocking".to_string(), + "--reason".to_string(), + "missing-coverage-artifacts".to_string(), + ], + &root, + ) + .expect_err("missing out should fail"); + assert!(missing_out.contains("missing --out")); + + let missing_reason = report_missing_gate_with_root( + &[ + "--scope".to_string(), + "radroots_a_blocking".to_string(), + "--out".to_string(), + root.join("missing-gate.json").display().to_string(), + ], + &root, + ) + .expect_err("missing reason should fail"); + assert!(missing_reason.contains("missing --reason")); + + let policy_err = report_missing_gate_with_root( + &[ + "--scope".to_string(), + "radroots_a_blocking".to_string(), + "--out".to_string(), + root.join("missing-gate.json").display().to_string(), + "--reason".to_string(), + "missing-coverage-artifacts".to_string(), + ], + &root, + ) + .expect_err("missing policy should fail"); + assert!(policy_err.contains("failed to read coverage policy")); + + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + write_file( + &coverage_dir.join("coverage.toml"), + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", + ); + let out_path = root.join("gate-report.json"); + fs::create_dir_all(&out_path).expect("create blocking output dir"); + let write_err = report_missing_gate_with_root( + &[ + "--scope".to_string(), + "radroots_a_blocking".to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--reason".to_string(), + "missing-coverage-artifacts".to_string(), + ], + &root, + ) + .expect_err("directory output should fail"); + assert!(write_err.contains("failed to write")); + + fs::remove_dir_all(root).expect("remove report missing gate error root"); + } + + #[test] + fn refresh_summary_uses_measured_gate_report_values() { + let root = temp_dir_path("refresh_summary_root"); + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + write_file( + &coverage_dir.join("coverage.toml"), + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\", \"radroots_b\"]\n", + ); + + let reports_root = root.join("target").join("coverage"); + let crate_dir = reports_root.join("radroots_a"); + fs::create_dir_all(&crate_dir).expect("create crate dir"); + write_file( + &crate_dir.join("gate-report.json"), + r#"{ + "scope": "radroots_a", + "thresholds": { + "executable_lines": 100.0, + "functions": 100.0, + "regions": 100.0, + "branches": 100.0, + "branches_required": true + }, + "measured": { + "executable_lines_percent": 100.0, + "executable_lines_source": "da", + "functions_percent": 100.0, + "branches_percent": 100.0, + "branches_available": true, + "summary_lines_percent": 100.0, + "summary_regions_percent": 97.5 + }, + "counts": { + "executable_lines": { + "covered": 4, + "total": 4 + }, + "branches": { + "covered": 2, + "total": 2 + } + }, + "result": { + "pass": true, + "fail_reasons": [] + } +}"#, + ); + let no_branch_crate_dir = reports_root.join("radroots_b"); + fs::create_dir_all(&no_branch_crate_dir).expect("create no branch crate dir"); + write_file( + &no_branch_crate_dir.join("gate-report.json"), + r#"{ + "scope": "radroots_b", + "thresholds": { + "executable_lines": 100.0, + "functions": 100.0, + "regions": 100.0, + "branches": 100.0, + "branches_required": false + }, + "measured": { + "executable_lines_percent": 100.0, + "executable_lines_source": "da", + "functions_percent": 100.0, + "branches_percent": null, + "branches_available": false, + "summary_lines_percent": 100.0, + "summary_regions_percent": 100.0 + }, + "counts": { + "executable_lines": { + "covered": 4, + "total": 4 + }, + "branches": { + "covered": 0, + "total": 0 + } + }, + "result": { + "pass": true, + "fail_reasons": [] + } +}"#, + ); + + let refresh_out = reports_root.join("coverage-refresh.tsv"); + let status_out = reports_root.join("coverage-refresh-status.tsv"); + run_with_root( + &[ + "refresh-summary".to_string(), + "--reports-root".to_string(), + reports_root.display().to_string(), + "--out".to_string(), + refresh_out.display().to_string(), + "--status-out".to_string(), + status_out.display().to_string(), + ], + &root, + ) + .expect("write refresh summary"); + + let refresh = fs::read_to_string(&refresh_out).expect("read refresh summary"); + assert!(refresh.contains("crate\tstatus\texec\tfunc\tbranch\tregion\treport")); + assert!( + refresh.contains("radroots_a\tpass\t100.000000\t100.000000\t100.000000\t97.500000\t") + ); + assert!( + refresh.contains("radroots_b\tpass\t100.000000\t100.000000\tunavailable\t100.000000\t") + ); + + let status = fs::read_to_string(&status_out).expect("read status summary"); + assert_eq!( + status, + "crate\tstatus\nradroots_a\tpass\nradroots_b\tpass\n" + ); + + fs::remove_dir_all(root).expect("remove root"); + + let defaults_root = temp_dir_path("refresh_summary_defaults_root"); + let defaults_coverage_dir = defaults_root.join("contracts"); + fs::create_dir_all(&defaults_coverage_dir).expect("create defaults coverage dir"); + write_file( + &defaults_coverage_dir.join("coverage.toml"), + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", + ); + write_file( + &defaults_root + .join("target") + .join("coverage") + .join("radroots_a") + .join("gate-report.json"), + r#"{ + "scope": "radroots_a", + "thresholds": { + "executable_lines": 100.0, + "functions": 100.0, + "regions": 100.0, + "branches": 100.0, + "branches_required": true + }, + "measured": { + "executable_lines_percent": 100.0, + "executable_lines_source": "da", + "functions_percent": 100.0, + "branches_percent": 100.0, + "branches_available": true, + "summary_lines_percent": 100.0, + "summary_regions_percent": 100.0 + }, + "counts": { + "executable_lines": { + "covered": 4, + "total": 4 + }, + "branches": { + "covered": 2, + "total": 2 + } + }, + "result": { + "pass": false, + "fail_reasons": ["synthetic-fail"] + } +}"#, + ); + + let _guard = lock_cwd(); + let previous_dir = std::env::current_dir().expect("read current dir"); + std::env::set_current_dir(&defaults_root).expect("set current dir"); + run_with_root(&["refresh-summary".to_string()], &defaults_root) + .expect("write refresh summary defaults"); + let defaults_refresh = fs::read_to_string( + defaults_root + .join("target") + .join("coverage") + .join("coverage-refresh.tsv"), + ) + .expect("read defaults refresh summary"); + assert!( + defaults_refresh + .contains("radroots_a\tfail\t100.000000\t100.000000\t100.000000\t100.000000\t") + ); + + let dispatch_root = temp_dir_path("refresh_summary_parentless_root"); + let dispatch_coverage_dir = dispatch_root.join("contracts"); + fs::create_dir_all(&dispatch_coverage_dir).expect("create dispatch coverage dir"); + write_file( + &dispatch_coverage_dir.join("coverage.toml"), + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", + ); + write_file( + &dispatch_root.join("Cargo.toml"), + "[workspace]\nmembers = []\nresolver = \"2\"\n", + ); + write_file( + &dispatch_root + .join("target") + .join("coverage") + .join("radroots_a") + .join("gate-report.json"), + r#"{ + "scope": "radroots_a", + "thresholds": { + "executable_lines": 100.0, + "functions": 100.0, + "regions": 100.0, + "branches": 100.0, + "branches_required": true + }, + "measured": { + "executable_lines_percent": 100.0, + "executable_lines_source": "da", + "functions_percent": 100.0, + "branches_percent": 100.0, + "branches_available": true, + "summary_lines_percent": 100.0, + "summary_regions_percent": 100.0 + }, + "counts": { + "executable_lines": { + "covered": 4, + "total": 4 + }, + "branches": { + "covered": 2, + "total": 2 + } + }, + "result": { + "pass": true, + "fail_reasons": [] + } +}"#, + ); + std::env::set_current_dir(&dispatch_root).expect("set dispatch current dir"); + run_with_root( + &[ + "report-missing".to_string(), + "--scope".to_string(), + "radroots_a_blocking".to_string(), + "--out".to_string(), + "missing-gate.json".to_string(), + "--reason".to_string(), + "missing-coverage-artifacts".to_string(), + ], + &dispatch_root, + ) + .expect("dispatch report-missing"); + run_with_root( + &[ + "refresh-summary".to_string(), + "--out".to_string(), + "coverage-refresh.tsv".to_string(), + "--status-out".to_string(), + "coverage-refresh-status.tsv".to_string(), + ], + &dispatch_root, + ) + .expect("dispatch refresh-summary"); + std::env::set_current_dir(previous_dir).expect("restore current dir"); + + assert!(dispatch_root.join("missing-gate.json").exists()); + assert!(dispatch_root.join("coverage-refresh.tsv").exists()); + assert!(dispatch_root.join("coverage-refresh-status.tsv").exists()); + + fs::remove_dir_all(defaults_root).expect("remove defaults root"); + fs::remove_dir_all(dispatch_root).expect("remove dispatch root"); + } + + #[test] + fn refresh_summary_rejects_empty_output_paths() { + let root = temp_dir_path("refresh_summary_empty_paths_root"); + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + write_file( + &coverage_dir.join("coverage.toml"), + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", + ); + write_file( + &root + .join("target") + .join("coverage") + .join("radroots_a") + .join("gate-report.json"), + r#"{ + "scope": "radroots_a", + "thresholds": { + "executable_lines": 100.0, + "functions": 100.0, + "regions": 100.0, + "branches": 100.0, + "branches_required": true + }, + "measured": { + "executable_lines_percent": 100.0, + "executable_lines_source": "da", + "functions_percent": 100.0, + "branches_percent": 100.0, + "branches_available": true, + "summary_lines_percent": 100.0, + "summary_regions_percent": 100.0 + }, + "counts": { + "executable_lines": { + "covered": 4, + "total": 4 + }, + "branches": { + "covered": 2, + "total": 2 + } + }, + "result": { + "pass": true, + "fail_reasons": [] + } +}"#, + ); + + let out_err = run_with_root( + &[ + "refresh-summary".to_string(), + "--reports-root".to_string(), + root.join("target").join("coverage").display().to_string(), + "--out".to_string(), + String::new(), + ], + &root, + ) + .expect_err("empty out path should fail"); + assert!(out_err.contains("failed to write")); + + let status_err = run_with_root( + &[ + "refresh-summary".to_string(), + "--reports-root".to_string(), + root.join("target").join("coverage").display().to_string(), + "--out".to_string(), + root.join("target") + .join("coverage") + .join("coverage-refresh.tsv") + .display() + .to_string(), + "--status-out".to_string(), + String::new(), + ], + &root, + ) + .expect_err("empty status out path should fail"); + assert!(status_err.contains("failed to write")); + + fs::remove_dir_all(root).expect("remove empty path root"); + } + + #[test] + fn refresh_summary_reports_output_parent_creation_failure() { + let root = temp_dir_path("refresh_summary_out_parent_fail"); + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + write_file( + &coverage_dir.join("coverage.toml"), + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", + ); + write_file( + &root + .join("target") + .join("coverage") + .join("radroots_a") + .join("gate-report.json"), + r#"{ + "scope": "radroots_a", + "thresholds": { + "executable_lines": 100.0, + "functions": 100.0, + "regions": 100.0, + "branches": 100.0, + "branches_required": true + }, + "measured": { + "executable_lines_percent": 100.0, + "executable_lines_source": "da", + "functions_percent": 100.0, + "branches_percent": 100.0, + "branches_available": true, + "summary_lines_percent": 100.0, + "summary_regions_percent": 100.0 + }, + "counts": { + "executable_lines": { + "covered": 4, + "total": 4 + }, + "branches": { + "covered": 2, + "total": 2 + } + }, + "result": { + "pass": true, + "fail_reasons": [] + } +}"#, + ); + write_file(&root.join("out-blocker"), "x"); + + let err = run_with_root( + &[ + "refresh-summary".to_string(), + "--reports-root".to_string(), + root.join("target").join("coverage").display().to_string(), + "--out".to_string(), + root.join("out-blocker") + .join("nested") + .join("coverage-refresh.tsv") + .display() + .to_string(), + ], + &root, + ) + .expect_err("out parent create failure should bubble up"); + assert!(err.contains("failed to create")); + + fs::remove_dir_all(root).expect("remove out parent fail root"); + } + + #[test] + fn refresh_summary_reports_status_output_parent_creation_failure() { + let root = temp_dir_path("refresh_summary_status_parent_fail"); + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + write_file( + &coverage_dir.join("coverage.toml"), + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", + ); + write_file( + &root + .join("target") + .join("coverage") + .join("radroots_a") + .join("gate-report.json"), + r#"{ + "scope": "radroots_a", + "thresholds": { + "executable_lines": 100.0, + "functions": 100.0, + "regions": 100.0, + "branches": 100.0, + "branches_required": true + }, + "measured": { + "executable_lines_percent": 100.0, + "executable_lines_source": "da", + "functions_percent": 100.0, + "branches_percent": 100.0, + "branches_available": true, + "summary_lines_percent": 100.0, + "summary_regions_percent": 100.0 + }, + "counts": { + "executable_lines": { + "covered": 4, + "total": 4 + }, + "branches": { + "covered": 2, + "total": 2 + } + }, + "result": { + "pass": true, + "fail_reasons": [] + } +}"#, + ); + write_file(&root.join("status-blocker"), "x"); + + let err = run_with_root( + &[ + "refresh-summary".to_string(), + "--reports-root".to_string(), + root.join("target").join("coverage").display().to_string(), + "--out".to_string(), + root.join("target") + .join("coverage") + .join("coverage-refresh.tsv") + .display() + .to_string(), + "--status-out".to_string(), + root.join("status-blocker") + .join("nested") + .join("coverage-refresh-status.tsv") + .display() + .to_string(), + ], + &root, + ) + .expect_err("status-out parent create failure should bubble up"); + assert!(err.contains("failed to create")); + + fs::remove_dir_all(root).expect("remove status parent fail root"); + } + + #[test] + fn refresh_summary_reports_policy_and_gate_report_errors() { + let root = temp_dir_path("refresh_summary_error_root"); + let policy_err = run_with_root( + &[ + "refresh-summary".to_string(), + "--reports-root".to_string(), + root.join("target").join("coverage").display().to_string(), + ], + &root, + ) + .expect_err("missing policy should fail"); + assert!(policy_err.contains("failed to read coverage policy")); + + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + write_file( + &coverage_dir.join("coverage.toml"), + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n", + ); + let gate_err = run_with_root( + &[ + "refresh-summary".to_string(), + "--reports-root".to_string(), + root.join("target").join("coverage").display().to_string(), + ], + &root, + ) + .expect_err("missing gate report should fail"); + assert!(gate_err.contains("failed to read gate report")); + + fs::remove_dir_all(root).expect("remove refresh summary error root"); + } + + #[test] + fn recover_lock_covers_ok_and_poisoned_paths() { + let ok_lock: &'static Mutex<()> = Box::leak(Box::new(Mutex::new(()))); + let _ok_guard = recover_lock(ok_lock); + + let poisoned_lock: &'static Mutex<()> = Box::leak(Box::new(Mutex::new(()))); + let handle = std::thread::spawn(move || { + let _guard = poisoned_lock.lock().expect("lock poisoned mutex"); + panic!("poison test mutex"); + }); + assert!(handle.join().is_err()); + + let _poisoned_guard = recover_lock(poisoned_lock); + } + + #[test] + fn read_summary_reports_empty_data_error() { + let path = temp_file_path("summary_empty_data"); + write_file(&path, r#"{"data":[]}"#); + let err = read_summary(&path).expect_err("summary without data should fail"); + assert!(err.contains("summary data is empty")); + fs::remove_file(path).expect("remove empty summary"); + } + + #[test] + fn reads_lcov_da_and_branch_metrics() { + let path = temp_file_path("lcov"); + fs::write( + &path, + "DA:1,1\nDA:2,0\nDA:3,1\nBRDA:1,0,0,1\nBRDA:1,0,1,0\nBRDA:2,0,0,3\nBRDA:2,0,1,-\n", + ) + .expect("write lcov"); + + let lcov = read_lcov(&path).expect("parse lcov"); + assert_eq!(lcov.executable_total, 3); + assert_eq!(lcov.executable_covered, 2); + assert!(lcov.branches_available); + assert_eq!(lcov.branch_total, 3); + assert_eq!(lcov.branch_covered, 2); + assert_eq!(lcov.branch_percent, Some(66.66666666666666)); + + fs::remove_file(path).expect("remove lcov"); + } + + #[test] + fn reads_lcov_branch_metrics_from_brf_brh_when_brda_missing() { + let path = temp_file_path("lcov_fallback"); + fs::write(&path, "DA:1,1\nDA:2,1\nBRF:4\nBRH:3\n").expect("write lcov"); + + let lcov = read_lcov(&path).expect("parse lcov"); + assert!(lcov.branches_available); + assert_eq!(lcov.branch_total, 4); + assert_eq!(lcov.branch_covered, 3); + assert_eq!(lcov.branch_percent, Some(75.0)); + + fs::remove_file(path).expect("remove lcov"); + } + + #[test] + fn gate_fails_when_branch_data_is_required_but_missing() { + let summary = CoverageSummary { + functions_percent: 100.0, + summary_lines_percent: 100.0, + summary_regions_percent: 100.0, + }; + let lcov = LcovCoverage { + executable_total: 10, + executable_covered: 10, + executable_percent: 100.0, + executable_source: ExecutableSource::Da, + branch_total: 0, + branch_covered: 0, + branches_available: false, + branch_percent: None, + }; + let thresholds = CoverageThresholds { + fail_under_exec_lines: 100.0, + fail_under_functions: 100.0, + fail_under_regions: 100.0, + fail_under_branches: 100.0, + require_branches: true, + }; + + let gate = evaluate_gate(&summary, &lcov, thresholds); + assert!(!gate.pass); + assert!( + gate.fail_reasons + .iter() + .any(|reason| reason == "branches=unavailable") + ); + } + + #[test] + fn reads_required_crates_and_rejects_duplicates() { + let path = temp_file_path("required_crates"); + fs::write( + &path, + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"a\", \"b\"]\n", + ) + .expect("write required crates"); + let crates = read_required_crates(&path).expect("parse required crates"); + assert_eq!(crates, vec!["a".to_string(), "b".to_string()]); + fs::remove_file(&path).expect("remove required crates"); + + let dup_path = temp_file_path("required_crates_dup"); + fs::write( + &dup_path, + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"a\", \"a\"]\n", + ) + .expect("write dup required crates"); + let err = read_required_crates(&dup_path).expect_err("duplicate required crates"); + assert!(err.contains("duplicate crate a")); + fs::remove_file(dup_path).expect("remove dup required crates"); + } + + #[test] + fn read_required_crates_reports_read_and_parse_errors() { + let missing = temp_file_path("required_missing"); + let read_err = read_required_crates(&missing).expect_err("missing required file"); + assert!(read_err.contains("failed to read coverage policy")); + + let invalid = temp_file_path("required_invalid"); + write_file(&invalid, "not = [toml"); + let parse_err = read_required_crates(&invalid).expect_err("invalid required file"); + assert!(parse_err.contains("failed to parse coverage policy")); + fs::remove_file(invalid).expect("remove invalid required file"); + } + + #[test] + fn reads_workspace_crates_and_contains_xtask() { + let root = workspace_root(); + let crates = read_workspace_crates(&root).expect("workspace crates"); + assert!(!crates.is_empty()); + assert!(crates.iter().any(|crate_name| crate_name == "xtask")); + } + + #[test] + fn coverage_profiles_default_when_contract_file_is_missing() { + let root = temp_dir_path("profile_missing"); + fs::create_dir_all(&root).expect("create root"); + let profile = read_coverage_profile(&root, "radroots_log").expect("read profile"); + assert!(!profile.no_default_features); + assert!(profile.features.is_empty()); + assert_eq!(profile.test_threads, None); + fs::remove_dir_all(root).expect("remove root"); + } + + #[test] + fn coverage_profiles_merge_defaults_and_crate_overrides() { + let root = temp_dir_path("profile_merge"); + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + fs::write( + coverage_dir.join("coverage-profiles.toml"), + r#"[profiles.default] +no_default_features = false +features = ["std"] +test_threads = 2 + +[profiles.crates."radroots_log"] +no_default_features = true +features = ["rt"] +"#, + ) + .expect("write profiles"); + + let app_profile = read_coverage_profile(&root, "radroots_log").expect("app profile"); + assert!(app_profile.no_default_features); + assert_eq!(app_profile.features, vec!["rt".to_string()]); + assert_eq!(app_profile.test_threads, Some(2)); + + let other_profile = read_coverage_profile(&root, "radroots_types").expect("other profile"); + assert!(!other_profile.no_default_features); + assert_eq!(other_profile.features, vec!["std".to_string()]); + assert_eq!(other_profile.test_threads, Some(2)); + + fs::remove_dir_all(root).expect("remove root"); + } + + #[test] + fn coverage_profiles_accept_positive_test_threads() { + let root = temp_dir_path("profile_positive_threads"); + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + fs::write( + coverage_dir.join("coverage-profiles.toml"), + r#"[profiles.crates."radroots_log"] +test_threads = 4 +"#, + ) + .expect("write profiles"); + let profile = + read_coverage_profile(&root, "radroots_log").expect("valid positive thread profile"); + assert_eq!(profile.test_threads, Some(4)); + fs::remove_dir_all(root).expect("remove root"); + } + + #[test] + fn coverage_profiles_reject_invalid_feature_and_thread_values() { + let root = temp_dir_path("profile_invalid"); + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + fs::write( + coverage_dir.join("coverage-profiles.toml"), + r#"[profiles.crates."radroots_log"] +features = [""] +test_threads = 0 +"#, + ) + .expect("write profiles"); + + let err = read_coverage_profile(&root, "radroots_log").expect_err("invalid profile"); + assert!( + err.contains("empty feature value"), + "unexpected error: {err}" + ); + + fs::remove_dir_all(root).expect("remove root"); + } + + #[test] + fn coverage_profiles_reject_invalid_toml() { + let root = temp_dir_path("profile_invalid_toml"); + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + fs::write( + coverage_dir.join("coverage-profiles.toml"), + "[profiles.default\n", + ) + .expect("write invalid profiles"); + let err = read_coverage_profile(&root, "radroots_log").expect_err("invalid toml"); + assert!(err.contains("failed to parse")); + fs::remove_dir_all(root).expect("remove root"); + } + + #[test] + fn coverage_profiles_reject_zero_test_threads_without_feature_error() { + let root = temp_dir_path("profile_invalid_threads"); + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + fs::write( + coverage_dir.join("coverage-profiles.toml"), + r#"[profiles.crates."radroots_log"] +test_threads = 0 +"#, + ) + .expect("write profiles"); + + let err = read_coverage_profile(&root, "radroots_log").expect_err("invalid thread count"); + assert!(err.contains("test_threads > 0")); + + fs::remove_dir_all(root).expect("remove root"); + } + + #[test] + fn parse_helpers_cover_success_and_error_paths() { + let args = vec![ + "--scope".to_string(), + "crate-a".to_string(), + "--value".to_string(), + "3.5".to_string(), + "--threads".to_string(), + "4".to_string(), + "--flag".to_string(), + ]; + assert_eq!( + parse_string_arg(&args, "scope").expect("scope value"), + "crate-a".to_string() + ); + assert_eq!( + parse_optional_string_arg(&args, "scope").expect("optional scope"), + "crate-a".to_string() + ); + assert_eq!(parse_f64_arg(&args, "value", 1.0).expect("f64 value"), 3.5); + assert_eq!( + parse_optional_u32_arg(&args, "threads").expect("u32 value"), + Some(4) + ); + assert!(parse_bool_flag(&args, "flag")); + assert_eq!(parse_optional_string_arg(&args, "missing"), None); + assert_eq!( + parse_f64_arg(&args, "missing", 2.25).expect("default f64"), + 2.25 + ); + assert_eq!( + parse_optional_u32_arg(&args, "missing").expect("missing u32"), + None + ); + + let missing_err = parse_string_arg(&args, "absent").expect_err("missing arg"); + assert!(missing_err.contains("missing --absent")); + + let missing_value = vec!["--scope".to_string()]; + let missing_value_err = + parse_string_arg(&missing_value, "scope").expect_err("missing arg value"); + assert!(missing_value_err.contains("missing value for --scope")); + + let invalid_f64 = vec!["--value".to_string(), "bad".to_string()]; + let invalid_f64_err = parse_f64_arg(&invalid_f64, "value", 1.0).expect_err("invalid f64"); + assert!(invalid_f64_err.contains("invalid --value value")); + + let invalid_u32 = vec!["--threads".to_string(), "bad".to_string()]; + let invalid_u32_err = + parse_optional_u32_arg(&invalid_u32, "threads").expect_err("invalid u32"); + assert!(invalid_u32_err.contains("invalid --threads value")); + } + + #[test] + fn executable_source_labels_cover_all_variants() { + assert_eq!(executable_source_label(ExecutableSource::Da), "da"); + assert_eq!(executable_source_label(ExecutableSource::LfLh), "lf_lh"); + } + + #[test] + fn read_required_crates_rejects_empty_and_blank_entries() { + let empty_path = temp_file_path("required_empty"); + write_file( + &empty_path, + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = []\n", + ); + let empty_err = read_required_crates(&empty_path).expect_err("empty required list"); + assert!(empty_err.contains("must not be empty")); + fs::remove_file(&empty_path).expect("remove empty required file"); + + let blank_path = temp_file_path("required_blank"); + write_file( + &blank_path, + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"a\", \" \"]\n", + ); + let blank_err = read_required_crates(&blank_path).expect_err("blank crate name"); + assert!(blank_err.contains("empty crate name")); + fs::remove_file(&blank_path).expect("remove blank required file"); + } + + #[test] + fn read_workspace_crates_rejects_invalid_workspace_shapes() { + let root_empty = temp_dir_path("workspace_empty_members"); + write_file( + &root_empty.join("Cargo.toml"), + "[workspace]\nmembers = []\n", + ); + let empty_err = read_workspace_crates(&root_empty).expect_err("empty workspace members"); + assert!(empty_err.contains("must not be empty")); + fs::remove_dir_all(&root_empty).expect("remove empty members root"); + + let root_blank = temp_dir_path("workspace_blank_package_name"); + write_file( + &root_blank.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/a\"]\n", + ); + write_file( + &root_blank.join("crates").join("a").join("Cargo.toml"), + "[package]\nname = \"\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ); + let blank_err = read_workspace_crates(&root_blank).expect_err("blank package name"); + assert!(blank_err.contains("empty package name")); + fs::remove_dir_all(&root_blank).expect("remove blank package root"); + + let root_duplicate = temp_dir_path("workspace_duplicate_package"); + write_file( + &root_duplicate.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/a\", \"crates/b\"]\n", + ); + let package_manifest = + "[package]\nname = \"duplicate\"\nversion = \"0.1.0\"\nedition = \"2024\"\n"; + write_file( + &root_duplicate.join("crates").join("a").join("Cargo.toml"), + package_manifest, + ); + write_file( + &root_duplicate.join("crates").join("b").join("Cargo.toml"), + package_manifest, + ); + let dup_err = read_workspace_crates(&root_duplicate).expect_err("duplicate package names"); + assert!(dup_err.contains("duplicate package name")); + fs::remove_dir_all(&root_duplicate).expect("remove duplicate package root"); + + let root_parse = temp_dir_path("workspace_parse_error"); + write_file( + &root_parse.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/a\"]\n", + ); + write_file( + &root_parse.join("crates").join("a").join("Cargo.toml"), + "[package", + ); + let parse_err = read_workspace_crates(&root_parse).expect_err("invalid package manifest"); + assert!(parse_err.contains("failed to parse")); + fs::remove_dir_all(&root_parse).expect("remove parse package root"); + } + + #[test] + fn parse_toml_reports_read_and_parse_errors() { + let missing = temp_file_path("parse_toml_missing"); + let read_err = + parse_toml::<CoveragePolicyFile>(&missing).expect_err("missing file should fail"); + assert!(read_err.contains("failed to read")); + + let invalid = temp_file_path("parse_toml_invalid"); + write_file(&invalid, "[gate]\nfail_under_exec_lines = 100.0\n"); + let parse_err = + parse_toml::<CoveragePolicyFile>(&invalid).expect_err("invalid toml should fail"); + assert!(parse_err.contains("failed to parse")); + fs::remove_file(invalid).expect("remove invalid toml"); + + let workspace_missing = temp_file_path("parse_toml_workspace_missing"); + let workspace_read_err = parse_toml::<WorkspaceManifest>(&workspace_missing) + .expect_err("missing workspace manifest should fail"); + assert!(workspace_read_err.contains("failed to read")); + + let workspace_invalid = temp_file_path("parse_toml_workspace_invalid"); + write_file(&workspace_invalid, "[workspace"); + let workspace_parse_err = parse_toml::<WorkspaceManifest>(&workspace_invalid) + .expect_err("invalid workspace manifest should fail"); + assert!(workspace_parse_err.contains("failed to parse")); + fs::remove_file(workspace_invalid).expect("remove invalid workspace manifest"); + + let package_missing = temp_file_path("parse_toml_package_missing"); + let package_read_err = parse_toml::<PackageManifest>(&package_missing) + .expect_err("missing package manifest should fail"); + assert!(package_read_err.contains("failed to read")); + + let package_invalid = temp_file_path("parse_toml_package_invalid"); + write_file(&package_invalid, "[package"); + let package_parse_err = parse_toml::<PackageManifest>(&package_invalid) + .expect_err("invalid package manifest should fail"); + assert!(package_parse_err.contains("failed to parse")); + fs::remove_file(package_invalid).expect("remove invalid package manifest"); + + let profiles_missing = temp_file_path("parse_toml_profiles_missing"); + let profiles_read_err = parse_toml::<CoverageProfilesFile>(&profiles_missing) + .expect_err("missing coverage profiles should fail"); + assert!(profiles_read_err.contains("failed to read")); + + let profiles_invalid = temp_file_path("parse_toml_profiles_invalid"); + write_file(&profiles_invalid, "[profiles.default"); + let profiles_parse_err = parse_toml::<CoverageProfilesFile>(&profiles_invalid) + .expect_err("invalid coverage profiles should fail"); + assert!(profiles_parse_err.contains("failed to parse")); + fs::remove_file(profiles_invalid).expect("remove invalid coverage profiles"); + } + + #[test] + fn parse_toml_parses_valid_coverage_required_contract() { + let valid = temp_file_path("parse_toml_valid"); + write_file( + &valid, + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_core\"]\n", + ); + let parsed = parse_toml::<CoveragePolicyFile>(&valid).expect("valid toml"); + assert_eq!(parsed.required.crates, vec!["radroots_core".to_string()]); + fs::remove_file(valid).expect("remove valid toml"); + } + + #[test] + fn read_lcov_rejects_invalid_records() { + let cases = vec![ + ("invalid_da_shape", "DA:1\n", "invalid DA record"), + ("invalid_da_hits", "DA:1,bad\n", "invalid DA hit count"), + ("invalid_lf", "LF:bad\n", "invalid LF value"), + ("invalid_lh", "LH:bad\n", "invalid LH value"), + ("invalid_brf", "BRF:bad\n", "invalid BRF value"), + ("invalid_brh", "BRH:bad\n", "invalid BRH value"), + ("invalid_brda_shape", "BRDA:1,0,0\n", "invalid BRDA record"), + ( + "invalid_brda_taken", + "BRDA:1,0,0,bad\n", + "invalid BRDA taken count", + ), + ( + "invalid_brda_extra", + "BRDA:1,0,0,1,extra\n", + "invalid BRDA record", + ), + ]; + for (prefix, raw, expected) in cases { + let path = temp_file_path(prefix); + write_file(&path, raw); + let err = read_lcov(&path).expect_err("invalid lcov record"); + assert!( + err.contains(expected), + "expected `{expected}` in `{err}` for case {prefix}" + ); + fs::remove_file(path).expect("remove invalid lcov file"); + } + } + + #[test] + fn read_lcov_reports_read_error() { + let missing = temp_file_path("lcov_missing"); + let err = read_lcov(&missing).expect_err("missing lcov should fail"); + assert!(err.contains("failed to read lcov")); + } + + #[test] + fn read_lcov_uses_lf_lh_when_da_is_missing_and_branches_absent() { + let path = temp_file_path("lcov_lf_lh"); + fs::write(&path, "LF:4\nLH:3\n").expect("write lcov"); + let parsed = read_lcov(&path).expect("parse lcov"); + assert_eq!(executable_source_label(parsed.executable_source), "lf_lh"); + assert_eq!(parsed.executable_total, 4); + assert_eq!(parsed.executable_covered, 3); + assert_eq!(parsed.executable_percent, 75.0); + assert!(!parsed.branches_available); + assert_eq!(parsed.branch_percent, None); + fs::remove_file(path).expect("remove lcov"); + } + + #[test] + fn read_lcov_defaults_to_full_when_no_line_records_exist() { + let path = temp_file_path("lcov_empty"); + write_file(&path, "TN:probe\n"); + let parsed = read_lcov(&path).expect("parse lcov"); + assert_eq!(parsed.executable_total, 0); + assert_eq!(parsed.executable_covered, 0); + assert_eq!(parsed.executable_percent, 100.0); + assert!(!parsed.branches_available); + assert_eq!(parsed.branch_percent, None); + fs::remove_file(path).expect("remove lcov"); + } + + #[test] + fn evaluate_gate_collects_all_failure_reasons() { + let summary = CoverageSummary { + functions_percent: 40.0, + summary_lines_percent: 50.0, + summary_regions_percent: 60.0, + }; + let lcov = LcovCoverage { + executable_total: 20, + executable_covered: 10, + executable_percent: 50.0, + executable_source: ExecutableSource::Da, + branch_total: 10, + branch_covered: 3, + branches_available: true, + branch_percent: Some(30.0), + }; + let thresholds = CoverageThresholds { + fail_under_exec_lines: 90.0, + fail_under_functions: 90.0, + fail_under_regions: 90.0, + fail_under_branches: 90.0, + require_branches: true, + }; + + let gate = evaluate_gate(&summary, &lcov, thresholds); + assert!(!gate.pass); + assert!( + gate.fail_reasons + .iter() + .any(|reason| reason.contains("executable_lines")) + ); + assert!( + gate.fail_reasons + .iter() + .any(|reason| reason.contains("functions")) + ); + assert!( + gate.fail_reasons + .iter() + .any(|reason| reason.contains("regions")) + ); + assert!( + gate.fail_reasons + .iter() + .any(|reason| reason.contains("branches")) + ); + } + + #[test] + fn run_command_covers_success_and_failure() { + let mut ok = Command::new("sh"); + ok.arg("-c").arg("exit 0"); + run_command(ok, "shell ok").expect("run ok command"); + + let mut fail = Command::new("sh"); + fail.arg("-c").arg("exit 9"); + let err = run_command(fail, "shell fail").expect_err("run failing command"); + assert!(err.contains("shell fail failed with status")); + + let missing = Command::new("/definitely/not/a/real/command"); + let err = run_command(missing, "shell missing").expect_err("missing command"); + assert!(err.contains("failed to run shell missing")); + } + + #[test] + fn apply_coverage_profile_flags_writes_expected_args() { + let profile = CoverageProfile { + no_default_features: true, + features: vec!["std".to_string(), "serde".to_string()], + test_threads: Some(2), + }; + let mut command = Command::new("cargo"); + apply_coverage_profile_flags(&mut command, &profile); + let args = command + .get_args() + .map(|arg| arg.to_string_lossy().to_string()) + .collect::<Vec<_>>(); + assert_eq!( + args, + vec![ + "--no-default-features".to_string(), + "--features".to_string(), + "std,serde".to_string() + ] + ); + } + + #[test] + fn run_crate_with_runner_builds_all_command_steps() { + let out = temp_dir_path("run_crate_runner"); + let args = vec![ + "--crate".to_string(), + "radroots_core".to_string(), + "--out".to_string(), + out.display().to_string(), + "--test-threads".to_string(), + "3".to_string(), + ]; + let mut names = Vec::new(); + let mut rendered_commands = Vec::new(); + let mut runner = |cmd: Command, name: &str| { + names.push(name.to_string()); + let rendered = cmd + .get_args() + .map(|arg| arg.to_string_lossy().to_string()) + .collect::<Vec<_>>() + .join(" "); + assert!(!rendered.is_empty()); + rendered_commands.push(rendered); + Ok(()) + }; + run_crate_with_runner(&args, &mut runner).expect("run crate with stub runner"); + assert_eq!( + names, + vec![ + "cargo llvm-cov clean --workspace".to_string(), + "cargo llvm-cov --no-report".to_string(), + "cargo llvm-cov report --json --summary-only".to_string(), + "cargo llvm-cov report --json".to_string(), + "cargo llvm-cov report --lcov".to_string(), + ] + ); + assert!( + rendered_commands + .iter() + .filter(|rendered| rendered.contains("report -p radroots_core")) + .all(|rendered| rendered.contains("--ignore-filename-regex")) + ); + assert!( + rendered_commands + .iter() + .filter(|rendered| rendered.contains("report -p radroots_core")) + .all(|rendered| rendered.contains(COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX)) + ); + fs::remove_dir_all(out).expect("remove run crate output dir"); + } + + #[test] + fn coverage_ignore_filename_regex_excludes_external_and_sibling_workspace_paths() { + let root = workspace_root(); + let ignore_regex = + coverage_ignore_filename_regex(&root, "radroots_core").expect("build ignore regex"); + assert!(ignore_regex.contains(COVERAGE_EXTERNAL_IGNORE_FILENAME_REGEX)); + assert!(ignore_regex.contains("crates/identity")); + assert!(ignore_regex.contains("crates/core/tests")); + assert!(!ignore_regex.contains("crates/core/src")); + } + + #[test] + fn escape_regex_literal_escapes_regex_metacharacters() { + let escaped = escape_regex_literal(r"\.+*?()|[]{}^$"); + assert_eq!(escaped, r"\\\.\+\*\?\(\)\|\[\]\{\}\^\$"); + } + + #[test] + fn coverage_cargo_command_defaults_to_rustup_nightly() { + let cmd = coverage_cargo_command_with_override(None); + let mut args = Vec::new(); + for arg in cmd.get_args() { + args.push(arg.to_string_lossy().to_string()); + } + + assert_eq!(cmd.get_program().to_string_lossy(), "rustup"); + assert_eq!( + args, + vec![ + "run".to_string(), + "nightly".to_string(), + "cargo".to_string() + ] + ); + } + + #[test] + fn normalized_coverage_cargo_override_trims_and_filters_values() { + assert_eq!( + normalized_coverage_cargo_override(Some(" /tmp/cargo ".to_string())), + Some("/tmp/cargo".to_string()) + ); + assert_eq!( + normalized_coverage_cargo_override(Some(" ".to_string())), + None + ); + assert_eq!(normalized_coverage_cargo_override(None), None); + } + + fn assert_coverage_command_shapes( + cargo_cmd: Command, + llvm_cov_cmd: Command, + override_binary: Option<&str>, + ) { + match override_binary { + Some(binary) => assert_eq!(cargo_cmd.get_program().to_string_lossy(), binary), + None => assert_eq!(cargo_cmd.get_program().to_string_lossy(), "rustup"), + } + + let llvm_args = llvm_cov_cmd + .get_args() + .map(|arg| arg.to_string_lossy().to_string()) + .collect::<Vec<_>>(); + match override_binary { + Some(_) => assert_eq!(llvm_args, vec!["llvm-cov".to_string()]), + None => assert_eq!( + llvm_args, + vec![ + "run".to_string(), + "nightly".to_string(), + "cargo".to_string(), + "llvm-cov".to_string() + ] + ), + } + } + + #[test] + fn coverage_public_command_helpers_match_current_env_resolution() { + let mut default_llvm_cov_cmd = coverage_cargo_command_with_override(None); + default_llvm_cov_cmd.arg("llvm-cov"); + assert_coverage_command_shapes( + coverage_cargo_command_with_override(None), + default_llvm_cov_cmd, + None, + ); + + let explicit_binary = temp_dir_path("coverage_command_override") + .join("nightly-cargo") + .to_string_lossy() + .to_string(); + let mut explicit_llvm_cov_cmd = + coverage_cargo_command_with_override(Some(&explicit_binary)); + explicit_llvm_cov_cmd.arg("llvm-cov"); + assert_coverage_command_shapes( + coverage_cargo_command_with_override(Some(&explicit_binary)), + explicit_llvm_cov_cmd, + Some(explicit_binary.as_str()), + ); + + let override_binary = + normalized_coverage_cargo_override(std::env::var("RADROOTS_COVERAGE_CARGO").ok()); + assert_coverage_command_shapes( + coverage_cargo_command(), + coverage_llvm_cov_command(), + override_binary.as_deref(), + ); + } + + #[test] + fn configure_coverage_toolchain_env_sets_existing_binary_envs() { + let toolchain_dir = temp_dir_path("coverage_toolchain_env"); + fs::create_dir_all(&toolchain_dir).expect("create toolchain env dir"); + for binary in ["rustc", "rustdoc", "llvm-cov", "llvm-profdata"] { + write_file(&toolchain_dir.join(binary), ""); + } + + let mut cmd = Command::new("cargo"); + configure_coverage_toolchain_env(&mut cmd, &toolchain_dir); + let envs = collect_command_envs(&cmd); + assert_eq!( + envs.get("RUSTC"), + Some(&Some( + toolchain_dir.join("rustc").to_string_lossy().to_string() + )) + ); + assert_eq!( + envs.get("RUSTDOC"), + Some(&Some( + toolchain_dir.join("rustdoc").to_string_lossy().to_string() + )) + ); + assert_eq!( + envs.get("LLVM_COV"), + Some(&Some( + toolchain_dir.join("llvm-cov").to_string_lossy().to_string() + )) + ); + assert_eq!( + envs.get("LLVM_PROFDATA"), + Some(&Some( + toolchain_dir + .join("llvm-profdata") + .to_string_lossy() + .to_string() + )) + ); + + fs::remove_dir_all(toolchain_dir).expect("remove toolchain env dir"); + } + + #[test] + fn configure_coverage_toolchain_env_skips_missing_binary_envs() { + let toolchain_dir = temp_dir_path("coverage_toolchain_missing_env"); + fs::create_dir_all(&toolchain_dir).expect("create missing env dir"); + + let mut cmd = Command::new("cargo"); + configure_coverage_toolchain_env(&mut cmd, &toolchain_dir); + let envs = collect_command_envs(&cmd); + assert!(!envs.contains_key("RUSTC")); + assert!(!envs.contains_key("RUSTDOC")); + assert!(!envs.contains_key("LLVM_COV")); + assert!(!envs.contains_key("LLVM_PROFDATA")); + + fs::remove_dir_all(toolchain_dir).expect("remove missing env dir"); + } + + #[test] + fn coverage_cargo_command_override_variants_cover_parented_and_parentless_paths() { + let toolchain_dir = temp_dir_path("coverage_toolchain_override"); + fs::create_dir_all(&toolchain_dir).expect("create toolchain dir"); + for binary in [ + "nightly-cargo", + "rustc", + "rustdoc", + "llvm-cov", + "llvm-profdata", + ] { + write_file(&toolchain_dir.join(binary), ""); + } + + let default_cmd = coverage_cargo_command_with_override(None); + let mut args = Vec::new(); + for arg in default_cmd.get_args() { + args.push(arg.to_string_lossy().to_string()); + } + assert_eq!(default_cmd.get_program().to_string_lossy(), "rustup"); + assert_eq!( + args, + vec![ + "run".to_string(), + "nightly".to_string(), + "cargo".to_string() + ] + ); + + let override_binary = toolchain_dir.join("nightly-cargo"); + let cmd = coverage_cargo_command_with_override(Some( + override_binary + .to_str() + .expect("override path should be utf-8"), + )); + + assert_eq!( + cmd.get_program().to_string_lossy(), + override_binary.to_string_lossy() + ); + assert!(cmd.get_args().next().is_none()); + let mut envs = collect_command_envs(&cmd); + envs.insert("MISSING".to_string(), None); + assert_eq!( + envs.get("RUSTC"), + Some(&Some( + toolchain_dir.join("rustc").to_string_lossy().to_string() + )) + ); + assert_eq!( + envs.get("RUSTDOC"), + Some(&Some( + toolchain_dir.join("rustdoc").to_string_lossy().to_string() + )) + ); + assert_eq!( + envs.get("LLVM_COV"), + Some(&Some( + toolchain_dir.join("llvm-cov").to_string_lossy().to_string() + )) + ); + assert_eq!( + envs.get("LLVM_PROFDATA"), + Some(&Some( + toolchain_dir + .join("llvm-profdata") + .to_string_lossy() + .to_string() + )) + ); + let path_env = envs + .get("PATH") + .and_then(|value| value.as_ref()) + .expect("override binary should prepend PATH"); + assert!(path_env.starts_with(toolchain_dir.to_string_lossy().as_ref())); + let mut cmd = coverage_cargo_command_with_override(Some("/")); + cmd.env_remove("RUSTC"); + cmd.env_remove("LLVM_COV"); + assert_eq!(cmd.get_program().to_string_lossy(), "/"); + let envs = collect_command_envs(&cmd); + assert_eq!(envs.get("RUSTC"), Some(&None)); + assert_eq!(envs.get("LLVM_COV"), Some(&None)); + + fs::remove_dir_all(toolchain_dir).expect("remove toolchain dir"); + } + + #[test] + fn workspace_root_override_takes_precedence() { + let root = workspace_root_with_override(Some("/tmp/radroots-coverage-root")); + assert_eq!(root, PathBuf::from("/tmp/radroots-coverage-root")); + + let fallback = workspace_root_with_override(Some("")); + assert!(fallback.join("Cargo.toml").exists()); + + let default_root = workspace_root_with_override(None); + assert!(default_root.join("Cargo.toml").exists()); + } + + #[test] + fn prepend_toolchain_bin_to_path_covers_missing_and_existing_path_inputs() { + let toolchain_dir = PathBuf::from("/tmp/radroots-coverage-toolchain"); + let no_path = prepend_toolchain_bin_to_path(&toolchain_dir, None); + assert_eq!(no_path, OsString::from(&toolchain_dir)); + + let joined = + prepend_toolchain_bin_to_path(&toolchain_dir, Some(OsString::from("/usr/bin:/bin"))); + let joined = joined.to_string_lossy().to_string(); + assert!(joined.starts_with("/tmp/radroots-coverage-toolchain")); + assert!(joined.contains("/usr/bin")); + } + + #[test] + fn collect_command_envs_cover_helper_paths() { + let mut cmd = Command::new("sh"); + cmd.env("PRESENT", "value"); + cmd.env_remove("REMOVED"); + let envs = collect_command_envs(&cmd); + assert_eq!(envs.get("PRESENT"), Some(&Some("value".to_string()))); + assert_eq!(envs.get("REMOVED"), Some(&None)); + } + + #[test] + fn ok_runner_helper_returns_success() { + let cmd = Command::new("true"); + assert!(ok_runner(cmd, "noop").is_ok()); + } + + #[test] + fn run_crate_with_runner_uses_default_output_dir_when_out_is_missing() { + let args = vec!["--crate".to_string(), "radroots_core".to_string()]; + let mut output_path_seen = false; + let mut runner = |cmd: Command, _: &str| { + let rendered = cmd + .get_args() + .map(|arg| arg.to_string_lossy().to_string()) + .collect::<Vec<_>>(); + if rendered + .iter() + .any(|arg| arg.ends_with("coverage-summary.json")) + || rendered + .iter() + .any(|arg| arg.ends_with("coverage-details.json")) + || rendered + .iter() + .any(|arg| arg.ends_with("coverage-lcov.info")) + { + output_path_seen = true; + } + Ok(()) + }; + run_crate_with_runner(&args, &mut runner).expect("run crate with default out"); + assert!(output_path_seen); + } + + #[test] + fn run_crate_with_runner_propagates_runner_failures() { + let out = temp_dir_path("run_crate_runner_fail"); + let args = vec![ + "--crate".to_string(), + "radroots_core".to_string(), + "--out".to_string(), + out.display().to_string(), + ]; + let mut runner = |_: Command, _: &str| Err("runner failed".to_string()); + let err = + run_crate_with_runner(&args, &mut runner).expect_err("runner failure should bubble up"); + assert_eq!(err, "runner failed".to_string()); + fs::remove_dir_all(out).expect("remove run crate failure output dir"); + let root = temp_dir_path("run_crate_create_out_error"); + write_file(&root.join("blocker"), "x"); + let args = vec![ + "--crate".to_string(), + "radroots_core".to_string(), + "--out".to_string(), + root.join("blocker").join("nested").display().to_string(), + ]; + let mut runner = run_command; + let err = run_crate_with_runner(&args, &mut runner) + .expect_err("output dir create error should fail"); + assert!(err.contains("failed to create")); + fs::remove_dir_all(root).expect("remove run crate create error root"); + } + + #[test] + fn run_crate_wrapper_returns_missing_crate_error_without_running_commands() { + let err = run_crate(&[]).expect_err("missing crate flag"); + assert!(err.contains("missing --crate")); + } + + #[test] + fn run_crate_with_runner_at_root_covers_profile_and_runner_error_paths() { + let write_minimal_workspace = |root: &Path| { + write_file( + &root.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/core\"]\n", + ); + write_file( + &root.join("crates").join("core").join("Cargo.toml"), + "[package]\nname = \"radroots_core\"\nversion = \"0.1.0-alpha.2\"\nedition = \"2024\"\n", + ); + }; + + let profile_root = temp_dir_path("run_crate_profile_invalid"); + write_minimal_workspace(&profile_root); + write_file( + &profile_root + .join("contracts") + .join("coverage-profiles.toml"), + "[profiles.default]\nfeatures = [\"\"]\n", + ); + let profile_args = vec![ + "--crate".to_string(), + "radroots_core".to_string(), + "--out".to_string(), + profile_root.join("out").display().to_string(), + ]; + let mut runner = run_command; + let profile_err = run_crate_with_runner_at_root(&profile_args, &profile_root, &mut runner) + .expect_err("invalid profile should fail"); + assert!(profile_err.contains("empty feature value")); + fs::remove_dir_all(&profile_root).expect("remove profile root"); + + let thread_root = temp_dir_path("run_crate_bad_threads"); + fs::create_dir_all(&thread_root).expect("create thread root"); + write_minimal_workspace(&thread_root); + let thread_args = vec![ + "--crate".to_string(), + "radroots_core".to_string(), + "--out".to_string(), + thread_root.join("out").display().to_string(), + "--test-threads".to_string(), + "bad".to_string(), + ]; + let mut runner = run_command; + let thread_err = run_crate_with_runner_at_root(&thread_args, &thread_root, &mut runner) + .expect_err("invalid test threads should fail"); + assert!(thread_err.contains("invalid --test-threads value")); + fs::remove_dir_all(&thread_root).expect("remove thread root"); + + for fail_step in [2usize, 3usize, 4usize] { + let step_root = temp_dir_path("run_crate_step_fail"); + write_minimal_workspace(&step_root); + let step_args = vec![ + "--crate".to_string(), + "radroots_core".to_string(), + "--out".to_string(), + step_root.join("out").display().to_string(), + ]; + let mut calls = 0usize; + let mut runner = |_: Command, name: &str| { + calls += 1; + if calls == fail_step { + return Err(format!("runner failure at {name}")); + } + Ok(()) + }; + let err = run_crate_with_runner_at_root(&step_args, &step_root, &mut runner) + .expect_err("runner should fail at selected step"); + assert!(err.contains("runner failure at")); + fs::remove_dir_all(&step_root).expect("remove step root"); + } + } + + #[test] + fn report_gate_writes_report_file_on_success() { + let root = temp_dir_path("report_gate_success"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); + + let args = vec![ + "--scope".to_string(), + "crate-x".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--policy-gate".to_string(), + ]; + report_gate(&args).expect("report gate success"); + let report_raw = fs::read_to_string(&out_path).expect("read report"); + assert!(report_raw.contains("\"scope\": \"crate-x\"")); + assert!(report_raw.contains("\"regions\": 98.0")); + assert!(report_raw.contains("\"pass\": true")); + fs::remove_dir_all(root).expect("remove report gate success root"); + } + + #[test] + fn report_gate_normalizes_duplicate_generic_records_from_details() { + let root = temp_dir_path("report_gate_normalized_generics"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{ + "data": [ + { + "totals": { + "functions": {"percent": 96.0}, + "lines": {"percent": 99.0}, + "regions": {"percent": 22.0} + } + } + ] +}"#, + ); + write_file( + &root.join("coverage-details.json"), + r#"{ + "data": [ + { + "functions": [ + { + "count": 4, + "filenames": ["/tmp/crates/runtime_manager/src/lib.rs"], + "regions": [ + [10, 1, 12, 2, 4, 0, 0, 0], + [13, 1, 13, 8, 4, 0, 0, 0] + ] + }, + { + "count": 0, + "filenames": ["/tmp/crates/runtime_manager/src/lib.rs"], + "regions": [ + [10, 1, 12, 2, 0, 0, 0, 0], + [13, 1, 13, 8, 0, 0, 0, 0] + ] + } + ] + } + ] +}"#, + ); + write_file( + &lcov_path, + "DA:1,1\nDA:2,0\nLF:2\nLH:1\nBRDA:1,0,0,1\nBRDA:2,0,0,0\n", + ); + + let args = vec![ + "--scope".to_string(), + "radroots_runtime_manager".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-exec-lines".to_string(), + "50.0".to_string(), + "--fail-under-functions".to_string(), + "100.0".to_string(), + "--fail-under-regions".to_string(), + "100.0".to_string(), + "--fail-under-branches".to_string(), + "50.0".to_string(), + ]; + report_gate(&args).expect("normalized report gate success"); + + let report_raw = fs::read_to_string(&out_path).expect("read normalized report"); + assert!(report_raw.contains("\"functions_percent\": 100.0")); + assert!(report_raw.contains("\"summary_regions_percent\": 100.0")); + assert!(report_raw.contains("\"pass\": true")); + + fs::remove_dir_all(root).expect("remove normalized report gate root"); + } + + #[test] + fn report_gate_with_root_uses_scope_specific_override_thresholds() { + let root = temp_dir_path("report_gate_override_success"); + let coverage_dir = root.join("contracts"); + fs::create_dir_all(&coverage_dir).expect("create coverage dir"); + write_file( + &coverage_dir.join("coverage.toml"), + "[gate]\nfail_under_exec_lines = 100.0\nfail_under_functions = 100.0\nfail_under_regions = 100.0\nfail_under_branches = 100.0\nrequire_branches = true\n\n[required]\ncrates = [\"radroots_a\"]\n\n[overrides.radroots_a]\nfail_under_exec_lines = 88.5\nfail_under_functions = 77.5\nfail_under_regions = 66.5\nfail_under_branches = 55.5\nrequire_branches = false\ntemporary = true\nreason = \"temporary publish unblocker\"\n", + ); + + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":80.0},"lines":{"percent":88.5},"regions":{"percent":70.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,1\nLF:1\nLH:1\nBRDA:1,0,0,1\n"); + + report_gate_with_root( + &[ + "--scope".to_string(), + "radroots_a".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--policy-gate".to_string(), + ], + &root, + ) + .expect("report gate should honor override"); + + let report_raw = fs::read_to_string(&out_path).expect("read override report"); + assert!(report_raw.contains("\"functions\": 77.5")); + assert!(report_raw.contains("\"regions\": 66.5")); + assert!(report_raw.contains("\"branches_required\": false")); + assert!(report_raw.contains("\"pass\": true")); + + fs::remove_dir_all(root).expect("remove report gate override root"); + } + + #[test] + fn report_gate_returns_error_on_failed_thresholds() { + let root = temp_dir_path("report_gate_fail"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":10.0},"lines":{"percent":10.0},"regions":{"percent":10.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,0\nBRDA:1,0,0,0\n"); + + let args = vec![ + "--scope".to_string(), + "crate-y".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-exec-lines".to_string(), + "100.0".to_string(), + "--fail-under-functions".to_string(), + "100.0".to_string(), + "--fail-under-regions".to_string(), + "100.0".to_string(), + "--fail-under-branches".to_string(), + "100.0".to_string(), + ]; + let err = report_gate(&args).expect_err("report gate failure"); + assert!(err.contains("coverage gate failed")); + fs::remove_dir_all(root).expect("remove report gate failure root"); + } + + #[test] + fn report_gate_handles_nan_threshold_input() { + let root = temp_dir_path("report_gate_nan"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); + + let args = vec![ + "--scope".to_string(), + "crate-nan".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-functions".to_string(), + "NaN".to_string(), + ]; + let err = report_gate(&args).expect_err("nan threshold should fail coverage gate"); + assert!(err.contains("invalid --fail-under-functions value")); + fs::remove_dir_all(root).expect("remove report gate nan root"); + } + + #[test] + fn report_gate_reports_write_failure() { + let root = temp_dir_path("report_gate_write_fail"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); + fs::create_dir_all(&out_path).expect("create directory at output path"); + + let args = vec![ + "--scope".to_string(), + "crate-write".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--policy-gate".to_string(), + ]; + let err = report_gate(&args).expect_err("writing report to directory should fail"); + assert!(err.contains("failed to write")); + fs::remove_dir_all(root).expect("remove report gate write root"); + } + + #[test] + fn report_gate_logs_branch_unavailable_path() { + let root = temp_dir_path("report_gate_no_branches"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,1\n"); + + let args = vec![ + "--scope".to_string(), + "crate-no-branch".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-exec-lines".to_string(), + "100.0".to_string(), + "--fail-under-functions".to_string(), + "100.0".to_string(), + "--fail-under-regions".to_string(), + "100.0".to_string(), + "--fail-under-branches".to_string(), + "100.0".to_string(), + ]; + report_gate(&args).expect("report gate no branches"); + let report_raw = fs::read_to_string(&out_path).expect("read report"); + assert!(report_raw.contains("\"branches_available\": false")); + fs::remove_dir_all(root).expect("remove no branch report root"); + } + + #[test] + fn report_gate_reports_argument_and_input_errors() { + let missing_scope = report_gate(&[]).expect_err("missing scope"); + assert!(missing_scope.contains("missing --scope")); + + let missing_summary = report_gate(&["--scope".to_string(), "crate".to_string()]) + .expect_err("missing summary"); + assert!(missing_summary.contains("missing --summary")); + + let missing_lcov = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + "summary.json".to_string(), + ]) + .expect_err("missing lcov"); + assert!(missing_lcov.contains("missing --lcov")); + + let missing_out = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + "summary.json".to_string(), + "--lcov".to_string(), + "coverage.info".to_string(), + ]) + .expect_err("missing out"); + assert!(missing_out.contains("missing --out")); + + let root = temp_dir_path("report_gate_arg_errors"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); + + let invalid_functions = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-functions".to_string(), + "bad".to_string(), + ]) + .expect_err("invalid functions threshold"); + assert!(invalid_functions.contains("invalid --fail-under-functions value")); + + let invalid_exec = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-exec-lines".to_string(), + "bad".to_string(), + ]) + .expect_err("invalid executable threshold"); + assert!(invalid_exec.contains("invalid --fail-under-exec-lines value")); + + let invalid_regions = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-regions".to_string(), + "bad".to_string(), + ]) + .expect_err("invalid regions threshold"); + assert!(invalid_regions.contains("invalid --fail-under-regions value")); + + let invalid_branches = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-branches".to_string(), + "bad".to_string(), + ]) + .expect_err("invalid branches threshold"); + assert!(invalid_branches.contains("invalid --fail-under-branches value")); + + let missing_thresholds = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + ]) + .expect_err("missing thresholds"); + assert!(missing_thresholds.contains("missing coverage thresholds")); + + let missing_functions = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-exec-lines".to_string(), + "100".to_string(), + ]) + .expect_err("missing functions threshold"); + assert!(missing_functions.contains("missing coverage thresholds")); + + let missing_regions = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-exec-lines".to_string(), + "100".to_string(), + "--fail-under-functions".to_string(), + "100".to_string(), + ]) + .expect_err("missing regions threshold"); + assert!(missing_regions.contains("missing coverage thresholds")); + + let missing_branches = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--fail-under-exec-lines".to_string(), + "100".to_string(), + "--fail-under-functions".to_string(), + "100".to_string(), + "--fail-under-regions".to_string(), + "100".to_string(), + ]) + .expect_err("missing branches threshold"); + assert!(missing_branches.contains("missing coverage thresholds")); + + let missing_summary_file = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + root.join("missing-summary.json").display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--policy-gate".to_string(), + ]) + .expect_err("missing summary file should fail"); + assert!(missing_summary_file.contains("failed to read summary")); + + let missing_gate_report = read_gate_report(&root.join("missing-gate-report.json")) + .expect_err("missing gate report should fail"); + assert!(missing_gate_report.contains("failed to read gate report")); + + write_file(&out_path, "{not-json"); + let invalid_gate_report = read_gate_report(&out_path).expect_err("invalid gate report"); + assert!(invalid_gate_report.contains("failed to parse gate report")); + + let missing_lcov_file = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + root.join("missing-lcov.info").display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--policy-gate".to_string(), + ]) + .expect_err("missing lcov file should fail"); + assert!(missing_lcov_file.contains("failed to read lcov")); + + let mixed_policy_gate = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--policy-gate".to_string(), + "--fail-under-functions".to_string(), + "100.0".to_string(), + ]) + .expect_err("policy gate mixed with explicit thresholds"); + assert!(mixed_policy_gate.contains("cannot be combined")); + + let mixed_policy_gate_regions = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--policy-gate".to_string(), + "--fail-under-regions".to_string(), + "100.0".to_string(), + ]) + .expect_err("policy gate mixed with regions threshold"); + assert!(mixed_policy_gate_regions.contains("cannot be combined")); + + let mixed_policy_gate_branches_flag = report_gate(&[ + "--scope".to_string(), + "crate".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--policy-gate".to_string(), + "--require-branches".to_string(), + ]) + .expect_err("policy gate mixed with require-branches"); + assert!(mixed_policy_gate_branches_flag.contains("cannot be combined")); + + fs::remove_dir_all(root).expect("remove report arg errors root"); + } + + #[test] + fn coverage_ignore_filename_regex_reports_unknown_crate() { + let root = temp_dir_path("coverage_unknown_crate_root"); + write_file( + &root.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/core\"]\n", + ); + write_file( + &root.join("crates").join("core").join("Cargo.toml"), + "[package]\nname = \"radroots_core\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ); + + let err = coverage_ignore_filename_regex(&root, "radroots_missing") + .expect_err("unknown crate should fail"); + assert!(err.contains("could not resolve crate directory")); + + fs::remove_dir_all(root).expect("remove unknown crate root"); + } + + #[test] + fn coverage_ignore_filename_regex_reports_workspace_manifest_errors() { + let root = temp_dir_path("coverage_regex_workspace_error_root"); + let read_err = coverage_ignore_filename_regex(&root, "radroots_core") + .expect_err("missing workspace manifest should fail"); + assert!(read_err.contains("failed to read")); + + write_file(&root.join("Cargo.toml"), "[workspace"); + let parse_err = coverage_ignore_filename_regex(&root, "radroots_core") + .expect_err("invalid workspace manifest should fail"); + assert!(parse_err.contains("failed to parse")); + + fs::remove_dir_all(root).expect("remove workspace error root"); + } + + #[test] + fn run_crate_with_runner_at_root_reports_ignore_filter_errors() { + let root = temp_dir_path("run_crate_ignore_filter_error"); + write_file( + &root.join("Cargo.toml"), + "[workspace]\nmembers = [\"crates/other\"]\n", + ); + write_file( + &root.join("crates").join("other").join("Cargo.toml"), + "[package]\nname = \"radroots_other\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ); + let args = vec![ + "--crate".to_string(), + "radroots_core".to_string(), + "--out".to_string(), + root.join("target").join("coverage").display().to_string(), + ]; + let mut runner = ok_runner; + let err = run_crate_with_runner_at_root(&args, &root, &mut runner) + .expect_err("missing crate coverage filter should fail"); + assert!(err.contains("could not resolve crate directory")); + + fs::remove_dir_all(root).expect("remove run crate ignore filter root"); + } + + #[test] + fn run_dispatches_subcommands_and_errors() { + run(&["help".to_string()]).expect("help subcommand"); + run(&["required-crates".to_string()]).expect("required crates subcommand"); + run(&["workspace-crates".to_string()]).expect("workspace crates subcommand"); + let run_crate_err = run(&["run-crate".to_string()]).expect_err("run crate missing args"); + assert!(run_crate_err.contains("missing --crate")); + let unknown_err = run(&["unknown".to_string()]).expect_err("unknown subcommand"); + assert!(unknown_err.contains("unknown coverage subcommand")); + let missing_err = run(&[]).expect_err("missing subcommand"); + assert!(missing_err.contains("missing coverage subcommand")); + } + + #[test] + fn list_root_helpers_report_missing_contract_files() { + let root = temp_dir_path("list_helper_missing"); + fs::create_dir_all(&root).expect("create list helper root"); + let mut output = Vec::new(); + let required_err = list_required_crates_with_root(&root, &mut output) + .expect_err("missing required crates file should fail"); + assert!(required_err.contains("failed to read coverage policy")); + + let workspace_err = list_workspace_crates_with_root(&root, &mut output) + .expect_err("missing workspace manifest should fail"); + assert!(workspace_err.contains("failed to read")); + + fs::remove_dir_all(root).expect("remove list helper root"); + } + + #[test] + fn write_crate_names_output_covers_success_and_error_paths() { + let mut output = Vec::new(); + write_crate_names_output( + &mut output, + vec!["radroots_a".to_string(), "radroots_b".to_string()], + "required crates", + ) + .expect("write crate names"); + let rendered = String::from_utf8(output).expect("utf8"); + assert!(rendered.contains("radroots_a")); + assert!(rendered.contains("radroots_b")); + + let mut failing = FailingWriter; + let err = write_crate_names_output( + &mut failing, + vec!["radroots_a".to_string()], + "workspace crates", + ) + .expect_err("writer failure"); + assert!(err.contains("failed to write workspace crates output")); + failing.flush().expect("flush failing writer"); + } + + #[test] + fn run_report_subcommand_dispatches_to_report_gate() { + let root = temp_dir_path("run_dispatch_report"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); + + run(&[ + "report".to_string(), + "--scope".to_string(), + "dispatch".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--policy-gate".to_string(), + ]) + .expect("dispatch report"); + assert!(out_path.exists()); + fs::remove_dir_all(root).expect("remove report dispatch root"); + } + + #[test] + fn report_gate_with_root_reports_policy_read_errors() { + let root = temp_dir_path("report_gate_policy_root_error"); + let summary_path = root.join("summary.json"); + let lcov_path = root.join("coverage.info"); + let out_path = root.join("gate-report.json"); + write_file( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, + ); + write_file(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n"); + + let err = report_gate_with_root( + &[ + "--scope".to_string(), + "crate-x".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + out_path.display().to_string(), + "--policy-gate".to_string(), + ], + &root, + ) + .expect_err("missing policy should fail"); + assert!(err.contains("failed to read coverage policy")); + + fs::remove_dir_all(root).expect("remove report gate policy error root"); + } +} diff --git a/tools/xtask/src/hygiene.rs b/tools/xtask/src/hygiene.rs @@ -0,0 +1,333 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +pub fn run(args: &[String], root: &Path) -> Result<(), String> { + match args.first().map(String::as_str) { + Some("forbidden-identifiers") => validate_forbidden_identifiers(root), + _ => Err("unknown hygiene subcommand".to_string()), + } +} + +pub fn validate_forbidden_identifiers(root: &Path) -> Result<(), String> { + let mut failures = Vec::new(); + reject_substrings( + root, + &[PathBuf::from("crates/relay_transport/src")], + &["RadrootsEventIngest::verified"], + "relay fetch must not bypass event-store verification", + &[], + &mut failures, + ); + reject_substrings( + root, + &[PathBuf::from("crates/event_store/src")], + &["last_created_at", "last_event_id"], + "event-store projection cursors must use last_event_seq", + &[], + &mut failures, + ); + reject_raw_protocol_strings(root, &mut failures); + reject_substrings( + root, + &[ + PathBuf::from("crates/events/src"), + PathBuf::from("crates/events_codec/src"), + PathBuf::from("crates/trade/src"), + ], + &[ + "RadrootsTradeMessageType", + "RadrootsTradeEnvelope", + "RadrootsTradeMessagePayload", + "RadrootsTradeQuestion", + "RadrootsTradeAnswer", + "RadrootsTradeDiscount", + "RadrootsTradeOrder", + "RadrootsActiveOrder", + "RadrootsActiveTrade", + "RadrootsTradeListingParseError", + "RadrootsTradeDomain", + "radroots_sdk::trade::", + "TradeListingParseError", + "TradeListingEnvelope", + "TradeListingMessage", + "KIND_TRADE_ORDER", + "TRADE_LISTING_KINDS", + "build_envelope_draft", + "parse_envelope", + "public_trade", + "events::trade::", + "events_codec::trade::", + "trade_order_economics_digest", + "trade_revision", + "trade_lifecycle", + "reduce_active_order", + "canonicalize_active_order", + "active_trade_", + "ActiveOrder", + "active_order", + "active order", + "active trade", + "RADROOTS_TRADE_LISTING_DOMAIN", + "RADROOTS_TRADE_ENVELOPE_VERSION", + ], + "legacy trade identifiers must not reappear", + &[], + &mut failures, + ); + reject_substrings( + root, + &[PathBuf::from("crates"), PathBuf::from("contracts")], + &[ + "KIND_TRADE_LISTING_ORDER", + "KIND_TRADE_LISTING_QUESTION", + "KIND_TRADE_LISTING_ANSWER", + "KIND_TRADE_LISTING_DISCOUNT", + "KIND_TRADE_LISTING_CANCEL", + "KIND_TRADE_LISTING_FULFILLMENT", + "KIND_TRADE_LISTING_RECEIPT", + ], + "legacy trade listing kind constants must not reappear", + &[], + &mut failures, + ); + reject_substrings( + root, + &[ + PathBuf::from("crates"), + PathBuf::from("contracts"), + PathBuf::from("tools"), + PathBuf::from("build"), + ], + &["tangle"], + "legacy identifier 'tangle' must not reappear", + &["tools/xtask/src/hygiene.rs"], + &mut failures, + ); + + if failures.is_empty() { + println!("forbidden identifier hygiene passed"); + Ok(()) + } else { + Err(format!( + "forbidden identifier hygiene violations:\n{}", + failures.join("\n") + )) + } +} + +fn reject_substrings( + root: &Path, + rel_roots: &[PathBuf], + patterns: &[&str], + label: &str, + ignored_rel_paths: &[&str], + failures: &mut Vec<String>, +) { + for file in files_under(root, rel_roots) { + let rel = display_path(root, &file); + if ignored_rel_paths.contains(&rel.as_str()) { + continue; + } + let Ok(content) = fs::read_to_string(&file) else { + continue; + }; + for (line_index, line) in content.lines().enumerate() { + for pattern in patterns { + if line.contains(pattern) { + failures.push(format!( + "{label}: {}:{}: {}", + rel, + line_index + 1, + line.trim() + )); + } + } + } + } +} + +fn reject_raw_protocol_strings(root: &Path, failures: &mut Vec<String>) { + let rel_roots = [ + PathBuf::from("crates/events/src"), + PathBuf::from("crates/events_codec/src"), + PathBuf::from("crates/trade/src"), + ]; + for file in files_under(root, &rel_roots) { + let Ok(content) = fs::read_to_string(&file) else { + continue; + }; + let mut struct_name = String::new(); + for (line_index, line) in content.lines().enumerate() { + let trimmed = line.trim(); + if let Some(rest) = trimmed.strip_prefix("pub struct ") { + struct_name = rest + .split(['<', '{', ' ', '(']) + .next() + .unwrap_or_default() + .to_owned(); + } + if trimmed == "}" { + struct_name.clear(); + } + if is_raw_protocol_field(trimmed) && !is_allowed_raw_boundary(&struct_name) { + failures.push(format!( + "raw commercial protocol identifier String fields are forbidden: {}:{}: {}", + display_path(root, &file), + line_index + 1, + trimmed + )); + } + } + } +} + +fn is_raw_protocol_field(line: &str) -> bool { + [ + "pub order_id: String,", + "pub listing_addr: String,", + "pub revision_id: String,", + "pub quote_id: String,", + "pub primary_bin_id: String,", + "pub bin_id: String,", + "pub economics_digest: String,", + ] + .contains(&line) +} + +fn is_allowed_raw_boundary(struct_name: &str) -> bool { + struct_name == "RadrootsOrderEnvelope" + || struct_name == "RadrootsValidationReceiptTags" + || struct_name == "RadrootsTradeListing" + || struct_name.ends_with("Projection") + || struct_name.ends_with("Accounting") + || struct_name.ends_with("Availability") + || struct_name.ends_with("Reservation") + || struct_name.ends_with("Issue") + || struct_name.ends_with("NormalizedInventoryCount") +} + +fn files_under(root: &Path, rel_roots: &[PathBuf]) -> Vec<PathBuf> { + let mut files = Vec::new(); + for rel_root in rel_roots { + collect_files(root.join(rel_root), &mut files); + } + files.sort(); + files +} + +fn collect_files(path: PathBuf, files: &mut Vec<PathBuf>) { + let Ok(metadata) = fs::metadata(&path) else { + return; + }; + if metadata.is_file() { + if matches!( + path.extension().and_then(|ext| ext.to_str()), + Some("json" | "md" | "nix" | "rs" | "sh" | "sql" | "toml") + ) { + files.push(path); + } + return; + } + let Ok(entries) = fs::read_dir(path) else { + return; + }; + for entry in entries.flatten() { + collect_files(entry.path(), files); + } +} + +fn display_path(root: &Path, file: &Path) -> String { + file.strip_prefix(root) + .unwrap_or(file) + .to_string_lossy() + .to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn unique_temp_dir(prefix: &str) -> PathBuf { + let ns = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time") + .as_nanos(); + std::env::temp_dir().join(format!("radroots_xtask_hygiene_{prefix}_{ns}")) + } + + fn write_file(root: &Path, rel: &str, content: &str) { + let path = root.join(rel); + fs::create_dir_all(path.parent().expect("parent")).expect("create parent"); + fs::write(path, content).expect("write"); + } + + #[test] + fn forbidden_identifiers_accept_clean_synthetic_tree() { + let root = unique_temp_dir("clean"); + write_file( + &root, + "crates/relay_transport/src/fetch.rs", + "fn fetch() { let _ = RadrootsEventIngest::new; }\n", + ); + write_file( + &root, + "crates/event_store/src/store.rs", + "pub struct RadrootsProjectionCursor { pub last_event_seq: i64 }\n", + ); + write_file( + &root, + "crates/trade/src/order.rs", + "pub struct RadrootsOrderProjection { pub order_id: RadrootsOrderId, }\n", + ); + validate_forbidden_identifiers(&root).expect("clean tree"); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn forbidden_identifiers_reject_regressions() { + let root = unique_temp_dir("dirty"); + write_file( + &root, + "crates/relay_transport/src/fetch.rs", + "fn fetch() { let _ = RadrootsEventIngest::verified; }\n", + ); + write_file( + &root, + "crates/event_store/src/store.rs", + "pub struct Cursor { pub last_event_id: String }\n", + ); + write_file( + &root, + "crates/trade/src/order.rs", + "pub struct BadOrder {\n pub order_id: String,\n}\n", + ); + write_file(&root, "contracts/events/social-events.md", "tangle\n"); + write_file( + &root, + "crates/events/src/kinds.rs", + "pub const KIND_TRADE_LISTING_ORDER: u64 = 1;\n", + ); + let err = validate_forbidden_identifiers(&root).expect_err("dirty tree"); + assert!(err.contains("relay fetch must not bypass event-store verification")); + assert!(err.contains("event-store projection cursors must use last_event_seq")); + assert!(err.contains("raw commercial protocol identifier String fields are forbidden")); + assert!(err.contains("legacy identifier 'tangle' must not reappear")); + assert!(err.contains("legacy trade listing kind constants must not reappear")); + let _ = fs::remove_dir_all(root); + } + + #[test] + fn run_dispatches_forbidden_identifiers() { + let root = unique_temp_dir("run"); + write_file( + &root, + "crates/relay_transport/src/fetch.rs", + "fn fetch() { let _ = RadrootsEventIngest::new; }\n", + ); + run(&["forbidden-identifiers".to_string()], &root).expect("hygiene run"); + let unknown = run(&["unknown".to_string()], &root).expect_err("unknown hygiene command"); + assert!(unknown.contains("unknown hygiene subcommand")); + let _ = fs::remove_dir_all(root); + } +} diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs @@ -0,0 +1,239 @@ +#![cfg_attr(coverage_nightly, feature(coverage_attribute))] +#![forbid(unsafe_code)] + +#[cfg_attr(coverage_nightly, coverage(off))] +mod contract; +mod coverage; +#[cfg_attr(coverage_nightly, coverage(off))] +mod hygiene; + +use std::env; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; + +fn usage() { + eprintln!("usage:"); + eprintln!(" cargo xtask contract validate"); + eprintln!(" cargo xtask release preflight"); + eprintln!(" cargo xtask coverage run-crate --crate <crate> [--out <dir>]"); + eprintln!(" cargo xtask coverage required-crates"); + eprintln!(" cargo xtask coverage workspace-crates"); + eprintln!( + " cargo xtask coverage report --scope <scope> --summary <file> --lcov <file> --out <file> [--policy-gate | (--fail-under-exec-lines <pct> --fail-under-functions <pct> --fail-under-regions <pct> --fail-under-branches <pct> [--require-branches])]" + ); + eprintln!( + " cargo xtask coverage report-missing --scope <scope> --out <file> --reason <reason>" + ); + eprintln!( + " cargo xtask coverage refresh-summary [--reports-root <dir>] [--out <file>] [--status-out <file>]" + ); + eprintln!(" cargo xtask hygiene forbidden-identifiers"); +} + +fn workspace_root_with_override(override_root: Option<&str>) -> PathBuf { + if let Some(raw) = override_root { + let trimmed = raw.trim(); + if !trimmed.is_empty() { + return PathBuf::from(trimmed); + } + } + let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let crates_dir = manifest_dir.parent().unwrap_or(manifest_dir); + let root = crates_dir.parent().unwrap_or(crates_dir); + root.to_path_buf() +} + +fn workspace_root() -> PathBuf { + let override_root = env::var("RADROOTS_WORKSPACE_ROOT").ok(); + workspace_root_with_override(override_root.as_deref()) +} + +fn validate_contract() -> Result<(), String> { + let root = workspace_root(); + contract::load_contract_bundle(&root) + .and_then(|bundle| contract::validate_contract_bundle(&bundle)) + .and_then(|_| contract::validate_canonical_event_boundary(&root)) +} + +#[cfg_attr(coverage_nightly, coverage(off))] +fn release_preflight() -> Result<(), String> { + contract::validate_release_preflight(&workspace_root()) +} + +fn run_release(args: &[String]) -> Result<(), String> { + match args.first().map(String::as_str) { + Some("preflight") => release_preflight(), + _ => Err("unknown release subcommand".to_string()), + } +} + +fn run_contract(args: &[String]) -> Result<(), String> { + match args.first().map(String::as_str) { + Some("validate") => validate_contract(), + _ => Err("unknown contract subcommand".to_string()), + } +} + +fn run(args: &[String]) -> Result<(), String> { + match args.first().map(String::as_str) { + Some("contract") => run_contract(&args[1..]), + Some("coverage") => coverage::run(&args[1..]), + Some("hygiene") => hygiene::run(&args[1..], &workspace_root()), + Some("release") => run_release(&args[1..]), + _ => Err("unknown command".to_string()), + } +} + +fn main_with_args(args: Vec<String>) -> ExitCode { + if args.is_empty() { + usage(); + return ExitCode::from(2); + } + match run(&args) { + Ok(()) => ExitCode::SUCCESS, + Err(err) => { + eprintln!("{err}"); + usage(); + ExitCode::from(2) + } + } +} + +#[cfg_attr(coverage_nightly, coverage(off))] +fn main() -> ExitCode { + main_with_args(env::args().skip(1).collect()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::sync::{Mutex, MutexGuard, OnceLock}; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn workspace_lock() -> &'static Mutex<()> { + static LOCK: OnceLock<Mutex<()>> = OnceLock::new(); + LOCK.get_or_init(|| Mutex::new(())) + } + + fn lock_workspace() -> MutexGuard<'static, ()> { + match workspace_lock().lock() { + Ok(guard) => guard, + Err(poison) => poison.into_inner(), + } + } + + fn unique_temp_dir(prefix: &str) -> PathBuf { + let ns = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time") + .as_nanos(); + std::env::temp_dir().join(format!("radroots_xtask_main_{prefix}_{ns}")) + } + + #[test] + fn workspace_root_resolves() { + let root = workspace_root(); + assert!(root.join("Cargo.toml").exists()); + } + + #[test] + fn workspace_root_override_takes_precedence() { + let root = workspace_root_with_override(Some("/tmp/radroots-test-root")); + assert_eq!(root, PathBuf::from("/tmp/radroots-test-root")); + + let fallback = workspace_root_with_override(Some(" ")); + assert!(fallback.join("Cargo.toml").exists()); + + let default_root = workspace_root_with_override(None); + assert!(default_root.join("Cargo.toml").exists()); + } + + #[test] + fn run_release_and_dispatchers_cover_error_paths() { + let unknown_release = + run_release(&["unknown".to_string()]).expect_err("unknown release subcommand"); + assert!(unknown_release.contains("unknown release subcommand")); + + let unknown_contract = + run_contract(&["unknown".to_string()]).expect_err("unknown contract subcommand"); + assert!(unknown_contract.contains("unknown contract subcommand")); + + let unknown_root = run(&["unknown".to_string()]).expect_err("unknown command"); + assert!(unknown_root.contains("unknown command")); + + let removed_sdk = run(&["sdk".to_string(), "validate".to_string()]) + .expect_err("removed sdk command namespace"); + assert!(removed_sdk.contains("unknown command")); + } + + #[test] + fn lock_workspace_recovers_from_poisoned_mutex() { + let handle = std::thread::spawn(|| { + let _guard = workspace_lock().lock().expect("lock workspace"); + panic!("poison workspace lock"); + }); + assert!(handle.join().is_err()); + + let _guard = lock_workspace(); + } + + #[test] + fn contract_and_coverage_dispatchers_execute() { + let _guard = lock_workspace(); + let out_dir = unique_temp_dir("coverage_dispatch"); + fs::create_dir_all(&out_dir).expect("create out dir"); + + run_contract(&["validate".to_string()]).expect("validate contract"); + coverage::run(&["help".to_string()]).expect("coverage help"); + coverage::run(&["required-crates".to_string()]).expect("coverage required crates"); + coverage::run(&["workspace-crates".to_string()]).expect("coverage workspace crates"); + + let summary_path = out_dir.join("summary.json"); + let lcov_path = out_dir.join("coverage.info"); + let gate_out = out_dir.join("gate-report.json"); + fs::write( + &summary_path, + r#"{"data":[{"totals":{"functions":{"percent":100.0},"lines":{"percent":100.0},"regions":{"percent":100.0}}}]}"#, + ) + .expect("write summary"); + fs::write(&lcov_path, "DA:1,1\nBRDA:1,0,0,1\n").expect("write lcov"); + coverage::run(&[ + "report".to_string(), + "--scope".to_string(), + "main-test".to_string(), + "--summary".to_string(), + summary_path.display().to_string(), + "--lcov".to_string(), + lcov_path.display().to_string(), + "--out".to_string(), + gate_out.display().to_string(), + "--policy-gate".to_string(), + ]) + .expect("coverage report"); + + run(&["coverage".to_string(), "help".to_string()]).expect("root run coverage"); + run(&["hygiene".to_string(), "forbidden-identifiers".to_string()]) + .expect("hygiene forbidden identifiers"); + + let _ = fs::remove_dir_all(out_dir); + } + + #[test] + fn usage_and_main_entrypoints_execute() { + usage(); + let empty_code = main_with_args(Vec::new()); + assert_eq!(empty_code, ExitCode::from(2)); + let success_code = main_with_args(vec!["coverage".to_string(), "help".to_string()]); + assert_eq!(success_code, ExitCode::SUCCESS); + let failure_code = main_with_args(vec!["unknown".to_string()]); + assert_eq!(failure_code, ExitCode::from(2)); + let _ = main(); + } + + #[test] + fn run_contract_dispatches_validate_command() { + let _guard = lock_workspace(); + run_contract(&["validate".to_string()]).expect("contract validate"); + } +}