atlas-iac/testing/tests/hux_node_gate.py

94 lines
3.5 KiB
Python
Raw Permalink Normal View History

"""Portable node test-runner gate for the HUX browser suites.
Runs ``node --test --experimental-test-coverage`` without the
version-gated threshold/include flags (Node >= 22.8 only) and enforces
the per-source thresholds by parsing the coverage table, so the same
gate passes on the CI image's Node 20 and on newer local Nodes.
TypeScript suites need ``--experimental-strip-types`` (Node >= 22.6);
on older runtimes they skip with an explicit reason instead of failing
on a missing capability.
"""
from __future__ import annotations
import re
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
STRIP_TYPES_MINIMUM = (22, 6)
ROW = re.compile(r"([^|\s][^|]*?)\s*\|\s*([0-9.]+)\s*\|\s*([0-9.]+)\s*\|\s*([0-9.]+)")
METRICS = ("lines", "branches", "functions")
def node_version() -> tuple[int, int]:
try:
raw = subprocess.run(
["node", "--version"], check=True, capture_output=True, text=True, timeout=15
).stdout.strip().lstrip("v")
except (FileNotFoundError, subprocess.CalledProcessError):
pytest.skip("node is unavailable on this runner")
major, minor = raw.split(".")[:2]
return int(major), int(minor)
def require_node() -> None:
"""Skip a node-dependent test on runners without a node binary."""
node_version()
def run_node_coverage(
sources: list[str],
tests: list[str],
thresholds: dict[str, float],
strip_types: bool = False,
timeout: int = 60,
overrides: dict[str, dict[str, float]] | None = None,
) -> subprocess.CompletedProcess:
"""Run the node suite and enforce coverage thresholds per source file.
``overrides`` maps a source basename to explicit per-metric floors for
documented, pre-existing coverage debt (the old aggregate node gate hid
per-file shortfalls); an override still enforces its stated floor.
"""
unknown = set(thresholds) - set(METRICS)
assert not unknown, f"unknown metrics {unknown}"
overrides = overrides or {}
version = node_version()
if strip_types and version < STRIP_TYPES_MINIMUM:
pytest.skip(
f"node {version[0]}.{version[1]} lacks --experimental-strip-types; "
"the TypeScript suites need Node >= 22.6"
)
command = ["node", "--test"]
if strip_types:
command.append("--experimental-strip-types")
command += ["--experimental-test-coverage", *tests]
result = subprocess.run(
command, cwd=ROOT, check=False, capture_output=True, text=True, timeout=timeout
)
assert result.returncode == 0, result.stdout + result.stderr
rows: dict[str, tuple[float, float, float]] = {}
for line in result.stdout.splitlines():
match = ROW.search(line)
if match:
label = match.group(1).replace("\u2139", " ").strip()
name = label.split("/")[-1]
rows[name] = tuple(float(match.group(i)) for i in (2, 3, 4))
for source in sources:
name = Path(source).name
assert name in rows, (
f"no coverage row for {source}\n" + result.stdout + result.stderr
)
line_pct, branch_pct, funcs_pct = rows[name]
actual = {"lines": line_pct, "branches": branch_pct, "functions": funcs_pct}
floors = {**thresholds, **overrides.get(name, {})}
for metric, minimum in floors.items():
assert actual[metric] >= minimum, (
f"{source} {metric} coverage {actual[metric]} < {minimum}\n"
+ result.stdout
)
return result