Summarize successful steps, print the failing check and error text, and split the validate job into seven steps. Local npm test stays TAP.
145 lines
5.0 KiB
Python
145 lines
5.0 KiB
Python
"""Output contract for the quality-gate runner (BUG-1230).
|
|
|
|
The gate used to stream every child report into one CI step. Compact mode
|
|
prints a single success line. A failing step keeps its exit code, the last
|
|
200 lines, and a full log under gate-logs/. --verbose streams the child
|
|
again and does not capture.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
|
|
import run_quality_gate as gate # noqa: E402
|
|
|
|
|
|
def setup_function() -> None:
|
|
gate.set_verbose(False)
|
|
gate.reset_output_state()
|
|
|
|
|
|
def test_quick_banner_is_one_line() -> None:
|
|
line = gate.format_profile_banner("quick", gate.QUALITY_GATE_PROFILES["quick"], verbose=False)
|
|
assert "\n" not in line
|
|
assert "profile=quick" in line
|
|
assert "test_timeout_seconds=90" in line
|
|
assert "skip_frontend_runtime=false" in line
|
|
|
|
|
|
def test_verbose_banner_keeps_indented_profile_json() -> None:
|
|
text = gate.format_profile_banner("quick", gate.QUALITY_GATE_PROFILES["quick"], verbose=True)
|
|
assert "== Quality gate profile: quick ==" in text
|
|
assert '"test_timeout_seconds": 90' in text
|
|
assert text.count("\n") > 3
|
|
|
|
|
|
def test_last_output_lines_keeps_the_tail() -> None:
|
|
text = "\n".join(f"n{index}" for index in range(201))
|
|
tail = gate.last_output_lines(text).splitlines()
|
|
assert tail[0] == "n1"
|
|
assert tail[-1] == "n200"
|
|
assert len(tail) == 200
|
|
|
|
|
|
def test_compact_hides_child_stdout(capfd, tmp_path: Path) -> None:
|
|
script = tmp_path / "child_marker.py"
|
|
script.write_text("print('hidden-child-marker')\n", encoding="utf-8")
|
|
assert gate.run([sys.executable, str(script)]) is True
|
|
out, _err = capfd.readouterr()
|
|
assert "hidden-child-marker" not in out
|
|
assert "✓" in out
|
|
assert out.strip().endswith("s")
|
|
|
|
|
|
def test_verbose_streams_child_output(capfd) -> None:
|
|
gate.set_verbose(True)
|
|
assert gate.run([sys.executable, "-c", "print('verbose-child-marker')"]) is True
|
|
out, _err = capfd.readouterr()
|
|
assert "$" in out
|
|
assert "verbose-child-marker" in out
|
|
assert "✓" not in out
|
|
|
|
|
|
def test_capture_does_not_invent_a_timeout(monkeypatch, capfd) -> None:
|
|
seen: dict = {}
|
|
|
|
def fake_run(cmd, **kwargs):
|
|
seen.update(kwargs)
|
|
return subprocess.CompletedProcess(cmd, 0, stdout="ignored\n", stderr="")
|
|
|
|
monkeypatch.setattr(gate.subprocess, "run", fake_run)
|
|
assert gate.run([sys.executable, "scripts/audit_capabilities.py", "--mode", "validate"]) is True
|
|
assert seen.get("capture_output") is True
|
|
assert "timeout" not in seen
|
|
out, _err = capfd.readouterr()
|
|
assert "ignored" not in out
|
|
assert "scripts/audit_capabilities.py" in out
|
|
|
|
|
|
def test_failure_shows_mark_tail_and_log(monkeypatch, tmp_path: Path, capfd) -> None:
|
|
monkeypatch.setattr(gate, "ROOT", tmp_path)
|
|
stdout = "".join(f"line-{index:03d}\n" for index in range(250))
|
|
|
|
def fake_run(cmd, **kwargs):
|
|
assert kwargs.get("capture_output") is True
|
|
return subprocess.CompletedProcess(cmd, 3, stdout=stdout, stderr="err-tail\n")
|
|
|
|
monkeypatch.setattr(gate.subprocess, "run", fake_run)
|
|
with pytest.raises(SystemExit) as caught:
|
|
gate.run([sys.executable, "scripts/audit_capabilities.py"])
|
|
assert caught.value.code == 3
|
|
_out, err = capfd.readouterr()
|
|
assert "✗ scripts/audit_capabilities.py exit=3" in err
|
|
assert "line-249" in err
|
|
assert "line-000" not in err
|
|
assert "err-tail" in err
|
|
assert "full log: gate-logs/" in err
|
|
logs = list((tmp_path / "gate-logs").glob("*.log"))
|
|
assert len(logs) == 1
|
|
body = logs[0].read_text(encoding="utf-8")
|
|
assert "line-000" in body
|
|
assert "line-249" in body
|
|
assert "err-tail" in body
|
|
assert len(err.splitlines()) < 250
|
|
|
|
|
|
def test_real_failing_command_keeps_its_exit_code(monkeypatch, tmp_path: Path, capfd) -> None:
|
|
monkeypatch.setattr(gate, "ROOT", tmp_path)
|
|
with pytest.raises(SystemExit) as caught:
|
|
gate.run([sys.executable, "-c", "import sys; print('boom-line'); sys.exit(4)"])
|
|
assert caught.value.code == 4
|
|
_out, err = capfd.readouterr()
|
|
assert "exit=4" in err
|
|
assert "boom-line" in err
|
|
assert "full log: gate-logs/" in err
|
|
assert (tmp_path / "gate-logs").is_dir()
|
|
|
|
|
|
def test_optional_failure_continues(monkeypatch, tmp_path: Path, capfd) -> None:
|
|
monkeypatch.setattr(gate, "ROOT", tmp_path)
|
|
|
|
def fake_run(cmd, **kwargs):
|
|
return subprocess.CompletedProcess(cmd, 7, stdout="nope\n", stderr="")
|
|
|
|
monkeypatch.setattr(gate.subprocess, "run", fake_run)
|
|
assert gate.run([sys.executable, "scripts/validate_logic_v2.py"], optional=True) is False
|
|
out, err = capfd.readouterr()
|
|
assert "exit=7" in err
|
|
assert "continuing" in f"{out}\n{err}"
|
|
|
|
|
|
def test_compact_compile_is_one_line(capfd) -> None:
|
|
gate.compile_targets()
|
|
out, _err = capfd.readouterr()
|
|
lines = [line for line in out.splitlines() if line.strip()]
|
|
assert lines == [line for line in lines if line.startswith("✓ py_compile ")]
|
|
assert len(lines) == 1
|
|
assert "files" in lines[0]
|