rdkafka-hs-1.1.0.0: scripts/coverage_common.py
"""Candidate identity and compiler-derived public exports for coverage tools."""
import hashlib
import json
import re
import subprocess
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def run(*args):
return subprocess.check_output(args, cwd=ROOT, text=True)
def library_hpc_includes(mixfiles):
return ["--include=" + p.parent.name + ":" + p.stem for p in mixfiles]
def compiler_generated_declaration(line):
"""Ticks on deriving declarations come from GHC, not handwritten methods."""
if not re.match(r"^\s*deriving\b", line):
return False
if ";" in line:
raise ValueError("mixed deriving/code line requires explicit coverage classification")
return True
def fingerprint():
# Evidence-only documentation is deliberately excluded. Include untracked
# test/source additions; git diff alone cannot identify a dirty candidate.
files = set()
for directory in ("src", "test", "test-integration", "test-support", "scripts", "nix"):
files.update(p for p in (ROOT / directory).rglob("*") if p.is_file() and "__pycache__" not in p.parts)
files.update(ROOT / name for name in ("rdkafka-hs.cabal", "cabal.project", "cabal.project.local", "cabal.project.freeze", ".envrc", ".envrc.local", "flake.nix", "flake.lock", "flake.module.nix", "justfile"))
digest = hashlib.sha256()
for path in sorted(files):
if path.exists():
digest.update(str(path.relative_to(ROOT)).encode() + b"\0" + path.read_bytes() + b"\0")
return digest.hexdigest()
def exposed_modules():
cabal = (ROOT / "rdkafka-hs.cabal").read_text()
# Scope deliberately limited to this package's flat exposed-modules stanza.
# Reject unrecognised syntax instead of silently losing conditional exports.
sections = re.findall(r"^ exposed-modules:\s*\n((?: [^\n]+\n)+)", cabal, re.M)
if len(sections) != 1:
raise ValueError("expected exactly one flat exposed-modules stanza")
modules = sections[0].split()
if not all(re.fullmatch(r"[A-Z][\w]*(?:\.[A-Z][\w]*)*", m) for m in modules):
raise ValueError("unsupported exposed-modules syntax")
return modules
def components(builddir):
plan = json.loads((ROOT / builddir / "cache/plan.json").read_text())
return {c["component-name"]: Path(c["dist-dir"]) for c in plan["install-plan"] if c.get("pkg-name") == "rdkafka-hs"}
def compiler_exports(builddir):
library = components(builddir)["lib"] / "build"
result = {}
for module in exposed_modules():
interface = library / (module.replace(".", "/") + ".hi")
# GHC resolves multiline exports, module re-exports, constructors,
# record selectors and opaque types. This parser consumes its explicit
# export section, never a source-code approximation of Haskell exports.
output = run("ghc", "--show-iface", str(interface))
match = re.search(r"^exports:\n((?: [^\n]+\n)+)", output, re.M)
if not match:
raise ValueError(f"missing compiler exports: {module}")
entries = []
for line in match[1].splitlines():
names = re.findall(r"[A-Za-z_][A-Za-z_0-9'.]*", line)
if not names:
raise ValueError(f"unsupported export syntax: {line}")
names = [name if "." in name else module + "." + name for name in names]
entries.append({"symbol": names[0], "members": sorted(set(names[1:]))})
result[module] = sorted(entries, key=lambda x: x["symbol"])
return result
QUALIFICATION_MODES = frozenset(("--broker-qualification", "--authentication-qualification"))
def qualification_context_errors(context, candidate, registered=None):
"""Supplement behavioral registration only; never manufacture HPC ticks."""
errors = []
if context.get("kind") != "qualification-context" or context.get("mode") not in QUALIFICATION_MODES:
errors.append("unsupported qualification context")
if context.get("fingerprint") != candidate:
errors.append("stale qualification context")
names = context.get("tests", [])
if not isinstance(names, list) or not names or len(names) != len(set(names)):
errors.append("empty or duplicate qualification tests")
elif registered is not None and set(names) != set(registered):
errors.append("qualification context differs from current registered tests")
if context.get("passed") != len(names):
errors.append("incomplete qualification context")
if not context.get("environment") or not context.get("source") or context.get("mode") not in context.get("command", []):
errors.append("missing qualification source/environment/command")
artifacts = context.get("artifacts", {})
if not isinstance(artifacts, dict) or not artifacts:
errors.append("missing qualification artifacts")
artifacts = {}
for name, checksum in artifacts.items():
path = ROOT / name
if not path.is_file() or hashlib.sha256(path.read_bytes()).hexdigest() != checksum:
errors.append("missing or modified qualification artifact: " + name)
for field in ("listing", "log"):
name = context.get(field)
if name not in artifacts:
errors.append("missing qualification " + field)
continue
path = ROOT / name
if not path.is_file():
continue
content = path.read_text()
if field == "listing" and content.splitlines() != names:
errors.append("qualification listing differs from recorded tests")
if field == "log" and (re.findall(r"All (\d+) tests passed", content) != [str(len(names))] or re.search(r": FAIL(?:\s|$)", content)):
errors.append("qualification log does not prove every test passed")
return errors