Files
workinf_Blender_Wasm/tools/web/run-wbv2-feature-smoke-plan.mjs
mes123456 b088b792a8
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
test(web): enforce one-pass smoke execution
2026-09-07 06:52:11 +08:00

122 lines
6.2 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
import { execFileSync, spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const planPath = path.join(root, "tests/golden/WBV2/feature-smoke-plan.json");
const reportPath = path.join(root, "tests/golden/WBV2/feature-smoke-report.json");
const plan = JSON.parse(fs.readFileSync(planPath, "utf8"));
const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/WBV2-P1424-001/product-implementation-report.json"), "utf8"));
const args = new Set(process.argv.slice(2));
const execute = args.has("--execute");
function fail(message) {
throw new Error(`feature-smoke-plan-invalid: ${message}`);
}
if (plan.operation !== "ONE_PASS_FEATURE_SMOKE" || plan.policy.oneSmokePerFeature !== true) fail("missing one-pass policy");
if (plan.cases.length !== report.implementationTasks.length) fail(`case count ${plan.cases.length} != task count ${report.implementationTasks.length}`);
const ids = new Set();
for (const smoke of plan.cases) {
if (ids.has(smoke.featureId) || ids.has(smoke.smokeId)) fail(`duplicate feature or smoke id ${smoke.featureId}`);
ids.add(smoke.featureId); ids.add(smoke.smokeId);
if (smoke.runOnce !== true || smoke.maxAttempts !== 1) fail(`non-idempotent case ${smoke.featureId}`);
if (smoke.kind === "playwright") {
const file = path.join(root, "web", smoke.testFile);
if (!fs.existsSync(file)) fail(`${smoke.featureId} test file missing: ${smoke.testFile}`);
if (!fs.readFileSync(file, "utf8").includes(smoke.testTitle)) fail(`${smoke.featureId} test title missing: ${smoke.testTitle}`);
}
if (smoke.kind === "command" && !smoke.command) fail(`${smoke.featureId} command missing`);
}
const expected = new Set(report.implementationTasks.map((task) => task.id));
for (const id of expected) if (!ids.has(id)) fail(`implementation task has no smoke case: ${id}`);
const runCheck = (command, cwd = root) => {
const result = spawnSync("bash", ["-lc", command], { cwd, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 });
return { command, cwd: path.relative(root, cwd) || ".", exitCode: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
};
if (!execute) {
process.stdout.write(`feature-smoke-plan-ok cases=${plan.cases.length} onePass=true playwright=${plan.cases.filter((item) => item.kind === "playwright").length} command=${plan.cases.filter((item) => item.kind === "command").length}\n`);
process.exit(0);
}
if (fs.existsSync(reportPath)) {
const previous = JSON.parse(fs.readFileSync(reportPath, "utf8"));
if (previous.planId === plan.planId && Array.isArray(previous.cases) && previous.cases.some((item) => Number(item.attempts ?? 0) > 0)) {
process.stderr.write(`feature-smoke-once-only: plan ${plan.planId} already has recorded attempts; create a new focused-fix batch instead of rerunning\n`);
process.exit(2);
}
}
const startedAt = new Date().toISOString();
const smokeReport = {
schemaVersion: 1,
planId: plan.planId,
operation: "ONE_PASS_FEATURE_SMOKE_EXECUTION",
browser: "chromium",
startedAt,
policy: plan.policy,
status: "RUNNING",
cases: plan.cases.map((item) => ({ featureId: item.featureId, smokeId: item.smokeId, ordinal: item.ordinal, status: "PENDING", attempts: 0 })),
checks: [],
};
const flush = () => fs.writeFileSync(reportPath, `${JSON.stringify(smokeReport, null, 2)}\n`);
flush();
// These are global preconditions and run once; they are not counted as feature cases.
for (const command of [
"node tools/web/check-blender-feature-route-catalog.mjs",
"node tools/web/check-web-engine-command-registry.mjs",
]) {
const result = runCheck(command);
smokeReport.checks.push(result);
if (result.exitCode !== 0) {
smokeReport.status = "BLOCKED";
smokeReport.finishedAt = new Date().toISOString();
flush();
process.stderr.write(result.stderr || result.stdout);
process.exit(1);
}
}
// Group only cases that share one Playwright file. Each title is still selected
// exactly once, and a failed group is never retried in this execution.
const playwrightGroups = new Map();
for (const item of plan.cases.filter((candidate) => candidate.kind === "playwright")) {
const list = playwrightGroups.get(item.testFile) ?? [];
list.push(item);
playwrightGroups.set(item.testFile, list);
}
for (const [testFile, items] of playwrightGroups) {
const pattern = `(?:${items.map((item) => item.testTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`;
const result = runCheck(`npx playwright test --config playwright.config.ts ${testFile} --grep '${pattern}' --workers=1`, path.join(root, "web"));
smokeReport.checks.push(result);
for (const item of items) {
const entry = smokeReport.cases.find((candidate) => candidate.featureId === item.featureId);
entry.attempts = 1;
entry.status = result.exitCode === 0 ? "PASS" : "FAIL";
entry.evidence = { testFile, testTitle: item.testTitle, exitCode: result.exitCode };
}
flush();
}
for (const item of plan.cases.filter((candidate) => candidate.kind === "command")) {
const result = runCheck(item.command);
smokeReport.checks.push(result);
const entry = smokeReport.cases.find((candidate) => candidate.featureId === item.featureId);
entry.attempts = 1;
entry.status = result.exitCode === 0 ? "PASS" : "FAIL";
entry.evidence = { command: item.command, exitCode: result.exitCode };
flush();
}
const failed = smokeReport.cases.filter((item) => item.status === "FAIL");
const pending = smokeReport.cases.filter((item) => item.status !== "PASS");
smokeReport.status = failed.length ? "FAILED_FOCUSED_FIX_REQUIRED" : pending.length ? "BLOCKED" : "PASS";
smokeReport.finishedAt = new Date().toISOString();
smokeReport.summary = { total: smokeReport.cases.length, passed: smokeReport.cases.filter((item) => item.status === "PASS").length, failed: failed.length, pending: pending.length, retries: 0 };
flush();
process.stdout.write(`feature-smoke-${smokeReport.status.toLowerCase()} total=${smokeReport.summary.total} passed=${smokeReport.summary.passed} failed=${smokeReport.summary.failed} pending=${smokeReport.summary.pending} retries=0\n`);
process.exit(failed.length || pending.length ? 1 : 0);