Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

806 lines
27 KiB
Python

#!/usr/bin/env python3
# Copyright © Advanced Micro Devices, Inc., or its affiliates.
# SPDX-License-Identifier: MIT
"""
Self-test for the migration toolchain on a synthetic fixture.
Creates 3 synthetic graphs: 2 isomorphic (same topology, different knobs)
and 1 distinct. Verifies:
1. skeleton_hash groups the 2 isomorphic graphs together
2. place_bundles produces 1 sweep (2 cases) + 1 standalone
3. verify_migration round-trip passes for all 3
4. import_graph detects an exact duplicate (skip) and a new case (append)
No C++ binary needed — everything is pure Python on synthetic data.
Usage::
python3 test_migration.py [-v]
"""
import json
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent
def _make_graph(node_type, tensor_uids, dims, strides, data_type, io_data_type=None):
"""Build a minimal synthetic graph dict."""
nodes = [
{
"type": f"{node_type}Attributes",
"name": "",
"inputs": {f"x_tensor_uid": tensor_uids[0]},
"outputs": {f"y_tensor_uid": tensor_uids[-1]},
}
]
tensors = []
for uid in tensor_uids:
tensors.append(
{
"uid": uid,
"name": "",
"dims": dims,
"strides": strides,
"data_type": data_type,
"virtual": False,
}
)
graph = {
"nodes": nodes,
"tensors": tensors,
"io_data_type": io_data_type or data_type,
"compute_data_type": "float",
"intermediate_data_type": "float",
"name": "",
}
return graph
def _write_captured(capture_dir, suite, case_name, graph, meta=None):
"""Write a captured case in the Hop A directory structure."""
case_dir = capture_dir / suite / case_name
case_dir.mkdir(parents=True, exist_ok=True)
with open(case_dir / f"{case_name}.json", "w") as f:
json.dump(graph, f, indent=2)
if meta is None:
meta = {"format_version": 1, "seed": 42}
with open(case_dir / f"{case_name}.meta.json", "w") as f:
json.dump(meta, f, indent=2)
def run(args, check=True, cwd=None):
"""Run a subprocess and return its result."""
result = subprocess.run(
args,
capture_output=True,
text=True,
cwd=cwd or SCRIPT_DIR,
)
if check and result.returncode != 0:
print(f" FAIL: {' '.join(str(a) for a in args)}", file=sys.stderr)
print(f" stdout: {result.stdout[:500]}", file=sys.stderr)
print(f" stderr: {result.stderr[:500]}", file=sys.stderr)
return result
def test_skeleton_grouping():
"""2 isomorphic graphs get the same skeleton hash; 1 distinct differs."""
sys.path.insert(0, str(SCRIPT_DIR))
from bundle_utils import skeleton_hash
g1 = _make_graph("Relu", [0, 1], [2, 3, 4, 5], [60, 20, 5, 1], "float")
g2 = _make_graph("Relu", [0, 1], [4, 6, 8, 10], [480, 80, 10, 1], "half")
g3 = _make_graph("Conv", [0, 1, 2], [2, 3, 4, 5], [60, 20, 5, 1], "float")
h1 = skeleton_hash(g1)
h2 = skeleton_hash(g2)
h3 = skeleton_hash(g3)
assert h1 == h2, f"isomorphic graphs should match: {h1} vs {h2}"
assert h1 != h3, f"distinct graphs should differ: {h1} vs {h3}"
print(" PASS: skeleton_grouping")
def test_place_and_verify():
"""End-to-end: capture -> place -> verify."""
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
capture_dir = tmp / "captured"
bundle_dir = tmp / "bundles"
g1 = _make_graph("Relu", [0, 1], [2, 3, 4, 5], [60, 20, 5, 1], "float")
g2 = _make_graph("Relu", [0, 1], [4, 6, 8, 10], [480, 80, 10, 1], "half")
g3 = _make_graph("Conv", [0, 1, 2], [2, 3, 4, 5], [60, 20, 5, 1], "float")
_write_captured(capture_dir, "Smoke/IntegrationGpuReluFp32", "small_fp32", g1)
_write_captured(capture_dir, "Smoke/IntegrationGpuReluFp16", "small_fp16", g2)
_write_captured(capture_dir, "Smoke/IntegrationGpuConvFp32", "small_conv", g3)
# Hop B: place
r = run(
[
sys.executable,
str(SCRIPT_DIR / "place_bundles.py"),
"--capture-dir",
str(capture_dir),
"--output-dir",
str(bundle_dir),
]
)
assert r.returncode == 0, f"place_bundles failed: {r.stderr}"
# Check output structure: should have 1 sweep (2 cases) + 1 standalone
sweeps = list(bundle_dir.rglob("sweep.json"))
templates = list(bundle_dir.rglob("graph.template.json"))
standalones = [
p
for p in bundle_dir.rglob("*.json")
if p.name not in ("sweep.json", "graph.template.json")
and not p.name.endswith(".meta.json")
]
assert len(sweeps) == 1, f"expected 1 sweep, got {len(sweeps)}: {sweeps}"
assert len(templates) == 1, f"expected 1 template, got {len(templates)}"
assert len(standalones) == 1, f"expected 1 standalone, got {len(standalones)}"
with open(sweeps[0]) as f:
sweep = json.load(f)
assert (
len(sweep["cases"]) == 2
), f"expected 2 sweep cases, got {len(sweep['cases'])}"
# Hop C: verify
r = run(
[
sys.executable,
str(SCRIPT_DIR / "verify_migration.py"),
"--capture-dir",
str(capture_dir),
"--bundle-dir",
str(bundle_dir),
]
)
assert r.returncode == 0, f"verify_migration failed: {r.stderr}"
print(" PASS: place_and_verify")
def test_import_dedup():
"""import_graph detects exact dups and appends new cases."""
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
bundle_dir = tmp / "bundles"
bundle_dir.mkdir()
g1 = _make_graph("Relu", [0, 1], [2, 3, 4, 5], [60, 20, 5, 1], "float")
g2 = _make_graph("Relu", [0, 1], [4, 6, 8, 10], [480, 80, 10, 1], "half")
g_dup = _make_graph("Relu", [0, 1], [2, 3, 4, 5], [60, 20, 5, 1], "float")
# First import
graph_path = tmp / "g1.json"
with open(graph_path, "w") as f:
json.dump(g1, f)
r = run(
[
sys.executable,
str(SCRIPT_DIR / "import_graph.py"),
"--graph",
str(graph_path),
"--bundle-dir",
str(bundle_dir),
]
)
assert r.returncode == 0, f"first import failed: {r.stderr}"
# Import exact dup -> should skip
dup_path = tmp / "g_dup.json"
with open(dup_path, "w") as f:
json.dump(g_dup, f)
r = run(
[
sys.executable,
str(SCRIPT_DIR / "import_graph.py"),
"--graph",
str(dup_path),
"--bundle-dir",
str(bundle_dir),
]
)
assert r.returncode == 0, f"dup import should skip (rc=0): {r.stderr}"
assert "DUPLICATE" in r.stderr, "should report DUPLICATE"
# Import exact dup with --strict -> should fail
r = run(
[
sys.executable,
str(SCRIPT_DIR / "import_graph.py"),
"--graph",
str(dup_path),
"--bundle-dir",
str(bundle_dir),
"--strict",
],
check=False,
)
assert r.returncode == 1, "strict dup should exit 1"
# Import structural match with new knobs -> should append
g2_path = tmp / "g2.json"
with open(g2_path, "w") as f:
json.dump(g2, f)
r = run(
[
sys.executable,
str(SCRIPT_DIR / "import_graph.py"),
"--graph",
str(g2_path),
"--bundle-dir",
str(bundle_dir),
]
)
assert r.returncode == 0, f"new-case import failed: {r.stderr}"
assert "appended" in r.stderr, "should report appended"
# Verify the sweep now has 2 cases
sweeps = list(bundle_dir.rglob("sweep.json"))
assert len(sweeps) == 1
with open(sweeps[0]) as f:
sweep = json.load(f)
assert (
len(sweep["cases"]) == 2
), f"expected 2 cases after append, got {len(sweep['cases'])}"
print(" PASS: import_dedup")
def test_round_trip_expansion():
"""expand(template, values) reproduces the original graph exactly."""
sys.path.insert(0, str(SCRIPT_DIR))
from bundle_utils import canon, expand
g = _make_graph("Relu", [0, 1], [2, 3, 4, 5], [60, 20, 5, 1], "float")
template = {
"nodes": [
{
"type": "ReluAttributes",
"name": "",
"inputs": {"x_tensor_uid": 0},
"outputs": {"y_tensor_uid": 1},
}
],
"tensors": [
{
"uid": 0,
"name": "",
"dims": "${case.dims}",
"strides": "${case.strides}",
"data_type": "${case.data_type}",
"virtual": False,
},
{
"uid": 1,
"name": "",
"dims": "${case.dims}",
"strides": "${case.strides}",
"data_type": "${case.data_type}",
"virtual": False,
},
],
"io_data_type": "${case.io_data_type}",
"compute_data_type": "float",
"intermediate_data_type": "float",
"name": "",
}
values = {
"io_data_type": "float",
"tensors": [
{
"uid": 0,
"dims": [2, 3, 4, 5],
"strides": [60, 20, 5, 1],
"data_type": "float",
},
{
"uid": 1,
"dims": [2, 3, 4, 5],
"strides": [60, 20, 5, 1],
"data_type": "float",
},
],
}
expanded = expand(template, values)
assert canon(expanded) == canon(g), "round-trip expansion mismatch"
print(" PASS: round_trip_expansion")
def test_case_ids():
"""Case ids are stable, bounded, and unique even for input-range-only diffs."""
sys.path.insert(0, str(SCRIPT_DIR))
from bundle_utils import assign_case_ids
def case(dims, strides, dtype, inputs=None, attrs=None):
v = {
"io_data_type": dtype,
"tensors": [
{"uid": 0, "dims": dims, "strides": strides, "data_type": dtype}
],
}
if attrs:
v["attributes"] = attrs
return {"values": v, "metadata": {"seed": 1, "inputs": inputs or {}}}
# 1. Stability: same input list, run twice -> identical ids, order-independent.
a = [
case([2, 3, 4, 5], [60, 20, 5, 1], "float"),
case([8, 3, 4, 5], [60, 20, 5, 1], "half"),
]
b = [dict(c) for c in reversed(a)] # reversed order
assign_case_ids(a)
assign_case_ids(b)
ids_a = {id(x): x["id"] for x in a}
# reversed list must yield the same id per identical case content
assert a[0]["id"] != a[1]["id"], "distinct cases must get distinct ids"
# re-run on a fresh copy -> identical
a2 = [
case([2, 3, 4, 5], [60, 20, 5, 1], "float"),
case([8, 3, 4, 5], [60, 20, 5, 1], "half"),
]
assign_case_ids(a2)
assert [c["id"] for c in a] == [c["id"] for c in a2], "ids must be deterministic"
# 2. Input-range-only difference: identical graph, different bias range.
# Must NOT collide — this is the case the readable tokens cannot express.
r1 = case([2, 3, 4, 5], [60, 20, 5, 1], "float", inputs={"1": {"lo": -1, "hi": 1}})
r2 = case([2, 3, 4, 5], [60, 20, 5, 1], "float", inputs={"1": {"lo": -2, "hi": 2}})
pair = [r1, r2]
assign_case_ids(pair)
assert pair[0]["id"] != pair[1]["id"], (
"cases differing only in input range must get distinct ids "
f"(got {pair[0]['id']!r} twice)"
)
# 3. Uniqueness across a larger mixed sweep.
many = [
case(
[1, 16, 3, 3], [144, 9, 3, 1], "half", attrs={"parameters__stride": [s, s]}
)
for s in (1, 2)
] + [r1, r2]
assign_case_ids(many)
assert len({c["id"] for c in many}) == len(many), "all ids must be unique"
# 4. Bounded length: no id may be an unusable gtest name.
assert all(len(c["id"]) <= 120 for c in many), "ids must stay bounded"
print(" PASS: case_ids")
def test_inputs_uid_canonicalized_by_name():
"""Cases with shuffled auto-assigned UIDs compress, fill specs follow names.
The C++ builder assigns UIDs non-deterministically (GraphTensorIds.hpp
iterates an unordered_set), so two captures of the SAME topology can label
the same logical tensor with different UIDs. This fixture reproduces that:
both cases are the same ternary op, but case1 has x/dy/scale UIDs shuffled
relative to case0, with each case's fill-spec map keyed to match itself.
place_bundles canonicalizes UIDs by tensor NAME, so:
* both cases collapse into ONE sweep (compression works despite shuffle),
* each fill spec stays bound to its tensor (x keeps [-1,1] etc.) even
though it moved to a different UID.
Verified per-tensor by re-keying the placed inputs by name.
"""
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
capture_dir = tmp / "captured"
bundle_dir = tmp / "bundles"
def ternary_graph(uids, dims, strides):
"""uids = {name: uid}; out is virtual."""
return {
"nodes": [
{
"type": "TernaryAttributes",
"name": "",
"inputs": {
"dy_tensor_uid": uids["dy"],
"scale_tensor_uid": uids["scale"],
"x_tensor_uid": uids["x"],
},
"outputs": {"out_tensor_uid": uids["out"]},
}
],
"tensors": [
{
"uid": uids[nm],
"name": nm,
"dims": dims,
"strides": strides,
"data_type": "float",
"virtual": (nm == "out"),
}
for nm in ("x", "dy", "scale", "out")
],
"io_data_type": "float",
"compute_data_type": "float",
"intermediate_data_type": "float",
"name": "",
}
# Fill spec per tensor, keyed by that case's own UID for that tensor.
specs = {
"x": {"kind": "free", "lo": -1.0, "hi": 1.0},
"dy": {"kind": "free", "lo": -0.1, "hi": 0.1},
"scale": {"kind": "free", "lo": -0.2, "hi": 0.2},
}
def meta_for(uids):
return {
"format_version": 1,
"seed": 7,
"inputs": {str(uids[nm]): specs[nm] for nm in ("x", "dy", "scale")},
}
# case0 and case1 describe the same op with DIFFERENT UID assignments.
uids0 = {"x": 1, "dy": 2, "scale": 3, "out": 4}
uids1 = {"x": 3, "dy": 4, "scale": 1, "out": 2} # shuffled
_write_captured(
capture_dir,
"Smoke/IntegrationGpuTernary",
"case0",
ternary_graph(uids0, [2, 3, 4, 5], [60, 20, 5, 1]),
meta=meta_for(uids0),
)
_write_captured(
capture_dir,
"Smoke/IntegrationGpuTernary",
"case1",
ternary_graph(uids1, [4, 6, 8, 10], [480, 80, 10, 1]),
meta=meta_for(uids1),
)
r = run(
[
sys.executable,
str(SCRIPT_DIR / "place_bundles.py"),
"--capture-dir",
str(capture_dir),
"--output-dir",
str(bundle_dir),
]
)
assert r.returncode == 0, f"place_bundles failed: {r.stderr}"
# Compression: shuffled-UID siblings must land in ONE sweep, not two.
sweeps = list(bundle_dir.rglob("sweep.json"))
standalones = [
p
for p in bundle_dir.rglob("*.json")
if p.name not in ("sweep.json", "graph.template.json")
and not p.name.endswith(".meta.json")
]
assert len(sweeps) == 1, f"expected 1 sweep (compression), got {sweeps}"
assert not standalones, f"no standalone fallback expected: {standalones}"
with open(sweeps[0]) as f:
sweep = json.load(f)
assert len(sweep["cases"]) == 2, f"expected 2 cases, got {len(sweep['cases'])}"
# Fill spec must follow the tensor by name, not by original UID. Re-key
# placed inputs through the template's uid->name map and check each case.
templates = list(bundle_dir.rglob("graph.template.json"))
with open(templates[0]) as f:
tpl = json.load(f)
uid_to_name = {t["uid"]: t["name"] for t in tpl["tensors"]}
for case in sweep["cases"]:
by_name = {
uid_to_name[int(u)]: spec
for u, spec in case["metadata"]["inputs"].items()
}
assert by_name["x"] == specs["x"], f"x spec moved: {by_name}"
assert by_name["dy"] == specs["dy"], f"dy spec moved: {by_name}"
assert by_name["scale"] == specs["scale"], f"scale spec moved: {by_name}"
# Hop C must pass (it compares metadata inputs by name).
r = run(
[
sys.executable,
str(SCRIPT_DIR / "verify_migration.py"),
"--capture-dir",
str(capture_dir),
"--bundle-dir",
str(bundle_dir),
]
)
assert r.returncode == 0, f"verify_migration failed: {r.stderr}"
print(" PASS: inputs_uid_canonicalized_by_name")
def test_import_inputs_uid_canonicalized_by_name():
"""import_graph canonicalizes UIDs by name so imports match placed sweeps.
A graph captured with one UID assignment and re-imported with a shuffled
assignment must dedup against the existing case (same topology, same fill
specs per tensor) rather than appearing as a spurious new case. This mirrors
test_inputs_uid_canonicalized_by_name for the incremental import path.
"""
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
bundle_dir = tmp / "bundles"
bundle_dir.mkdir()
def ternary_graph(uids):
return {
"nodes": [
{
"type": "TernaryAttributes",
"name": "",
"inputs": {
"dy_tensor_uid": uids["dy"],
"scale_tensor_uid": uids["scale"],
"x_tensor_uid": uids["x"],
},
"outputs": {"out_tensor_uid": uids["out"]},
}
],
"tensors": [
{
"uid": uids[nm],
"name": nm,
"dims": [2, 3, 4, 5],
"strides": [60, 20, 5, 1],
"data_type": "float",
"virtual": (nm == "out"),
}
for nm in ("x", "dy", "scale", "out")
],
"io_data_type": "float",
"compute_data_type": "float",
"intermediate_data_type": "float",
"name": "",
}
specs = {
"x": {"kind": "free", "lo": -1.0, "hi": 1.0},
"dy": {"kind": "free", "lo": -0.1, "hi": 0.1},
"scale": {"kind": "free", "lo": -0.2, "hi": 0.2},
}
def inputs_for(uids):
return {str(uids[nm]): specs[nm] for nm in ("x", "dy", "scale")}
uids0 = {"x": 1, "dy": 2, "scale": 3, "out": 4}
uids1 = {"x": 3, "dy": 4, "scale": 1, "out": 2} # shuffled
# First import (case0).
g0_path = tmp / "g0.json"
with open(g0_path, "w") as f:
json.dump(ternary_graph(uids0), f)
r = run(
[
sys.executable,
str(SCRIPT_DIR / "import_graph.py"),
"--graph",
str(g0_path),
"--bundle-dir",
str(bundle_dir),
"--seed",
"7",
"--meta",
f"inputs={json.dumps(inputs_for(uids0))}",
]
)
assert r.returncode == 0, f"first import failed: {r.stderr}"
# Re-import the same op with SHUFFLED UIDs + matching specs -> must dedup,
# because canonicalization by name makes it identical to case0.
g1_path = tmp / "g1.json"
with open(g1_path, "w") as f:
json.dump(ternary_graph(uids1), f)
r = run(
[
sys.executable,
str(SCRIPT_DIR / "import_graph.py"),
"--graph",
str(g1_path),
"--bundle-dir",
str(bundle_dir),
"--seed",
"7",
"--meta",
f"inputs={json.dumps(inputs_for(uids1))}",
]
)
assert r.returncode == 0, f"shuffled re-import failed: {r.stderr}"
assert "DUPLICATE" in r.stderr, (
"shuffled re-import of the same op must dedup by name, " f"got:\n{r.stderr}"
)
# Fill spec must be attached to the correctly-named tensor in the bundle.
sweeps = list(bundle_dir.rglob("sweep.json"))
assert len(sweeps) == 1, f"expected 1 sweep, got {sweeps}"
with open(sweeps[0]) as f:
sweep = json.load(f)
templates = list(bundle_dir.rglob("graph.template.json"))
with open(templates[0]) as f:
tpl = json.load(f)
uid_to_name = {t["uid"]: t["name"] for t in tpl["tensors"]}
by_name = {
uid_to_name[int(u)]: spec
for u, spec in sweep["cases"][0]["metadata"]["inputs"].items()
}
assert by_name["x"] == specs["x"], f"x spec misbound: {by_name}"
print(" PASS: import_inputs_uid_canonicalized_by_name")
def _write_sweep(bundle_dir, tier, operation, variant, cases):
"""Write a minimal sweep.json under <tier>/<operation>/<variant>/."""
d = bundle_dir / tier / operation / variant
d.mkdir(parents=True, exist_ok=True)
with open(d / "sweep.json", "w") as f:
json.dump({"version": 1, "cases": cases}, f, indent=2)
def _gtest_json(cases):
"""Build a GTest --gtest_output=json document from {suite: {case: status}}."""
suites = []
for suite, by_case in cases.items():
tc = []
for case, status in by_case.items():
entry = {"name": case, "status": "RUN", "result": "COMPLETED"}
if status == "SKIP":
entry["status"] = "NOTRUN"
entry["result"] = "SKIPPED"
elif status == "FAIL":
entry["failures"] = [{"failure": "x"}]
tc.append(entry)
suites.append({"name": suite, "testsuite": tc})
return {"testsuites": suites}
def test_diff_coverage_suite_qualified_join():
"""diff_coverage joins on (suite, case_id), not the collision-prone case_id.
The same bare case_id can appear in sibling suites (e.g. Default and
Variant2). If the join used the bare id, a PASS in one suite would falsely
satisfy the coverage requirement for a C++ test whose bundle lives in — and
is SKIPPED in — the other. Here two suites share case id ``shape_a`` mapped
to two different C++ sources; only the Default one passes as a bundle. The
Variant2 C++ source must be reported as a regression, proving the bare id
does not leak coverage across suites.
"""
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
bundle_dir = tmp / "bundles"
bundle_dir.mkdir()
# Same bare case id "shape_a" in two suites, distinct C++ sources.
_write_sweep(
bundle_dir,
"full",
"Batchnorm",
"Default",
[
{
"id": "shape_a",
"values": {},
"metadata": {
"reference_source": (
"c++ integration suite: Full/BnDefault.Correctness_0"
)
},
}
],
)
_write_sweep(
bundle_dir,
"full",
"Batchnorm",
"Variant2",
[
{
"id": "shape_a",
"values": {},
"metadata": {
"reference_source": (
"c++ integration suite: Full/BnVariant2.Correctness_0"
)
},
}
],
)
# C++: both sources PASSED.
cpp = _gtest_json(
{
"Full/BnDefault": {"Correctness/0": "PASS"},
"Full/BnVariant2": {"Correctness/0": "PASS"},
}
)
# Bundle: Default's shape_a PASSED, Variant2's shape_a SKIPPED.
bundle = _gtest_json(
{
"full_Batchnorm_Default": {"shape_a": "PASS"},
"full_Batchnorm_Variant2": {"shape_a": "SKIP"},
}
)
cpp_path = tmp / "cpp.json"
bundle_path = tmp / "bundle.json"
with open(cpp_path, "w") as f:
json.dump(cpp, f)
with open(bundle_path, "w") as f:
json.dump(bundle, f)
r = run(
[
sys.executable,
str(SCRIPT_DIR / "diff_coverage.py"),
"--cpp",
str(cpp_path),
"--bundle",
str(bundle_path),
"--bundle-dir",
str(bundle_dir),
],
check=False,
)
# Variant2 source must be flagged as a regression (bundle SKIPPED it),
# even though a same-named case passed in Default.
assert r.returncode == 1, (
"expected regression exit(1) when a cross-suite case_id collides; "
f"got rc={r.returncode}\n{r.stderr}"
)
assert (
"BnVariant2" in r.stderr
), f"Variant2 source should be the reported regression:\n{r.stderr}"
assert (
"BnDefault" not in r.stderr.split("regressions:")[-1]
), f"Default source passed and must not be a regression:\n{r.stderr}"
print(" PASS: diff_coverage_suite_qualified_join")
def main() -> int:
verbose = "-v" in sys.argv
failures = 0
tests = [
test_skeleton_grouping,
test_round_trip_expansion,
test_case_ids,
test_place_and_verify,
test_import_dedup,
test_inputs_uid_canonicalized_by_name,
test_import_inputs_uid_canonicalized_by_name,
test_diff_coverage_suite_qualified_join,
]
for t in tests:
try:
t()
except Exception as e:
print(f" FAIL: {t.__name__}: {e}", file=sys.stderr)
if verbose:
import traceback
traceback.print_exc()
failures += 1
print(f"\n{len(tests) - failures}/{len(tests)} passed")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())