#!/usr/bin/env python3
"""Run all source, compiled API and handwritten coverage gates."""
import os
from pathlib import Path
import re
import subprocess
import sys
import xml.etree.ElementTree as ET
ROOT = Path(__file__).resolve().parents[1]
GENERATED = {"Slackblocks.Models", "Slackblocks.Internal.Schema"}
def run(*args, **kwargs):
return subprocess.run(args, cwd=ROOT, check=True, **kwargs)
def coverage(xml, expected):
tree = ET.fromstring(xml)
modules = {}
for module in tree.iter("module"):
name = module.attrib["name"].split("/")[-1]
if name in GENERATED:
continue
expressions = module.find("exprs")
if expressions is None:
raise ValueError(f"Missing expression coverage for {name}")
if name in modules:
raise ValueError(f"Duplicate module receipt: {name}")
used, total = int(expressions.attrib["count"]), int(expressions.attrib["boxes"])
if not 0 <= used <= total:
raise ValueError(f"Invalid coverage counts: {name}")
modules[name] = (used, total)
if set(modules) != expected:
raise ValueError(f"Missing/unexpected handwritten modules: {set(modules) ^ expected}")
used = sum(value[0] for value in modules.values())
total = sum(value[1] for value in modules.values())
if not total or used * 100 < total * 95:
raise ValueError(f"Handwritten expression coverage below 95%: {used}/{total}")
print(f"Handwritten expressions: {used}/{total} ({100 * used / total:.2f}%)")
return modules
def main():
for command, expected in (("ormolu", "ormolu 0.7.7.0"), ("hlint", "HLint v3.8")):
actual = run(command, "--version", capture_output=True, text=True).stdout.splitlines()[0]
if actual.split(",", 1)[0] != expected:
raise SystemExit(f"Expected {expected}; found {actual}")
run(sys.executable, "generator/generate.py", "--check")
sources = [p for folder in ('src', 'test', 'consumer', 'integrations/src') for p in sorted((ROOT / folder).rglob('*.hs'))]
for source in sources:
run("ormolu", "--mode", "check", str(source))
# Generated code is checked by formatting, deterministic regeneration and
# GHC warnings. HLint reviews every handwritten production/test module.
handwritten = []
for source in sources:
module = ".".join(source.relative_to(ROOT / "src").with_suffix("").parts) if source.is_relative_to(ROOT / "src") else None
if module not in GENERATED:
handwritten.append(str(source))
run("hlint", *handwritten)
run(sys.executable, "bin/api-audit")
build = Path(os.environ.get("SLACKBLOCKS_HASKELL_COVERAGE", "dist-coverage")).resolve()
for tix in build.rglob("*.tix"):
tix.unlink() # Only this checker's dedicated coverage build tree.
run("cabal", f"--builddir={build}", "test", "all", "--enable-coverage", "--disable-optimization", "--test-show-details=direct")
cabal = (ROOT / "slackblocks.cabal").read_text(encoding="utf-8")
suites = set(re.findall(r"^test-suite (\S+)", cabal, re.M))
tixes = sorted(build.glob("build/*/*/slackblocks-*/t/*/noopt/hpc/vanilla/tix/*.tix"))
if {p.stem for p in tixes} != suites or len(tixes) != len(suites):
raise SystemExit("Incomplete or ambiguous native coverage suite inventory")
mix = list(build.glob("build/*/*/slackblocks-*/noopt/build/extra-compilation-artifacts/hpc/vanilla/mix"))
if len(mix) != 1:
raise SystemExit("Ambiguous library coverage artifacts")
expected = {".".join(p.relative_to(ROOT / "src").with_suffix("").parts) for p in (ROOT / "src").rglob("*.hs")} - GENERATED
# A re-export-only module has no executable ticks, but must still have a
# compiled Mix record. Any added code is automatically included by HPC.
compiled = {p.stem for p in mix[0].rglob("*.mix")}
if not expected <= compiled:
raise SystemExit(f"Uninstrumented production modules: {expected - compiled}")
empty = {p.stem for p in mix[0].rglob("*.mix") if p.read_text(encoding="utf-8").rstrip().endswith(" []")}
expected -= empty
output = build / "quality"
output.mkdir(exist_ok=True)
combined = output / "combined.tix"
run("hpc", "sum", "--union", "--exclude=Main", "--exclude=Constructions", "--exclude=Paths_slackblocks", f"--output={combined}", *(str(p) for p in tixes))
report = ["hpc", "report", str(combined), f"--hpcdir={mix[0]}", "--per-module", "--xml-output"]
xml = run(*report, capture_output=True, text=True).stdout
(output / "coverage.xml").write_text(xml, encoding="utf-8")
coverage(xml, expected)
# Mutate actual compiled receipts, not a fabricated percentage, and prove
# the gate fails when execution disappears or a module is omitted.
tree = ET.fromstring(xml)
for exprs in tree.iter("exprs"):
exprs.set("count", "0")
for candidate, scope in ((ET.tostring(tree, encoding="unicode"), expected), (xml, expected | {"Slackblocks.Unmeasured"})):
try:
coverage(candidate, scope)
except ValueError:
pass
else:
raise SystemExit("Coverage guard accepted its negative control")
print("Source formatting, lint, compiled API, 95% handwritten coverage and negative controls passed.")
if __name__ == "__main__":
main()