lez-fuzzing/scripts/check_target_inventory.py
2026-07-02 15:04:31 +02:00

97 lines
3.3 KiB
Python
Executable File

#!/usr/bin/env python3
"""Fail if any fuzz target registered in fuzz/Cargo.toml is missing from a
human-authored doc that enumerates the target list.
`fuzz/Cargo.toml` is the single source of truth: every `[[bin]] name = "fuzz_*"`
must be mentioned by name in each consumer file below.
The CI workflows and shell scripts derive their target lists *directly* from
`fuzz/Cargo.toml` at runtime, so they cannot drift and are not checked here:
- `.github/workflows/{fuzz,fuzz-afl}.yml` and the `corpus-update.yml` matrices
use the `.github/actions/resolve-targets` composite action.
- `.github/workflows/mutants.yml` calls the same action for its build loop.
- `scripts/mutants-corpus-test.sh` parses `fuzz/Cargo.toml` inline.
Only the prose target tables in the docs below carry a hand-written description
per target and therefore need this drift gate.
Usage:
python3 scripts/check_target_inventory.py
Exit code 0 = all consumers list every target; 1 = drift detected (prints the
missing target/file pairs). Run from anywhere; paths are resolved relative to
the repository root.
"""
import re
import sys
from pathlib import Path
# Human-authored docs whose prose target tables must stay in sync with Cargo.toml.
# (Workflows/scripts auto-derive their lists from Cargo.toml — see the module docstring.)
# Paths are relative to the repository root.
CONSUMERS = [
"README.md",
"docs/fuzzing.md",
]
_BIN_NAME_RE = re.compile(r'name\s*=\s*"(fuzz_[a-z0-9_]+)"')
def registered_targets(cargo_toml: Path) -> list[str]:
"""Every `[[bin]] name = "fuzz_*"` in fuzz/Cargo.toml, in file order."""
names = _BIN_NAME_RE.findall(cargo_toml.read_text())
# Preserve order, drop duplicates defensively.
seen: set[str] = set()
ordered: list[str] = []
for n in names:
if n not in seen:
seen.add(n)
ordered.append(n)
return ordered
def main() -> None:
root = Path(__file__).parent.parent # repository root
cargo_toml = root / "fuzz" / "Cargo.toml"
if not cargo_toml.exists():
print(f"ERROR: {cargo_toml} not found", file=sys.stderr)
sys.exit(1)
targets = registered_targets(cargo_toml)
if not targets:
print(f"ERROR: no [[bin]] targets found in {cargo_toml}", file=sys.stderr)
sys.exit(1)
missing: list[tuple[str, str]] = []
for rel in CONSUMERS:
path = root / rel
if not path.exists():
print(f"ERROR: consumer file not found: {rel}", file=sys.stderr)
sys.exit(1)
text = path.read_text()
for target in targets:
if target not in text:
missing.append((rel, target))
if missing:
print(
f"Target-inventory drift: {len(targets)} targets registered in "
f"fuzz/Cargo.toml, but some consumers are missing entries:\n",
file=sys.stderr,
)
for rel, target in missing:
print(f" MISSING {rel} -> {target}", file=sys.stderr)
print(
"\nAdd the target(s) above to each listed file "
"(see scripts/add_fuzz_target.py for the canonical insertion points).",
file=sys.stderr,
)
sys.exit(1)
print(f"OK: all {len(CONSUMERS)} consumers list every one of the "
f"{len(targets)} registered targets.")
if __name__ == "__main__":
main()