Files
MobileGL/tools/trace_replay/trace_cases.py
T
swung0x48 8b76a4e010 [CI] (.github/workflows, docs/ci, tools/trace_replay): drop retrace-split's repeated inproc rows
- with build-linux reused, retrace-split runs its spawn rows plus OpenRA's inproc rows (the wire evidence); the other 75 inproc rows are replayed by the retrace job's inproc-acceptance rows on the same library
- trace_cases.py: github-split-matrix takes --transports and --keep-inproc; the self-test proves the spawn rows unchanged and every dropped inproc row an inproc-acceptance row; only a narrowed matrix that comes out empty raises, so the workflow's named error for an empty full subset still prints
- trace-cases: the spawn-only subset is only a saving, so a failed self-test or an empty result is a warning, the output is not written and retrace-split takes the full subset
- run_trace_case.cmake: MGTRACE_EXPECT_IPC (a harness name) requires the client's IPC line to carry the listed tokens; retrace-split legs prove strict=0 role-split-state=1 run-ahead=1 and a changed default reds every leg with the rows to restore; the acceptance rows prove strict=1 role-split-state=1 run-ahead=1 and gain the refusal census; canned tests in test_run_trace_case.py
2026-10-17 16:08:41 -04:00

643 lines
30 KiB
Python

#!/usr/bin/env python3
# MobileGL - tools/trace_replay/trace_cases.py
# Copyright (c) 2025-2026 MobileGL-Dev
# Licensed under the GNU Lesser General Public License v3.0:
# https://www.gnu.org/licenses/gpl-3.0.txt
# https://www.gnu.org/licenses/lgpl-3.0.txt
# SPDX-License-Identifier: LGPL-3.0-only
# End of Source File Header
import argparse
import json
import sys
from pathlib import Path
TRACE_CASES_JSON = Path(__file__).with_name("trace_cases.json")
CI_BACKENDS = ("DirectGLES", "DirectVulkan")
# Every key a case or the defaults block may carry. An UNKNOWN key is a hard error rather than a
# silent no-op, which is review finding N-4: `"split": true` mistyped as `"splitt": true` loaded
# clean, the split subset became [], the GitHub matrix became {"include":[]}, and `retrace-split`
# was skipped with no red anywhere. Every other way of getting `split` wrong already raised
# (`"ci": false`, a non-bool value) - the typo was the one hole, and it is the shape that makes a
# whole CI job quietly stop existing. P6 inverted the key's default (see load_trace_case_manifest),
# which closes the mirror-image hole: a case added later is IN the split subset unless it says
# otherwise, so forgetting the key can no longer drop a case out of the lane.
#
# Adding a key means adding it here, deliberately, in the same commit. That is the point.
KNOWN_CASE_KEYS = frozenset({
"name",
"trace_archive",
"trace_file",
"golden",
"alternate_golden",
"target_call",
"width",
"height",
"ssim_threshold",
"crop_x",
"crop_y",
"crop_width",
"crop_height",
"coherent_as_flush",
"timeout_seconds",
"ci",
"ci_backends",
"verify",
"split",
"avoid_angle_llvmpipe_explicit_lod_bias",
"backend_overrides",
# Documentation only - which trace a derived fixture was made from, and how. Nothing reads it.
"derived_from",
})
# The only fields a per-backend block may restate. Deliberately three: they are the three the
# comparison is made of. Letting a backend restate `target_call` or `crop_*` would mean the two
# backends stopped retracing the same frame of the same trace through the same window, and the
# rates would no longer be comparable - which is the one thing the two-backend matrix is for.
BACKEND_OVERRIDE_KEYS = frozenset({"golden", "alternate_golden", "ssim_threshold"})
def load_trace_case_manifest(path=TRACE_CASES_JSON):
with Path(path).open("r", encoding="utf-8") as file:
manifest = json.load(file)
defaults = manifest.get("defaults", {})
unknown_defaults = sorted(set(defaults) - KNOWN_CASE_KEYS)
if unknown_defaults:
raise ValueError(
f"unknown key(s) in the defaults block: {', '.join(unknown_defaults)}. "
f"Known keys are {', '.join(sorted(KNOWN_CASE_KEYS))}"
)
cases = []
seen = set()
for case in manifest.get("cases", []):
merged = {**defaults, **case}
name = merged.get("name")
if not name:
raise ValueError("trace case is missing name")
unknown = sorted(set(case) - KNOWN_CASE_KEYS)
if unknown:
raise ValueError(
f"unknown key(s) for {name}: {', '.join(unknown)}. A mistyped flag loads clean and "
f"turns its whole CI subset into an empty matrix, which GitHub skips with no red. "
f"Known keys are {', '.join(sorted(KNOWN_CASE_KEYS))}"
)
if name in seen:
raise ValueError(f"duplicate trace case: {name}")
seen.add(name)
for key in ("trace_archive", "trace_file", "golden", "target_call", "width", "height"):
if key not in merged:
raise ValueError(f"{key} is required for {name}")
# "verify" opts a case into the MOBILEGL_PIPE_VERIFY retrace subset. It is checked here
# rather than where the matrix is built so that a typo is a loud manifest error in every
# consumer (the cmake emitter included) instead of a subset that is quietly one case short.
verify = merged.get("verify", False)
if not isinstance(verify, bool):
raise ValueError(f"verify must be true or false for {name}")
if verify and not merged.get("ci", True):
raise ValueError(
f"{name} is marked verify but excluded from CI, so the verify matrix would drop it"
)
# "split" IS AN OPT-OUT, NOT AN OPT-IN, and P6 inverted it deliberately.
#
# It began as an opt-in listing one case, because the split arm was one
# case's worth of work. Now that the whole CI matrix runs split, 40
# identical `"split": true` lines would carry no information AND would
# reinstate the exact hole the key was written to close: a case added
# later gets the monolith arms by default and is silently absent from
# the split ones. Absent is the one state nothing reds on.
#
# So: a CI case is in the split subset unless it says `"split": false`,
# and saying so is a reviewable claim that this trace cannot cross a
# role boundary. `ci: false` removes a case from CI entirely and takes
# its split arms with it - the subset is a SUBSET of the CI matrix, and
# a case CI never runs cannot be one CI runs split.
#
# The arm runs the case's OWN ci_backends, both of them by default. The
# DirectGLES-only requirement that used to live here is gone with the
# DirectGLES-only arm (tools/trace_replay/CMakeLists.txt says why).
split = merged.get("split", merged.get("ci", True))
if not isinstance(split, bool):
raise ValueError(f"split must be true or false for {name}")
if split and not merged.get("ci", True):
raise ValueError(
f"{name} is marked split but excluded from CI, so the split matrix would drop it"
)
merged["split"] = split
validate_backend_overrides(merged)
cases.append(merged)
return {"defaults": defaults, "cases": cases}
def validate_backend_overrides(case):
"""`backend_overrides` names the golden/threshold ONE backend uses instead of the shared one.
P7 needs it because the device DirectVulkan matrix cannot be scored against the DirectGLES
picture. Until now the two backends shared `golden` plus an optional `alternate_golden`, and
`alternate_golden` is an OR, not a per-backend key: a DirectVulkan run that matched the
DirectGLES golden passed, and a DirectGLES run that matched a Magma-only image passed too.
That is exactly the "went green on the wrong arm" shape, so a per-backend golden REPLACES the
shared pair for that backend rather than being added to it (see case_for_backend).
Everything here is a load-time refusal rather than a runtime surprise, for the reason the
KNOWN_CASE_KEYS comment above gives: the failure mode of a manifest key is a lane that
quietly measures something else.
"""
overrides = case.get("backend_overrides")
if overrides is None:
return
name = case["name"]
if not isinstance(overrides, dict) or not overrides:
raise ValueError(f"backend_overrides must be a non-empty object for {name}")
allowed = ci_backends(case)
for backend, entry in overrides.items():
if backend not in CI_BACKENDS:
raise ValueError(f"unknown backend in backend_overrides for {name}: {backend}")
if backend not in allowed:
# An override for a backend the case never runs can only ever be dead text, and a
# dead override reads exactly like a live one to whoever audits the golden next.
raise ValueError(
f"backend_overrides names {backend} but {name} does not run it "
f"(ci_backends = {', '.join(allowed)})")
if not isinstance(entry, dict) or not entry:
raise ValueError(f"backend_overrides[{backend}] must be a non-empty object for {name}")
unknown = sorted(set(entry) - BACKEND_OVERRIDE_KEYS)
if unknown:
raise ValueError(
f"backend_overrides[{backend}] for {name} may only restate "
f"{', '.join(sorted(BACKEND_OVERRIDE_KEYS))}; got {', '.join(unknown)}")
for key in ("golden", "alternate_golden"):
if key in entry and not (isinstance(entry[key], str) and entry[key]):
raise ValueError(f"backend_overrides[{backend}].{key} must be a non-empty "
f"path for {name}")
threshold = entry.get("ssim_threshold")
if "ssim_threshold" in entry and (isinstance(threshold, bool)
or not isinstance(threshold, (int, float))):
raise ValueError(f"backend_overrides[{backend}].ssim_threshold must be a number "
f"for {name}")
if "alternate_golden" in entry and "golden" not in entry:
# Otherwise the block would add a second acceptable image to the SHARED golden,
# which is the OR this key exists to remove.
raise ValueError(f"backend_overrides[{backend}] for {name} sets alternate_golden "
f"without golden; state the backend's own golden explicitly")
def case_for_backend(case, backend):
"""The case as ONE backend sees it: its own golden pair and threshold, or the shared ones.
Returns the case UNCHANGED - the same object - when it declares no `backend_overrides` at
all, so a manifest without the key produces byte-identical selections, environments and run
fingerprints to the ones it produced before the key existed.
THE KEY ITSELF IS DROPPED from the result even for a backend that overrides nothing, and
that is deliberate: `run_tcp_matrix` fingerprints the resolved case, so leaving the block in
would make "give DirectVulkan its own golden" invalidate every DirectGLES checkpoint in the
same run - a full re-sweep of an arm whose golden, threshold and trace did not move.
"""
if "backend_overrides" not in case:
return case
merged = {key: value for key, value in case.items() if key != "backend_overrides"}
overrides = (case.get("backend_overrides") or {}).get(backend)
if overrides:
if "golden" in overrides:
# REPLACE, do not extend: an override's golden is this backend's whole answer.
merged["alternate_golden"] = ""
merged.update(overrides)
return merged
def backend_override_goldens(case, backend):
"""The goldens this case declares for OTHER backends - what `backend` must never match.
Two sources, and the second is the one that is easy to miss: when THIS backend replaced the
shared golden, the shared `golden`/`alternate_golden` stop being neutral and become the
other backends' images. A DirectVulkan run that matched the shared (DirectGLES) picture is
precisely the accident the per-backend key exists to make impossible, so it is named here
rather than reported as "a different golden".
"""
overrides = case.get("backend_overrides") or {}
foreign = set()
if "golden" in (overrides.get(backend) or {}):
for key in ("golden", "alternate_golden"):
if case.get(key):
foreign.add(case[key])
for other, entry in overrides.items():
if other == backend:
continue
for key in ("golden", "alternate_golden"):
if entry.get(key):
foreign.add(entry[key])
resolved = case_for_backend(case, backend)
for key in ("golden", "alternate_golden"):
foreign.discard(resolved.get(key))
return foreign
def refuse_backend_overrides(cases, consumer):
"""Consumers that cannot express a per-backend golden must say so, not drop it silently.
`add_trace_replay_test_for_backends` and the APK matrix both carry ONE golden path for both
backends. Emitting a case that declares a per-backend golden through either of them would
register the DirectVulkan arm against the DirectGLES image - the precise failure this key was
added to close - so they refuse until they learn the key.
"""
named = [case["name"] for case in cases if case.get("backend_overrides")]
if named:
raise ValueError(
f"{consumer} cannot express backend_overrides yet, and the following case(s) declare "
f"it: {', '.join(named)}. Teach {consumer} the key in the same commit that adds the "
f"override, or the arm it emits runs against another backend's golden.")
def load_trace_cases(path=TRACE_CASES_JSON):
return load_trace_case_manifest(path)["cases"]
def case_with_defaults(case):
return dict(case)
def trace_case_names(path=TRACE_CASES_JSON):
return [case["name"] for case in load_trace_cases(path)]
def find_trace_case(name, path=TRACE_CASES_JSON):
for case in load_trace_cases(path):
if case["name"] == name:
return case
raise KeyError(name)
def fixture_files(case, fixture_root):
root = fixture_root.rstrip("/\\")
files = [
f"{root}/{case['trace_archive']}",
f"{root}/{case['golden']}",
]
if case.get("alternate_golden"):
files.append(f"{root}/{case['alternate_golden']}")
return files
def github_apk_case(case):
refuse_backend_overrides([case], "the APK retrace matrix (github_apk_case)")
result = dict(case)
for key in ("trace_archive", "golden", "alternate_golden"):
if result.get(key):
result[key] = f"tools/trace_replay/fixtures/{result[key]}"
return result
def ci_trace_cases(cases):
return [case for case in cases if case.get("ci", True)]
def verify_trace_cases(cases):
"""The subset the third CI mode retraces.
The verify build compares two state models at every verb boundary and again at every accessor
read, which the design budgets at 5-10x, so the per-push lane runs a named subset and the full
79-case sweep happens at the phase exit and on workflow_dispatch.
"""
return [case for case in cases if case.get("verify", False)]
def split_trace_cases(cases):
"""The subset the split CI modes retrace - inproc AND spawn, both backends.
P5's phase gate named exactly one case, OpenRA at SSIM >= 0.99, because one case was the
work. P6 runs the whole CI matrix across a process boundary, so the subset is now every CI
case that has not explicitly opted out. load_cases() has already resolved the default, so
"split" is present and boolean on every case by the time this reads it.
"""
return [case for case in cases if case.get("split", False)]
def ci_backends(case):
backends = case.get("ci_backends")
if backends is None:
return CI_BACKENDS
if not isinstance(backends, list) or not backends:
raise ValueError(f"ci_backends must be a non-empty list for {case['name']}")
unknown = [backend for backend in backends if backend not in CI_BACKENDS]
if unknown:
raise ValueError(
f"unknown ci_backends for {case['name']}: {', '.join(unknown)}"
)
if len(set(backends)) != len(backends):
raise ValueError(f"ci_backends contains duplicates for {case['name']}")
return backends
def github_test_matrix(cases):
return {
"include": [
{"backend": backend, "case": case["name"]}
for case in cases
for backend in ci_backends(case)
]
}
def github_verify_matrix(cases):
return github_test_matrix(verify_trace_cases(cases))
# The two transports the split retrace lane runs. inproc is the CONTROL - same library, same
# codec, same picture, server role on a thread HERE - and spawn is the claim. Both, always: a
# spawn failure that inproc also shows is a phase bug, and one inproc does not show is a
# transport bug, and collapsing the two makes that distinction unaskable.
SPLIT_TRANSPORTS = ("inproc", "spawn")
def github_split_matrix(cases, transports=SPLIT_TRANSPORTS, keep_inproc=()):
"""{backend, case, transport} for the split arm.
THREE DIMENSIONS, NOT TWO. The job used to take {backend, case} and hard-code
MOBILEGL_TRANSPORT=inproc in its own run block, so `spawn` had no way into CI at all. The
backend list is the case's own ci_backends - the same list the monolith matrix uses - so a
case restricted to one backend stays restricted here instead of being registered with a
backend it does not run.
`transports` narrows the arms (test.yml drops inproc when the inproc-acceptance retrace rows
already replay the same library); `keep_inproc` names split cases whose inproc rows stay
anyway - OpenRA's, which carry the wire-volume evidence. A narrowed matrix that comes out
empty is an error; the full one is returned empty, and test.yml names that error itself.
"""
unknown = [t for t in transports if t not in SPLIT_TRANSPORTS]
if unknown or not transports:
raise ValueError(f"split transports must be a non-empty subset of {SPLIT_TRANSPORTS}: {transports}")
split_cases = split_trace_cases(cases)
split_names = {case["name"] for case in split_cases}
missing = [name for name in keep_inproc if name not in split_names]
if missing:
raise ValueError(f"--keep-inproc names cases that are not split CI cases: {missing}")
rows = [
{"backend": backend, "case": case["name"], "transport": transport}
for case in split_cases
for backend in ci_backends(case)
for transport in SPLIT_TRANSPORTS
if transport in transports or (transport == "inproc" and case["name"] in keep_inproc)
]
narrowed = tuple(transports) != SPLIT_TRANSPORTS or bool(keep_inproc)
if not rows and narrowed:
raise ValueError("the narrowed split matrix is empty")
return {"include": rows}
def github_apk_matrix(cases):
backends = {
"DirectGLES": {"name": "DirectGLES", "gpu": "software"},
"DirectVulkan": {"name": "DirectVulkan", "gpu": "lavapipe"},
}
return {
"include": [
{"backend": backends[backend], "case": github_apk_case(case)}
for case in cases
for backend in ci_backends(case)
]
}
def github_transport_matrix(cases, *, apk=False):
"""Run the entire original matrix as control and as acceptance, once per transport.
The transports use the same D/P artifact and each case's existing backend
list. The legacy split:true subset does not limit this main acceptance matrix.
Nested APK case/backend metadata is deliberately left untouched.
THE SPAWN LANE IS APK-ONLY, and that is a statement about where the evidence is missing
rather than about what spawn can do. On Linux the spawn arm already has a job of its own -
test.yml's `retrace-split`, which is {backend, case, transport} over spawn (and inproc too
when it cannot reuse this matrix's inproc rows' library) and whose legs prove in-test that
the transport resolved and, on OpenRA, that draw records crossed the wire
(run_trace_case.cmake's evidence). Adding spawn to THIS matrix would pay
for a second full Linux sweep that proves nothing the first one does not, and test.yml's
`retrace` job has neither the server image staged nor MOBILEGL_IPC_SERVER_PATH set - so the
lane would not merely be redundant, it would be red.
On Android there is no such job. Until this lane existed, nothing in CI ever started a
second MobileGL process on a device.
"""
build = github_apk_matrix if apk else github_test_matrix
if not cases:
raise ValueError("transport matrix needs at least one trace case")
lanes = [
(cases, "monolith-control", "monolith"),
(cases, "inproc-acceptance", "inproc"),
]
if apk:
lanes.append((cases, "spawn-acceptance", "spawn"))
return {"include": [
{**row, "lane": lane, "transport": transport}
for selected, lane, transport in lanes
for row in build(selected)["include"]
]}
def self_test_transport_matrices():
cases = ci_trace_cases(load_trace_cases())
for apk in (False, True):
build = github_apk_matrix if apk else github_test_matrix
original = build(cases)["include"]
mixed = github_transport_matrix(cases, apk=apk)["include"]
controls = [row for row in mixed if row["transport"] == "monolith"]
acceptance = [row for row in mixed if row["transport"] == "inproc"]
strip_lane = lambda rows: [{key: value for key, value in row.items()
if key not in ("lane", "transport")} for row in rows]
spawned = [row for row in mixed if row["transport"] == "spawn"]
assert strip_lane(controls) == original, "control lost original case/backend/parameters"
assert strip_lane(acceptance) == original, "acceptance lost original case/backend/parameters"
assert all(row["lane"] == "monolith-control" for row in controls)
assert all(row["lane"] == "inproc-acceptance" for row in acceptance)
# THE SPAWN LANE IS APK-ONLY AND MUST NOT DRIFT INTO THE LINUX MATRIX. test.yml's
# `retrace` job exports matrix.transport straight into MOBILEGL_TRANSPORT and stages no
# server image, so a spawn row reaching it would be red for a packaging reason and say
# nothing about the transport. Asserted in both directions: present for apk, absent
# otherwise - an accidental `if apk` removal has to be caught here rather than in CI.
if apk:
assert strip_lane(spawned) == original, "spawn lane lost original case/backend/parameters"
assert all(row["lane"] == "spawn-acceptance" for row in spawned)
else:
assert not spawned, "the Linux transport matrix must carry no spawn lane"
identities = [(row["case"]["name"] if apk else row["case"],
row["backend"]["name"] if apk else row["backend"], row["transport"])
for row in mixed]
assert len(identities) == len(set(identities)), "duplicate lane identity"
# A restricted case keeps exactly its original backend and golden data.
restricted = [{**cases[0], "split": False, "ci_backends": ["DirectGLES"]}]
rows = github_transport_matrix(restricted, apk=apk)["include"]
expected_lanes = 3 if apk else 2
assert len(rows) == expected_lanes
assert all(strip_lane(rows[:1]) == strip_lane(rows[index:index + 1])
for index in range(1, expected_lanes))
try:
github_transport_matrix([], apk=apk)
except ValueError:
pass
else:
raise AssertionError("an empty acceptance lane silently passed")
print(f"transport matrix {'APK' if apk else 'Linux'}: {len(controls)} unchanged controls, "
f"{len(acceptance)} inproc acceptance entries, {len(spawned)} spawn acceptance "
f"entries; exact metadata and negative controls OK")
def self_test_split_matrix(cases=None):
"""retrace-split may drop its inproc rows only because the inproc-acceptance rows replay them."""
if cases is None:
cases = ci_trace_cases(load_trace_cases())
full = github_split_matrix(cases)["include"]
spawn_only = github_split_matrix(cases, transports=("spawn",), keep_inproc=("OpenRA",))["include"]
assert [r for r in spawn_only if r["transport"] == "spawn"] == [r for r in full if r["transport"] == "spawn"], \
"the spawn-only matrix lost or changed a spawn row"
assert any(r["transport"] == "spawn" for r in spawn_only), "the spawn-only matrix has no spawn row"
kept = [r for r in spawn_only if r["transport"] == "inproc"]
assert kept and all(r["case"] == "OpenRA" for r in kept), "the kept inproc rows are not OpenRA's"
assert all(r in full for r in kept), "a kept inproc row is not a row of the full matrix"
acceptance = {(r["backend"], r["case"]) for r in github_transport_matrix(cases)["include"]
if r["lane"] == "inproc-acceptance"}
dropped = [(r["backend"], r["case"]) for r in full if r["transport"] == "inproc" and r not in kept]
uncovered = [row for row in dropped if row not in acceptance]
assert not uncovered, f"inproc rows dropped from retrace-split that no acceptance row replays: {uncovered}"
for bad in ({"transports": ("unix",)}, {"transports": ()}, {"keep_inproc": ("no-such-case",)}):
try:
github_split_matrix(cases, **bad)
except ValueError:
continue
raise AssertionError(f"github_split_matrix accepted {bad}")
try:
github_split_matrix([], transports=("spawn",))
except ValueError:
pass
else:
raise AssertionError("an empty narrowed split matrix silently passed")
assert github_split_matrix([]) == {"include": []}, \
"the full split matrix must come back empty, for the workflow's named error"
print(f"split matrix: {len(full)} rows, {len(spawn_only)} with inproc dropped ({len(kept)} OpenRA inproc "
f"kept); all {len(dropped)} dropped inproc rows are inproc-acceptance rows")
def cmake_quote(value):
return '"' + str(value).replace("\\", "/").replace('"', '\\"') + '"'
def emit_cmake(cases, fixture_root):
refuse_backend_overrides(cases, "the CTest catalog emitter (emit_cmake)")
root = fixture_root.rstrip("/\\")
lines = ["# Generated from tools/trace_replay/trace_cases.json.", ""]
keys = [
("TRACE_ARCHIVE", "trace_archive", True),
("TRACE_FILE", "trace_file", False),
("GOLDEN", "golden", True),
("ALTERNATE_GOLDEN", "alternate_golden", True),
("TARGET_CALL", "target_call", False),
("WIDTH", "width", False),
("HEIGHT", "height", False),
("SSIM_THRESHOLD", "ssim_threshold", False),
("CROP_X", "crop_x", False),
("CROP_Y", "crop_y", False),
("CROP_WIDTH", "crop_width", False),
("CROP_HEIGHT", "crop_height", False),
("COHERENT_AS_FLUSH", "coherent_as_flush", False),
]
def emit_one(function, case):
lines.append(f"{function}({cmake_quote(case['name'])}")
for cmake_key, json_key, fixture_path in keys:
value = case.get(json_key)
if value is None or value == "":
continue
if fixture_path:
value = f"{root}/{value}"
lines.append(f" {cmake_key} {cmake_quote(value)}")
lines.append(")")
lines.append("")
for case in cases:
emit_one("add_trace_replay_test_for_backends", case)
# P5's split arm, emitted beside the monolith pair rather than in a block of its own so
# that a case and its variant always carry identical parameters. The CMake side is a
# no-op unless MOBILEGL_BUILD_DISAGGREGATED is ON.
if case.get("split", False):
emit_one("add_trace_replay_split_test", case)
return "\n".join(lines)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("--case", dest="case_name", help="Trace case name.")
parser.add_argument("--ci", action="store_true", help="Only include cases enabled for CI.")
parser.add_argument("--self-test-transport-matrices", action="store_true")
parser.add_argument("--self-test-split-matrix", action="store_true",
help="prove the spawn-only split matrix drops only inproc-acceptance rows")
parser.add_argument("--fixture-root", default="tools/trace_replay/fixtures")
parser.add_argument("--transports", default=",".join(SPLIT_TRANSPORTS),
help="github-split-matrix: the comma-separated split transports to emit")
parser.add_argument("--keep-inproc", default="",
help="github-split-matrix: comma-separated split cases whose inproc rows stay")
parser.add_argument(
"--format",
choices=(
"names",
"github-test-matrix",
"github-test-transport-matrix",
"github-verify-matrix",
"github-split-matrix",
"github-apk",
"github-apk-matrix",
"github-apk-transport-matrix",
"fixture-files",
"cmake",
),
default="names",
)
return parser.parse_args()
def main():
args = parse_args()
if args.self_test_transport_matrices:
self_test_transport_matrices()
return 0
if args.self_test_split_matrix:
self_test_split_matrix()
return 0
cases = load_trace_cases()
if args.ci:
cases = ci_trace_cases(cases)
if args.format == "names":
print(json.dumps([case["name"] for case in cases], separators=(",", ":")))
elif args.format == "github-test-matrix":
print(json.dumps(github_test_matrix(cases), separators=(",", ":")))
elif args.format == "github-test-transport-matrix":
print(json.dumps(github_transport_matrix(cases), separators=(",", ":")))
elif args.format == "github-verify-matrix":
print(json.dumps(github_verify_matrix(cases), separators=(",", ":")))
elif args.format == "github-split-matrix":
transports = tuple(t for t in args.transports.split(",") if t)
keep = tuple(c for c in args.keep_inproc.split(",") if c)
print(json.dumps(github_split_matrix(cases, transports, keep), separators=(",", ":")))
elif args.format == "github-apk":
print(json.dumps([github_apk_case(case) for case in cases], separators=(",", ":")))
elif args.format == "github-apk-matrix":
print(json.dumps(github_apk_matrix(cases), separators=(",", ":")))
elif args.format == "github-apk-transport-matrix":
print(json.dumps(github_transport_matrix(cases, apk=True), separators=(",", ":")))
elif args.format == "fixture-files":
if not args.case_name:
print("--case is required for --format fixture-files", file=sys.stderr)
return 2
try:
case = find_trace_case(args.case_name)
except KeyError:
print(f"unknown trace case: {args.case_name}", file=sys.stderr)
return 2
print("\n".join(fixture_files(case, args.fixture_root)))
elif args.format == "cmake":
print(emit_cmake(cases, args.fixture_root))
return 0
if __name__ == "__main__":
raise SystemExit(main())