Files
workinf_Blender_Wasm/tools/web/run-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

165 lines
9.2 KiB
Python

#!/usr/bin/env python3
"""Run all Chromium E2E configurations once, retaining bounded batch evidence."""
import argparse
import fcntl
import importlib.util
import json
import os
from pathlib import Path
import shutil
import signal
import socket
import subprocess
import time
ROOT = Path(__file__).resolve().parents[2]
BASE = ROOT / "tests/golden/WBV2-P1424-001"
spec = importlib.util.spec_from_file_location("repairs", ROOT / "tools/web/run-wbv2-smoke-repairs.py")
repairs = importlib.util.module_from_spec(spec)
spec.loader.exec_module(repairs)
now, write_json, artifact = repairs.now, repairs.write_json, repairs.artifact
def rows(suites, source=""):
for suite in suites:
filename = suite.get("file", source)
for case in suite.get("specs", []):
for test in case.get("tests", []):
yield {"spec": Path(case.get("file", filename)).name,
"title": case["title"], "status": test.get("status"),
"attempts": [{"status": r.get("status"), "duration": r.get("duration"),
"errors": r.get("errors", [])} for r in test.get("results", [])]}
yield from rows(suite.get("suites", []), filename)
def bindings(archive):
paths = set()
for directory in ("web/app/src", "web/protocol", "web/tests/e2e", "web/tests/fixtures",
"blender-5.2.0/source/blender/web_engine"):
paths.update(p for p in (ROOT / directory).rglob("*") if p.is_file())
paths.update((ROOT / "web").glob("playwright*.config.ts"))
paths.update(ROOT / p for p in (
"web/app/public/vendor/blender/web_engine.wasm", "web/app/public/vendor/blender/web_engine.js",
"web/app/public/wasm-module-manifest.json", "web/package-lock.json",
"tools/web/run-wbv2-full-smoke.py", "tools/web/run-wbv2-smoke-repairs.py",
"tools/web/run-wbv2-smoke-closure.py", "tools/web/record-wbv2-full-smoke.py",
"blender-5.2.0/source/blender/blenkernel/intern/idtype.cc",
"blender-5.2.0/source/blender/blenkernel/intern/particle.cc",
"blender-5.2.0/source/blender/modifiers/intern/MOD_particlesystem.cc",
"tools/web/server-operator-job.py", "tools/web/server-operator-job-server.mjs"))
paths.update(ROOT / name for name in json.loads(os.environ.get("WBV2_MODIFIER_BATCH_FIXTURES", "[]")))
paths.add(archive)
return [artifact(p) for p in sorted(paths)]
def run_group(name, config, timeout, output, env):
folder = output / name
folder.mkdir()
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
env = {**env, "WEB_TEST_PORT": str(port), "PLAYWRIGHT_JSON_OUTPUT_FILE": str(folder / "playwright.json"),
"EDITING_SOAK_REPORT": str(folder / "soak.json")}
command = [shutil.which("node"), str(ROOT / "web/node_modules/@playwright/test/cli.js"), "test",
"--config", config, "--workers=1", "--retries=0", "--reporter=list,json",
"--output", str(folder / "attachments")]
result = {"group": name, "config": config, "command": command, "startedAt": now(),
"status": "RUNNING", "port": port, "timeoutSeconds": timeout}
write_json(folder / "result.json", result)
print(f"START {name}: {folder}", flush=True)
with (folder / "stdout.log").open("w") as stdout, (folder / "stderr.log").open("w") as stderr:
process = subprocess.Popen(command, cwd=ROOT / "web", env=env, stdout=stdout, stderr=stderr, start_new_session=True)
start = time.monotonic()
try:
while process.poll() is None:
elapsed = time.monotonic() - start
heartbeat = {"updatedAt": now(), "pid": os.getpid(), "childPid": process.pid,
"group": name, "elapsedSeconds": round(elapsed), "status": "RUNNING",
"output": str(output.relative_to(ROOT))}
write_json(output / "heartbeat.json", heartbeat)
print(json.dumps(heartbeat), flush=True)
if elapsed >= timeout:
result["status"] = "TIMEOUT"
break
try:
process.wait(timeout=min(30, timeout - elapsed))
except subprocess.TimeoutExpired:
pass
else:
result.update(exitCode=process.returncode, status="PASS" if process.returncode == 0 else "FAILED")
except BaseException:
result["status"] = "INTERRUPTED"
raise
finally:
repairs.stop_group(process)
result["finishedAt"] = now()
write_json(folder / "result.json", result)
report_path = folder / "playwright.json"
cases = []
if report_path.is_file():
report = json.loads(report_path.read_text())
cases = list(rows(report.get("suites", [])))
result.update(stats=report.get("stats", {}), errors=report.get("errors", []), tests=cases,
report=artifact(report_path))
if result["status"] == "PASS" and (not cases or result.get("errors") or any(c["status"] != "expected" for c in cases)):
result["status"] = "INCOMPLETE"
result["logs"] = [artifact(folder / filename) for filename in ("stdout.log", "stderr.log")]
write_json(folder / "result.json", result)
print(f"END {name}: {result['status']} {result.get('stats', {})}", flush=True)
return result
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--archive", type=Path, default=BASE / "offline-test-bundle/blender-web-offline.tar.gz")
args = parser.parse_args()
lock = (BASE / "full-smoke.lock").open("a+")
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
output = BASE / "full-smoke" / repairs.datetime.now(repairs.timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
output.mkdir(parents=True)
archive = args.archive.resolve()
env = {**os.environ, "CHROME_PATH": os.environ.get("CHROME_PATH") or shutil.which("google-chrome"),
"BLENDER_BIN": str(Path(os.environ.get("BLENDER_BIN", ROOT / "build_blender_native/bin/blender")).resolve()),
"M6_BINARY_ARCHIVE": str(archive), "EDITING_SOAK_DURATION_MS": "1800000", "EDITING_SOAK_ALLOW_SHORT": "0"}
if not env["CHROME_PATH"] or not Path(env["BLENDER_BIN"]).is_file() or not archive.is_file():
raise RuntimeError("Local Chromium, Blender and production archive are required")
initial = bindings(archive)
write_json(output / "inputs.json", initial)
expected_specs = sorted(p.name for p in (ROOT / "web/tests/e2e").rglob("*.spec.ts"))
summary = {"task": "WBV2-P1424-001", "startedAt": now(), "status": "RUNNING", "groups": [],
"expectedSpecs": expected_specs, "historicalSpecCount": 243,
"batchEnvironment": {key: env.get(key) for key in ("WBV2_MODIFIER_BATCH_FIXTURES", "WBV2_MODIFIER_BATCH_TYPES",
"WBV2_MODIFIER_BATCH_UNITS", "WBV2_MODIFIER_BATCH_PRODUCTION")},
"browser": "chromium", "retryPolicy": "No automatic retries; diagnose failures before a new batch"}
write_json(output / "summary.json", summary)
for name, config, timeout in (("main", "playwright.config.ts", 7200),
("archive", "playwright.archive.config.ts", 600),
("single-thread", "playwright.single-thread.config.ts", 300)):
try:
summary["groups"].append(run_group(name, config, timeout, output, env))
except BaseException as error:
summary.update(status="INTERRUPTED", error=str(error), finishedAt=now())
write_json(output / "summary.json", summary)
raise
write_json(output / "summary.json", summary)
cases = [case for group in summary["groups"] for case in group.get("tests", [])]
by_spec = {filename: [case for case in cases if case["spec"] == filename] for filename in expected_specs}
summary.update(finishedAt=now(), totalSpecs=len(expected_specs), totalTests=len(cases),
passedTests=sum(case["status"] == "expected" for case in cases),
passedSpecs=sum(bool(tests) and all(c["status"] == "expected" for c in tests) for tests in by_spec.values()),
missingSpecs=[filename for filename, tests in by_spec.items() if not tests],
failures=[case for case in cases if case["status"] != "expected"],
inputsUnchanged=initial == bindings(archive), inputs=artifact(output / "inputs.json"))
summary["status"] = "PASS" if (all(g["status"] == "PASS" for g in summary["groups"]) and
not summary["missingSpecs"] and summary["inputsUnchanged"]) else "FAILED"
write_json(output / "summary.json", summary)
write_json(output / "heartbeat.json", {"updatedAt": now(), "status": summary["status"], "pid": os.getpid()})
write_json(BASE / "full-smoke-latest.json", {"status": summary["status"], "report": artifact(output / "summary.json")})
print(json.dumps({key: value for key, value in summary.items() if key not in ("groups", "expectedSpecs")}), flush=True)
return 0 if summary["status"] == "PASS" else 1
if __name__ == "__main__":
signal.signal(signal.SIGTERM, lambda *_: (_ for _ in ()).throw(KeyboardInterrupt("SIGTERM")))
raise SystemExit(main())