147 lines
8.0 KiB
JavaScript
147 lines
8.0 KiB
JavaScript
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const reportPath = path.join(root, "tests/golden/WBV2/internal-submodule-smoke-report.json");
|
|
const fixReportPath = path.join(root, "tests/golden/WBV2/internal-submodule-smoke-fix-report.json");
|
|
const wasmPath = path.join(root, "web/app/src/vendor/blender/web_engine.wasm");
|
|
const loaderPath = path.join(root, "web/app/src/vendor/blender/web_engine.js");
|
|
const workerPath = path.join(root, "web/app/src/workers/web-engine.worker.ts");
|
|
const fixturePath = path.join(root, "tests/files/web/basic_scene.blend");
|
|
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
|
|
if (fs.existsSync(fixReportPath)) {
|
|
process.stderr.write("wbv2-submodule-focused-fixes-once-only: focused fix report already exists\n");
|
|
process.exit(2);
|
|
}
|
|
const smokeReport = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
|
const failures = smokeReport.cases.filter((item) => item.status !== "PASS").map((item) => item.submoduleId).sort();
|
|
const expected = [
|
|
"addModifier", "moveNLAStrip", "setCameraView", "setClothSettings", "setFluidSettings",
|
|
"setParticleCache", "setRenderPasses", "setRigidBodySettings", "setSimulationPlayback",
|
|
].sort();
|
|
if (JSON.stringify(failures) !== JSON.stringify(expected)) throw new Error("focused fixes do not match the initial smoke failures");
|
|
|
|
let engine;
|
|
let handle = 0;
|
|
const fixes = [];
|
|
const record = (submoduleId, status, verification, details = {}) => {
|
|
fixes.push({ submoduleId, verificationAttempt: 1, status, verification, ...details });
|
|
};
|
|
|
|
const rawCall = (cmd, params = {}) => {
|
|
const request = new TextEncoder().encode(JSON.stringify({ cmd, params }));
|
|
const input = engine._malloc(request.byteLength);
|
|
const dataOut = engine._malloc(4);
|
|
const lengthOut = engine._malloc(4);
|
|
engine.HEAPU8.set(request, input);
|
|
let resultPointer = 0;
|
|
try {
|
|
engine._web_engine_call_json(handle, input, request.byteLength, dataOut, lengthOut);
|
|
resultPointer = engine.HEAPU32[dataOut >>> 2];
|
|
const length = engine.HEAPU32[lengthOut >>> 2];
|
|
return length ? JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(resultPointer, resultPointer + length))) : null;
|
|
}
|
|
finally {
|
|
if (resultPointer) engine._web_engine_free_buffer(resultPointer);
|
|
engine._free(input);
|
|
engine._free(dataOut);
|
|
engine._free(lengthOut);
|
|
}
|
|
};
|
|
const apply = (submoduleId, command, verification) => {
|
|
const response = rawCall("applyCommand", { command });
|
|
const status = response?.ok === true ? "PASS" : "FAIL";
|
|
record(submoduleId, status, verification, {
|
|
route: "WASM_EDIT_ROUTE",
|
|
result: response?.result ?? null,
|
|
error: response?.error ?? null,
|
|
});
|
|
};
|
|
|
|
try {
|
|
const factory = (await import(pathToFileURL(loaderPath))).default;
|
|
engine = await factory({ wasmBinary: fs.readFileSync(wasmPath) });
|
|
handle = engine._web_engine_create();
|
|
const fixture = fs.readFileSync(fixturePath);
|
|
const pointer = engine._malloc(fixture.byteLength);
|
|
engine.HEAPU8.set(fixture, pointer);
|
|
const openCode = engine._web_engine_open_blend(handle, pointer, fixture.byteLength);
|
|
engine._free(pointer);
|
|
if (openCode !== 0) throw new Error("focused fixes could not open basic_scene.blend");
|
|
|
|
const before = rawCall("snapshot")?.result;
|
|
const mesh = before?.meshes?.[0];
|
|
const owner = before?.nodes?.find((node) => node.dataId === mesh?.id);
|
|
const scene = before?.scenes?.[0];
|
|
const camera = before?.nodes?.find((node) => node.type === "CAMERA");
|
|
const layerName = scene?.viewLayers?.[0]?.name ?? "ViewLayer";
|
|
if (!mesh || !owner || !scene) throw new Error("basic_scene.blend lacks the focused smoke targets");
|
|
|
|
apply("addModifier", { type: "addModifier", meshId: mesh.id, modifierType: "SUBSURF", name: "WBV2 Focused Smoke" }, "valid modifier command returns without a process abort after enabling C++ exception catching");
|
|
|
|
const worker = fs.readFileSync(workerPath, "utf8");
|
|
const nlaAdapter = worker.includes("function prepareNlaMoveCommand(") &&
|
|
worker.includes('payload.type === "moveNLAStrip" ? prepareNlaMoveCommand(payload) : payload');
|
|
record("moveNLAStrip", nlaAdapter ? "PASS" : "FAIL", "Worker adapts moveNLAStrip to the native setNLAStack route", {
|
|
route: "WORKER_ADAPTER",
|
|
files: ["web/app/src/workers/web-engine.worker.ts", "web/tests/e2e/nla-operator.spec.ts"],
|
|
});
|
|
|
|
apply("setCameraView", { type: "setCameraView", dataId: scene.id, cameraObjectId: camera?.id ?? null }, "camera binding is accepted by the native dispatcher");
|
|
apply("setRenderPasses", { type: "setRenderPasses", dataId: scene.id, layerName, passes: { passMask: 1, cryptomatteMask: 0, cryptomatteLevels: 6, alphaThreshold: 0.5 } }, "view-layer render passes are accepted by the native dispatcher");
|
|
apply("setRigidBodySettings", { type: "setRigidBodySettings", objectId: owner.id, family: "RIGID_BODY", settings: { mass: 2.25, friction: 0.4 } }, "rigid-body settings merge into the Physics manifest");
|
|
apply("setClothSettings", { type: "setClothSettings", objectId: owner.id, settings: { mass: 0.25, quality: 5, usePin: false } }, "cloth settings merge into the Physics manifest");
|
|
apply("setFluidSettings", { type: "setFluidSettings", objectId: owner.id, settings: { resolution: 64, timeScale: 1, cacheFrameStart: 1, cacheFrameEnd: 20 } }, "fluid settings merge into the Physics manifest");
|
|
apply("setParticleCache", { type: "setParticleCache", objectId: owner.id, settings: { frameStart: 1, frameEnd: 20, useDiskCache: false, pointCacheId: "focused-smoke" } }, "particle cache settings merge into the Physics manifest");
|
|
apply("setSimulationPlayback", { type: "setSimulationPlayback", settings: { frame: 2, playing: false, direction: 1 } }, "simulation playback updates the Main frame and persists bounded playback metadata");
|
|
|
|
const after = rawCall("snapshot")?.result;
|
|
const systems = after?.physicsSimulation?.systems ?? [];
|
|
const expectedSettings = new Map([
|
|
["RIGID_BODY", ["mass", 2.25]],
|
|
["CLOTH", ["quality", 5]],
|
|
["FLUID", ["resolution", 64]],
|
|
["PARTICLE", ["pointCacheId", "focused-smoke"]],
|
|
]);
|
|
for (const [family, [key, value]] of expectedSettings) {
|
|
const fix = fixes.find((item) => item.submoduleId === ({ RIGID_BODY: "setRigidBodySettings", CLOTH: "setClothSettings", FLUID: "setFluidSettings", PARTICLE: "setParticleCache" })[family]);
|
|
if (!systems.some((system) => system.family === family && system.ownerObjectId === owner.id && system.settings?.[key] === value)) {
|
|
fix.status = "FAIL";
|
|
fix.error = `final snapshot does not contain ${family}.${key}`;
|
|
}
|
|
}
|
|
const playbackFix = fixes.find((item) => item.submoduleId === "setSimulationPlayback");
|
|
if (after?.frame?.current !== 2) {
|
|
playbackFix.status = "FAIL";
|
|
playbackFix.error = `final snapshot frame is ${String(after?.frame?.current)}`;
|
|
}
|
|
}
|
|
catch (error) {
|
|
record("focusedVerifier", "FAIL", "focused verifier infrastructure", { error: error instanceof Error ? error.message : String(error) });
|
|
}
|
|
finally {
|
|
if (engine && handle) engine._web_engine_destroy(handle);
|
|
}
|
|
|
|
const failed = fixes.filter((item) => item.status !== "PASS");
|
|
const report = {
|
|
schemaVersion: 1,
|
|
planId: smokeReport.planId,
|
|
operation: "ONE_PASS_INTERNAL_SUBMODULE_SMOKE_FOCUSED_FIX_VERIFICATION",
|
|
policy: "verify only the initial smoke failures; never rerun the complete submodule smoke set",
|
|
status: failed.length === 0 && fixes.length === expected.length ? "PASS" : "FAIL",
|
|
artifact: {
|
|
wasmSha256: sha256(wasmPath),
|
|
loaderSha256: sha256(loaderPath),
|
|
fixtureSha256: sha256(fixturePath),
|
|
},
|
|
summary: { fixes: fixes.length, passed: fixes.length - failed.length, failed: failed.length, verificationAttempts: fixes.length, retries: 0 },
|
|
fixes,
|
|
};
|
|
fs.writeFileSync(fixReportPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
process.stdout.write(`wbv2-submodule-focused-fixes-${report.status.toLowerCase()} fixes=${fixes.length} passed=${report.summary.passed} failed=${failed.length} retries=0\n`);
|
|
process.exit(report.status === "PASS" ? 0 : 1);
|