Files
workinf_Blender_Wasm/tools/web/record-wbv2-full-smoke.py
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-10 11:18:01 +08:00

185 lines
12 KiB
Python

#!/usr/bin/env python3
"""Publish a completed full run with explicitly matched conditional-case receipts."""
import argparse
import hashlib
import importlib.util
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
BASE = ROOT / "tests/golden/WBV2-P1424-001"
spec = importlib.util.spec_from_file_location("full_smoke", ROOT / "tools/web/run-wbv2-full-smoke.py")
runner = importlib.util.module_from_spec(spec)
spec.loader.exec_module(runner)
write_json, artifact, now = runner.write_json, runner.artifact, runner.now
def load(path):
return json.loads(path.read_text())
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("run", type=Path)
parser.add_argument("--supplement", type=Path, action="append", default=[])
parser.add_argument("--repair", type=Path, action="append", default=[],
help="Complete spec receipt after a test-only correction; runtime input drift is rejected")
args = parser.parse_args()
run = args.run.resolve()
original = load(run / "summary.json")
inputs = load(run / "inputs.json")
blockers = []
if original.get("status") in ("RUNNING", "INTERRUPTED") or len(original.get("groups", [])) != 3:
raise RuntimeError("The main and both dedicated configurations must finish before publication")
drift = [ref["path"] for ref in inputs if not (ROOT / ref["path"]).is_file() or digest(ROOT / ref["path"]) != ref["sha256"]]
supplement_rows = {}
supplement_receipts = []
repair_rows = {}
repair_receipts = []
repair_paths = set()
for kind, folder in [("supplement", p) for p in args.supplement] + [("repair", p) for p in args.repair]:
folder = folder.resolve()
receipt = load(folder / "result.json")
report = load(folder / "playwright.json")
if receipt.get("status") != "PASS" or receipt.get("exitCode") != 0 or report.get("errors"):
raise RuntimeError(f"Supplement did not pass: {folder}")
if receipt["source"]["sha256"] != digest(ROOT / receipt["source"]["path"]):
raise RuntimeError(f"Supplement source changed: {folder}")
for ref in receipt["artifacts"]:
if digest(ROOT / ref["path"]) != ref["sha256"]:
raise RuntimeError(f"Supplement artifact changed: {ref['path']}")
for case in runner.rows(report.get("suites", [])):
if case["status"] != "expected":
raise RuntimeError(f"Supplement contains an unpassed case: {case['title']}")
key = (case["spec"], case["title"])
target = supplement_rows if kind == "supplement" else repair_rows
if key in target:
raise RuntimeError(f"Duplicate supplement: {key}")
target[key] = {**case, "receipt": artifact(folder / "result.json")}
if kind == "supplement":
supplement_receipts.append(artifact(folder / "result.json"))
else:
source = receipt["source"]["path"]
if not source.startswith("web/tests/e2e/") or not source.endswith(".spec.ts") or source not in drift:
raise RuntimeError("Only changed E2E spec files can use a test-only repair receipt")
repair_paths.add(source)
repair_receipts.append({"source": receipt["source"], "receipt": artifact(folder / "result.json"),
"originalSource": next(ref for ref in inputs if ref["path"] == source)})
runtime_drift = [name for name in drift if name not in repair_paths]
if runtime_drift or not original.get("inputsUnchanged"):
blockers.append({"kind": "INPUT_DRIFT", "paths": runtime_drift,
"nextAction": "Run a new complete batch on the changed runtime inputs"})
repaired_specs = {Path(name).name for name in repair_paths}
for name in repaired_specs:
previous = [case for group in original["groups"] for case in group.get("tests", []) if case["spec"] == name]
replacement = [case for (filename, _), case in repair_rows.items() if filename == name]
if not previous or len(replacement) < len(previous):
raise RuntimeError(f"Repair must execute the complete spec: {name}")
unchanged_titles = {case["title"] for case in previous if case["status"] == "expected"}
if not unchanged_titles.issubset({case["title"] for case in replacement}):
raise RuntimeError(f"Repair removed a previously passing test: {name}")
cases = []
conditional = []
for group in original["groups"]:
ref = group.get("report")
if ref and digest(ROOT / ref["path"]) != ref["sha256"]:
raise RuntimeError(f"Group report changed: {group['group']}")
if group.get("exitCode") not in (0, 1) or group.get("errors") or group.get("status") not in ("PASS", "INCOMPLETE", "FAILED"):
blockers.append({"kind": "GROUP_FAILURE", "group": group["group"], "status": group["status"],
"nextAction": "Inspect the group's JSON errors and failure attachments"})
if group.get("exitCode") == 1:
failed_in_group = [case for case in group.get("tests", []) if case["status"] not in ("expected", "skipped")]
if not failed_in_group or any(case["spec"] not in repaired_specs for case in failed_in_group):
blockers.append({"kind": "UNRESOLVED_GROUP_EXIT", "group": group["group"],
"nextAction": "Resolve every failing spec or the non-test runner error"})
for case in group.get("tests", []):
if case["spec"] in repaired_specs:
continue
key = (case["spec"], case["title"])
if case["status"] == "skipped" and key in supplement_rows:
replacement = supplement_rows.pop(key)
conditional.append({"spec": case["spec"], "title": case["title"],
"originalStatus": "skipped", "supplement": replacement["receipt"]})
cases.append(replacement)
else:
cases.append(case)
cases.extend(repair_rows.values())
if supplement_rows:
raise RuntimeError("Supplement cases must match a collected conditional skip exactly")
failed = [case for case in cases if case["status"] != "expected"]
blockers.extend({"kind": "TEST_NOT_PASSED", **case,
"nextAction": "Repair the concrete failing workflow using its test and local Blender source"} for case in failed)
expected = set(original["expectedSpecs"])
observed = {case["spec"] for case in cases}
if expected != observed:
blockers.append({"kind": "SPEC_COLLECTION_GAP", "missing": sorted(expected - observed),
"unexpected": sorted(observed - expected), "nextAction": "Supply the missing configuration or fixture and execute it"})
catalog = load(ROOT / "web/app/src/capabilities/blender-feature-route-catalog.json")
soak_path = run / "main/soak.json"
soak = load(soak_path) if soak_path.is_file() else {}
if soak.get("profile") != "FORMAL" or soak.get("status") != "READY" or soak.get("actualDurationMs", 0) < 1_800_000:
blockers.append({"kind": "FORMAL_SOAK_INCOMPLETE", "nextAction": "Complete the formal 30-minute editing and recovery test"})
report = {
"schemaVersion": 1, "task": "WBV2-P1424-001", "updatedAt": now(),
"scope": "Complete Chromium E2E batch plus both dedicated configurations and explicitly configured conditional cases",
"status": "PASS" if not blockers else "FAILED", "fullDesktopParity": "NOT_ESTABLISHED",
"totalSpecs": len(expected), "passedSpecs": sum(all(c["status"] == "expected" for c in cases if c["spec"] == name) for name in expected & observed),
"totalTests": len(cases), "passedTests": sum(c["status"] == "expected" for c in cases),
"failedTests": len(failed), "unresolvedSkips": sum(c["status"] == "skipped" for c in failed),
"batch": artifact(run / "summary.json"), "inputs": artifact(run / "inputs.json"),
"conditionalCases": conditional, "supplements": supplement_receipts,
"testOnlyRepairs": repair_receipts,
"formalSoak": {"receipt": artifact(soak_path) if soak else None,
**{key: soak.get(key) for key in ("actualDurationMs", "cycles", "autosaves", "reopens",
"finalSnapshotCount", "pageErrors", "observed", "limits")}},
"historical": {"totalSpecs": 243, "passed": 228, "failed": 13, "skippedConfigurations": 2},
"blockers": blockers,
"sourceInventory": {"unitCount": catalog["unitCount"],
"verifiedUnits": catalog["behavioralEvidence"]["verifiedUnits"],
"unverifiedUnits": catalog["behavioralEvidence"]["unverifiedUnits"],
"sourceTreeSha256": catalog["sourceTreeSha256"], "boundUnitDelta": 0,
"freshness": "Existing catalog; smoke results do not increase source-unit coverage"},
"limitations": ["Conditional cases retain their original skipped result and an explicit configured passing receipt.",
"Test-only repairs replace complete spec results; original failures and both test-source hashes remain recorded.",
"Offline coverage excludes server-native services and file://.",
"Passing this E2E suite is not proof of complete Blender desktop parity."],
}
report_path = BASE / "full-smoke-closure-report.json"
write_json(report_path, report)
latest_path = BASE / "full-smoke-latest.json"
write_json(latest_path, {"status": report["status"], "report": artifact(report_path),
"batch": report["batch"], "rawBatchStatus": original["status"]})
product_path = BASE / "product-implementation-report.json"
product = load(product_path)
product.setdefault("historicalFullE2ESmoke", product.get("fullE2ESmoke"))
product["fullE2ESmoke"] = {"status": report["status"], "countUnit": "spec_files", "total": report["totalSpecs"],
"passed": report["passedSpecs"], "failed": report["totalSpecs"] - report["passedSpecs"],
"testsTotal": report["totalTests"], "testsPassed": report["passedTests"],
"unresolvedSkips": report["unresolvedSkips"], "conditionalCases": len(conditional),
"report": artifact(report_path)}
write_json(product_path, product)
status_path = ROOT / "docs/status/WBV2-P1424-001.md"
lines = [line for line in status_path.read_text().splitlines() if not line.startswith("- full Chromium batch:")]
lines.append(f"- full Chromium batch: {report['status']}; {report['passedSpecs']}/{report['totalSpecs']} spec files, "
f"{report['passedTests']}/{report['totalTests']} tests; {len(conditional)} conditional cases explicitly supplemented; "
"`tests/golden/WBV2-P1424-001/full-smoke-closure-report.json`")
status_path.write_text("\n".join(lines) + "\n")
manifest_path = BASE / "manifest.json"
manifest = load(manifest_path)
for path in (report_path, latest_path, product_path, status_path, Path(__file__).resolve(),
ROOT / "tools/web/run-wbv2-full-smoke.py", ROOT / "tools/web/run-wbv2-smoke-closure.py",
BASE / "full-smoke-task-report.md"):
ref = artifact(path)
manifest["artifacts"][ref["path"]] = {"sha256": ref["sha256"], "bytes": path.stat().st_size}
write_json(manifest_path, manifest)
print(json.dumps({k: report[k] for k in ("status", "totalSpecs", "passedSpecs", "totalTests", "passedTests", "blockers")}), flush=True)
return 0 if not blockers else 1
if __name__ == "__main__":
raise SystemExit(main())