Files
Martin Vogel 64999462b6 fix(test): the Windows leg runs the test-impact contracts, and its verdict cannot be faked
The full Windows leg on the VM stopped at the coverage-map contract and was
reported green. Two defects, one hiding the other:

- The VM verdict counted the completion marker anywhere in the log. The
  isolated-venue contract's stubbed entry prints that marker, and a capacity
  case prints "expected=0 passed=True", so a leg that stopped at a later
  contract, with no suite run, read as complete. The marker now counts only as
  the log's last step header, which is where scripts/test.sh prints it; the
  verdict contract pins the observed shape (14 cases).
- Under the native python3 the Windows legs install, the test-impact
  contracts failed: the coverage-map builder cannot start the leg's bash
  fakes, the scripts wrote CRLF, and cmp (diffutils) is not installed there.
  selection.py, coverage-merge.py and coverage-map.py now write LF on every
  host, so their outputs are byte-identical across platforms; the two cmp
  checks compare bytes in the shell; the builder's functional leg is skipped
  under a native Windows python with the reason and what was tried (the map
  is built only by the ubuntu publish job), while both pure-python parser
  tests still run there.

Production was not affected by the CRLF: run-tests-parallel.sh strips CR from
the selection and the runner trims its tokens.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-06 03:19:38 +02:00

138 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""Turn a test-impact answer (`codebase-memory-mcp test-impact select`) into
what a CI consumer runs.
selection.py suites ANSWER
One line per suite whose tests the answer selects, or a single `*` when a
lane runs everything. The team-artifact producer re-runs exactly these
suites with coverage (scripts/ci/test-impact-publish.sh).
selection.py filter ANSWER --lane LANE --out FILE [--optional-out FILE2]
exit 0 narrowed: FILE holds the lane's CBM_TEST_ONLY_FILE lines, and
FILE2 the selected suites the engine saw registered only
conditionally (CONDITIONAL): a build may legitimately lack them
(scripts/run-tests-parallel.sh, CBM_TEST_SELECTION_OPTIONAL)
exit 11 the lane selects nothing: FILE is empty
anything else: run everything (FILE and FILE2 are removed)
It fails safe by construction: a run-all answer, a lane it cannot find, an
unknown schema or filter encoding, a token that is not `suite` or
`suite:test`, and a filter that disagrees with the lane's own suite list all
mean "run everything", and so does any crash (exit 1).
"""
import argparse
import json
import os
import re
import sys
SCHEMAS = {"cbm.test_impact.v0", "cbm.test_impact.v1"}
TOKEN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(:[A-Za-z_][A-Za-z0-9_]*)?$")
NARROWED, NOTHING, RUN_ALL = 0, 11, 10
def suites(answer):
if answer.get("decision") == "run_all":
return ["*"]
out = []
for lane in answer.get("lanes") or []:
if lane.get("decision") == "run_all" or lane.get("suites") is None:
return ["*"]
out += [suite["suite"] for suite in lane["suites"]]
return sorted(set(out))
def run_all(why):
print(f"selection: run everything: {why}", file=sys.stderr)
return RUN_ALL
def lane_filter(answer, name):
"""-> (exit code, lines)"""
if answer.get("schema") not in SCHEMAS:
return run_all(f"unknown schema {answer.get('schema')!r}"), []
if answer.get("decision") == "run_all":
return run_all("the answer runs everything"), []
lanes = [lane for lane in answer.get("lanes") or [] if lane.get("lane") == name]
if len(lanes) != 1:
return run_all(f"lane {name!r} appears {len(lanes)} times"), []
lane = lanes[0]
decision = lane.get("decision")
if decision == "skip":
if lane.get("suites") or lane.get("runner_filter") is not None:
return run_all("a skipped lane that still lists suites or a filter"), []
return NOTHING, []
if decision != "selected" or not isinstance(lane.get("suites"), list):
return run_all(f"lane decision {decision!r}"), []
flt = lane.get("runner_filter")
if not isinstance(flt, dict) or flt.get("encoding") != "cbm-test-only-lines" or \
not isinstance(flt.get("value"), str):
return run_all("no cbm-test-only-lines filter"), []
lines = [line for line in flt["value"].split("\n") if line]
bad = [line for line in lines if not TOKEN.match(line)]
if not lines or bad:
return run_all(f"filter tokens unusable ({bad[:3] or 'empty'})"), []
expected = set()
for suite in lane["suites"]:
if suite.get("mode") == "whole":
expected.add(suite["suite"])
elif suite.get("mode") == "tests" and suite.get("tests"):
expected |= {f"{suite['suite']}:{test['test']}" for test in suite["tests"]}
else:
return run_all(f"suite {suite.get('suite')!r} has mode {suite.get('mode')!r}"), []
if set(lines) != expected:
return run_all("the filter disagrees with the lane's suite list"), []
return NARROWED, lines
def conditional_suites(answer, name):
"""The lane's selected suites whose registration the engine saw as conditional."""
for lane in answer.get("lanes") or []:
if lane.get("lane") == name:
return sorted({suite["suite"] for suite in lane.get("suites") or []
if "CONDITIONAL" in (suite.get("whole_reason") or [])})
return []
def main():
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
sub = parser.add_subparsers(dest="command", required=True)
s = sub.add_parser("suites")
s.add_argument("answer")
f = sub.add_parser("filter")
f.add_argument("answer")
f.add_argument("--lane", required=True)
f.add_argument("--out", required=True)
f.add_argument("--optional-out")
args = parser.parse_args()
if args.command == "suites":
# LF on every host, like the files below: a Windows python would
# otherwise write CRLF.
sys.stdout.reconfigure(newline="\n")
with open(args.answer, encoding="utf-8") as handle:
print("\n".join(suites(json.load(handle))))
return 0
for stale in (args.out, args.optional_out):
if stale and os.path.exists(stale):
os.remove(stale)
try:
with open(args.answer, encoding="utf-8") as handle:
answer = json.load(handle)
except (OSError, ValueError) as error:
return run_all(f"unreadable answer ({error})")
if not isinstance(answer, dict):
return run_all("the answer is not an object")
code, lines = lane_filter(answer, args.lane)
if code in (NARROWED, NOTHING):
with open(args.out, "w", encoding="utf-8", newline="\n") as handle:
handle.write("".join(line + "\n" for line in lines))
if args.optional_out:
optional = conditional_suites(answer, args.lane) if code == NARROWED else []
with open(args.optional_out, "w", encoding="utf-8", newline="\n") as handle:
handle.write("".join(suite + "\n" for suite in optional))
return code
if __name__ == "__main__":
sys.exit(main())