Compare commits

...

1 Commits

Author SHA1 Message Date
overtrue bf00d8fd27 test(e2e): add platform-safe selection updates 2026-08-23 09:04:20 +08:00
2 changed files with 87 additions and 13 deletions
+6 -1
View File
@@ -278,4 +278,9 @@ listed by `cargo nextest list -p e2e_test`. Regenerate it when adding or
moving e2e tests so acceptance numbers in the test-strategy issues moving e2e tests so acceptance numbers in the test-strategy issues
(backlog#1147#1155) stay auditable. When a profile membership change is (backlog#1147#1155) stay auditable. When a profile membership change is
intentional, review its JSON listing before updating the matching intentional, review its JSON listing before updating the matching
`.config/e2e-*-selection.txt` test-ID digest. `.config/e2e-*-selection.txt` test-ID digest. Update only the platform that
produced the listing:
```bash
python3 scripts/check_test_wiring.py --update-profile e2e-full /path/to/listing.json linux
```
+81 -12
View File
@@ -219,7 +219,7 @@ def check_s3_tests_runner(root: Path) -> list[str]:
return [] return []
def profile_selection(root: Path, profile: str) -> str: def profile_selection_entries(root: Path, profile: str) -> tuple[Path, list[str], dict[str, str]]:
if not re.fullmatch(r"e2e-[a-z0-9-]+", profile): if not re.fullmatch(r"e2e-[a-z0-9-]+", profile):
raise ValueError(f"invalid e2e profile name: {profile}") raise ValueError(f"invalid e2e profile name: {profile}")
path = root / f".config/{profile}-selection.txt" path = root / f".config/{profile}-selection.txt"
@@ -227,6 +227,11 @@ def profile_selection(root: Path, profile: str) -> str:
values = dict(line.split("=", 1) for line in lines if "=" in line) values = dict(line.split("=", 1) for line in lines if "=" in line)
if len(values) != len(lines) or any(not re.fullmatch(r"sha256(?:-[a-z0-9]+)?", key) for key in values): if len(values) != len(lines) or any(not re.fullmatch(r"sha256(?:-[a-z0-9]+)?", key) for key in values):
raise ValueError(f"{path.relative_to(root).as_posix()}: invalid sha256 entry") raise ValueError(f"{path.relative_to(root).as_posix()}: invalid sha256 entry")
return path, lines, values
def profile_selection(root: Path, profile: str) -> str:
path, _, values = profile_selection_entries(root, profile)
key = f"sha256-{sys.platform}" key = f"sha256-{sys.platform}"
digest = values.get(key, values.get("sha256", "")) digest = values.get(key, values.get("sha256", ""))
if not re.fullmatch(r"[0-9a-f]{64}", digest): if not re.fullmatch(r"[0-9a-f]{64}", digest):
@@ -234,6 +239,29 @@ def profile_selection(root: Path, profile: str) -> str:
return digest return digest
def profile_listing_digest(listing: Path) -> tuple[int, str]:
data = json.loads(listing.read_text())
selected = sorted(
f"{suite_id}::{test_name}"
for suite_id, suite in data["rust-suites"].items()
for test_name, testcase in suite["testcases"].items()
if testcase.get("filter-match", {}).get("status") == "matches"
)
return len(selected), hashlib.sha256(("\n".join(selected) + "\n").encode()).hexdigest()
def update_profile_selection(root: Path, profile: str, listing: Path, platform: str) -> tuple[int, str, str]:
if not re.fullmatch(r"[a-z0-9]+", platform):
raise ValueError(f"invalid platform name: {platform}")
path, lines, values = profile_selection_entries(root, profile)
key = "sha256" if "sha256" in values else f"sha256-{platform}"
if key not in values:
raise ValueError(f"{path.relative_to(root).as_posix()}: missing {key} entry")
count, digest = profile_listing_digest(listing)
path.write_text("\n".join(f"{key}={digest}" if line.startswith(f"{key}=") else line for line in lines) + "\n")
return count, digest, key
def check_profile_definitions(root: Path) -> list[str]: def check_profile_definitions(root: Path) -> list[str]:
config = tomllib.loads((root / ".config/nextest.toml").read_text()) config = tomllib.loads((root / ".config/nextest.toml").read_text())
profiles = { profiles = {
@@ -547,22 +575,15 @@ def check_scheduled_alerts(root: Path) -> list[str]:
def check_profile_listing(root: Path, profile: str, listing: Path) -> list[str]: def check_profile_listing(root: Path, profile: str, listing: Path) -> list[str]:
try: try:
expected_digest = profile_selection(root, profile) expected_digest = profile_selection(root, profile)
data = json.loads(listing.read_text()) count, digest = profile_listing_digest(listing)
selected = sorted(
f"{suite_id}::{test_name}"
for suite_id, suite in data["rust-suites"].items()
for test_name, testcase in suite["testcases"].items()
if testcase.get("filter-match", {}).get("status") == "matches"
)
digest = hashlib.sha256(("\n".join(selected) + "\n").encode()).hexdigest()
except (FileNotFoundError, KeyError, TypeError, ValueError, json.JSONDecodeError) as error: except (FileNotFoundError, KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
return [f"cannot read {profile} nextest listing: {error}"] return [f"cannot read {profile} nextest listing: {error}"]
if digest != expected_digest: if digest != expected_digest:
return [ return [
f"{profile} selection changed: count={len(selected)} sha256={digest}; " f"{profile} selection changed: count={count} sha256={digest}; "
f"expected sha256={expected_digest}" f"expected sha256={expected_digest}"
] ]
print(f"{profile} selection OK: {len(selected)} tests, sha256={digest}") print(f"{profile} selection OK: {count} tests, sha256={digest}")
return [] return []
@@ -707,6 +728,45 @@ class SelfTests(unittest.TestCase):
with mock.patch.object(sys, "platform", "linux"): with mock.patch.object(sys, "platform", "linux"):
self.assertEqual(len(check_profile_listing(root, "e2e-full", listing)), 1) self.assertEqual(len(check_profile_listing(root, "e2e-full", listing)), 1)
def test_update_profile_selection_changes_only_requested_platform(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / ".config").mkdir()
darwin_digest = "a" * 64
(root / ".config/e2e-full-selection.txt").write_text(
f"sha256-darwin={darwin_digest}\nsha256-linux={'b' * 64}\n"
)
listing = root / "listing.json"
listing.write_text(
json.dumps(
{
"rust-suites": {
"suite": {
"testcases": {"linux": {"filter-match": {"status": "matches"}}}
}
}
}
)
)
count, digest, key = update_profile_selection(root, "e2e-full", listing, "linux")
self.assertEqual(count, 1)
self.assertEqual(key, "sha256-linux")
self.assertEqual(
(root / ".config/e2e-full-selection.txt").read_text(),
f"sha256-darwin={darwin_digest}\nsha256-linux={digest}\n",
)
with mock.patch.object(sys, "platform", "linux"):
self.assertEqual(check_profile_listing(root, "e2e-full", listing), [])
selection = root / ".config/e2e-smoke-selection.txt"
selection.write_text(f"sha256={'a' * 64}\n")
count, digest, key = update_profile_selection(root, "e2e-smoke", listing, "linux")
self.assertEqual((count, key), (1, "sha256"))
self.assertEqual(selection.read_text(), f"sha256={digest}\n")
def test_scheduled_alerts_require_completion_watchdog(self) -> None: def test_scheduled_alerts_require_completion_watchdog(self) -> None:
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp) root = Path(tmp)
@@ -984,9 +1044,18 @@ def main() -> int:
print(f"ERROR: {error}", file=sys.stderr) print(f"ERROR: {error}", file=sys.stderr)
return 1 return 1
return 0 return 0
if len(sys.argv) == 5 and sys.argv[1] == "--update-profile":
try:
count, digest, key = update_profile_selection(ROOT, sys.argv[2], Path(sys.argv[3]), sys.argv[4])
except (FileNotFoundError, KeyError, TypeError, ValueError, json.JSONDecodeError) as error:
print(f"ERROR: cannot update {sys.argv[2]} selection: {error}", file=sys.stderr)
return 1
print(f"Updated .config/{sys.argv[2]}-selection.txt: count={count} {key}={digest}")
return 0
if sys.argv[1:]: if sys.argv[1:]:
print( print(
"usage: check_test_wiring.py [--self-test | --check-profile PROFILE LISTING]", "usage: check_test_wiring.py [--self-test | --check-profile PROFILE LISTING | "
"--update-profile PROFILE LISTING PLATFORM]",
file=sys.stderr, file=sys.stderr,
) )
return 2 return 2