69 lines
5.3 KiB
TypeScript
69 lines
5.3 KiB
TypeScript
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { expect, test } from "@playwright/test";
|
|
|
|
const root = path.resolve(import.meta.dirname, "../../..");
|
|
const fixtures = {
|
|
OBJ: fs.readFileSync(path.join(root, "tests/files/web/m12_obj_multi_v1/multi-object.obj")),
|
|
STL: fs.readFileSync(path.join(root, "tests/files/web/m12_stl_capability_v1/capability-binary.stl")),
|
|
PLY: fs.readFileSync(path.join(root, "tests/files/web/m12_ply_mapping_v1/mapping-ascii.ply")),
|
|
};
|
|
const reportPath = path.join(root, "tests/golden/M12-07J/io-format-recovery-report.json");
|
|
const sha256 = (bytes: Uint8Array | Buffer) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
|
|
test("M12-07J recovers OBJ/STL/PLY after cancellation, OOM budget faults and Worker restart", async ({ page }) => {
|
|
await page.goto("/");
|
|
const result = await page.evaluate(async (input) => {
|
|
const recovery = await import("/src/testing/io-format-recovery.ts");
|
|
const run = (format: "OBJ" | "STL" | "PLY", bytes: number[], generation: number, phase: string) => new Promise<any>((resolve, reject) => {
|
|
const worker = new Worker("/src/workers/io-format-recovery-test.worker.ts", { type: "module" });
|
|
const requestId = `${format.toLowerCase()}-${phase}-${generation}`;
|
|
worker.onmessage = (event: MessageEvent<any>) => { worker.terminate(); if (!event.data.ok) reject(new Error(event.data.error)); else resolve(event.data); };
|
|
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
|
|
const payload = Uint8Array.from(bytes).buffer;
|
|
worker.postMessage({ type: "run", requestId, format, operation: "IMPORT", bytes: payload, workerGeneration: generation, baseRevision: 4 }, [payload]);
|
|
});
|
|
const cancel = (format: "OBJ" | "STL" | "PLY", bytes: number[]) => new Promise<any>((resolve, reject) => {
|
|
const worker = new Worker("/src/workers/io-format-recovery-test.worker.ts", { type: "module" });
|
|
const requestId = `${format.toLowerCase()}-cancel-1`;
|
|
worker.onmessage = (event: MessageEvent<any>) => { worker.terminate(); if (!event.data.ok) reject(new Error(event.data.error)); else resolve(event.data); };
|
|
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
|
|
const payload = Uint8Array.from(bytes).buffer;
|
|
worker.postMessage({ type: "run", requestId, format, operation: "IMPORT", bytes: payload, workerGeneration: 1, baseRevision: 4 }, [payload]);
|
|
setTimeout(() => worker.postMessage({ type: "cancel", targetRequestId: requestId }), 3);
|
|
});
|
|
const summary: Record<string, any> = {};
|
|
for (const format of ["OBJ", "STL", "PLY"] as const) {
|
|
const source = input[format];
|
|
const cancelled = await cancel(format, source);
|
|
const oversized = new Array(512 * 1024 + 1).fill(7);
|
|
const oom = await run(format, oversized, 1, "oom");
|
|
const first = await run(format, source, 1, "first");
|
|
const second = await run(format, source, 2, "second");
|
|
const recovered = recovery.recoverIOFormatRecoveryOperation(first.receipt, 2);
|
|
const small = await run(format, source, 3, "small");
|
|
summary[format] = { sourceSha256: await crypto.subtle.digest("SHA-256", Uint8Array.from(source)).then((digest) => Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("")), cancel: cancelled.receipt, oom: oom.receipt, first: first.receipt, second: second.receipt, recovered, small: small.receipt, hashes: { first: first.result.outputSha256, second: second.result.outputSha256, small: small.result.outputSha256 } };
|
|
}
|
|
return summary;
|
|
}, Object.fromEntries(Object.entries(fixtures).map(([format, bytes]) => [format, Array.from(bytes)])));
|
|
|
|
for (const format of ["OBJ", "STL", "PLY"] as const) {
|
|
const value = result[format];
|
|
expect(value.cancel).toMatchObject({ status: "CANCELLED", errorCode: "IO_FORMAT_OPERATION_CANCELLED", temporaryBytes: 0, liveRequests: 0, publishedResults: 0, committed: false });
|
|
expect(value.oom).toMatchObject({ status: "BLOCKED", errorCode: "IO_FORMAT_OOM", temporaryBytes: 0, liveRequests: 0, publishedResults: 0, committed: false });
|
|
expect(value.recovered).toMatchObject({ status: "RECOVERED", workerGeneration: 2, errorCode: "IO_FORMAT_WORKER_RESTARTED", committed: true });
|
|
expect(value.first).toMatchObject({ status: "COMMITTED", workerGeneration: 1, publishedResults: 1, committed: true });
|
|
expect(value.second).toMatchObject({ status: "COMMITTED", workerGeneration: 2, publishedResults: 1, committed: true });
|
|
expect(value.small).toMatchObject({ status: "COMMITTED", workerGeneration: 3, publishedResults: 1, committed: true });
|
|
expect(value.hashes.second).toBe(value.hashes.first);
|
|
expect(value.hashes.small).toBe(value.hashes.first);
|
|
}
|
|
const report = { schemaVersion: 1, task: "M12-07J", operation: "IO_FORMAT_THREE_WAY_RECOVERY", formats: result, assertions: { formats: ["OBJ", "STL", "PLY"], cancellationUnpublished: true, oomUnpublished: true, restartHashStable: true, smallRecoveryStable: true }, nextTask: "M13-01A" };
|
|
if (process.env.UPDATE_IO_FORMAT_RECOVERY_REPORT === "1") {
|
|
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n");
|
|
}
|
|
expect(report).toEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")));
|
|
});
|