98 lines
6.5 KiB
JavaScript
98 lines
6.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import ts from "typescript";
|
|
|
|
const repoRoot = path.resolve(import.meta.dirname, "../../..");
|
|
const protocolPath = path.join(repoRoot, "web/protocol/manifest.ts");
|
|
const protocolSource = fs.readFileSync(protocolPath, "utf8");
|
|
const transpiled = ts.transpileModule(protocolSource, {
|
|
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
|
fileName: protocolPath,
|
|
reportDiagnostics: true,
|
|
});
|
|
assert.deepEqual(transpiled.diagnostics, []);
|
|
const protocol = await import(`data:text/javascript;base64,${Buffer.from(transpiled.outputText).toString("base64")}`);
|
|
const { validateWebEngineManifestV2 } = protocol;
|
|
const goldenPath = path.join(repoRoot, "tests/golden/M6-04A/engine-manifest-v2.json");
|
|
const golden = JSON.parse(fs.readFileSync(goldenPath, "utf8"));
|
|
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, "web/package.json"), "utf8"));
|
|
|
|
function deepFreeze(value) {
|
|
if (value && typeof value === "object") {
|
|
Object.freeze(value);
|
|
for (const item of Object.values(value)) deepFreeze(item);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function invalidCase(name, mutate, code, errorPath) {
|
|
return { name, mutate, code, errorPath };
|
|
}
|
|
|
|
test("M6-04A accepts an immutable single/pthread manifest without mutating it", () => {
|
|
const input = deepFreeze(structuredClone(golden));
|
|
const parsed = validateWebEngineManifestV2(input);
|
|
|
|
assert.deepEqual(parsed, golden);
|
|
assert.notEqual(parsed, input);
|
|
assert.notEqual(parsed.variants[0], input.variants[0]);
|
|
assert.notEqual(parsed.variants[0].resources.js, input.variants[0].resources.js);
|
|
assert.equal(parsed.variants[1].resources.pthreadWorker.url, parsed.variants[1].resources.js.url);
|
|
assert.equal(protocol.WEB_ENGINE_WASM_PAGE_BYTES, 65_536);
|
|
assert.deepEqual(protocol.WEB_ENGINE_MEMORY_LIMITS, {
|
|
minimumInitialPages: 256,
|
|
maximumPages: 32_768,
|
|
});
|
|
|
|
const reversed = validateWebEngineManifestV2({ ...golden, variants: [...golden.variants].reverse() });
|
|
assert.deepEqual(reversed.variants.map((variant) => variant.id), ["single", "pthread"]);
|
|
});
|
|
|
|
test("M6-04A rejects malformed variants, resources and memory declarations", async (t) => {
|
|
const cases = [
|
|
invalidCase("old schema", (value) => { value.schemaVersion = 1; }, "PROTOCOL_MISMATCH", "schemaVersion"),
|
|
invalidCase("unsupported protocol", (value) => { value.protocolVersion = 2; }, "PROTOCOL_MISMATCH", "protocolVersion"),
|
|
invalidCase("missing release ID", (value) => { delete value.releaseId; }, "ENGINE_MANIFEST_INVALID", "releaseId"),
|
|
invalidCase("invalid release ID", (value) => { value.releaseId = "release/id"; }, "ENGINE_MANIFEST_INVALID", "releaseId"),
|
|
invalidCase("missing pthread variant", (value) => { value.variants.pop(); }, "ENGINE_MANIFEST_INVALID", "variants"),
|
|
invalidCase("duplicate single variant", (value) => { value.variants[1].id = "single"; delete value.variants[1].resources.pthreadWorker; value.variants[1].memory.shared = false; }, "ENGINE_MANIFEST_INVALID", "variants"),
|
|
invalidCase("single shared memory", (value) => { value.variants[0].memory.shared = true; }, "ENGINE_MANIFEST_INVALID", "variants[0].memory.shared"),
|
|
invalidCase("single pthread worker", (value) => { value.variants[0].resources.pthreadWorker = structuredClone(value.variants[1].resources.pthreadWorker); }, "ENGINE_MANIFEST_INVALID", "variants[0].resources.pthreadWorker"),
|
|
invalidCase("pthread unshared memory", (value) => { value.variants[1].memory.shared = false; }, "ENGINE_MANIFEST_INVALID", "variants[1].memory.shared"),
|
|
invalidCase("missing pthread worker", (value) => { delete value.variants[1].resources.pthreadWorker; }, "ENGINE_MANIFEST_INVALID", "variants[1].resources.pthreadWorker"),
|
|
invalidCase("invalid SHA-256", (value) => { value.variants[1].resources.wasm.sha256 = "ABC"; }, "ENGINE_MANIFEST_INVALID", "variants[1].resources.wasm.sha256"),
|
|
invalidCase("remote resource URL", (value) => { value.variants[0].resources.js.url = "https://cdn.invalid/web_engine.single.js"; }, "ENGINE_MANIFEST_INVALID", "variants[0].resources.js.url"),
|
|
invalidCase("file name mismatch", (value) => { value.variants[0].resources.wasm.url = "/vendor/blender/wrong.wasm"; }, "ENGINE_MANIFEST_INVALID", "variants[0].resources.wasm.url"),
|
|
invalidCase("initial memory below floor", (value) => { value.variants[0].memory.initialPages = 255; }, "ENGINE_MANIFEST_INVALID", "variants[0].memory.initialPages"),
|
|
invalidCase("maximum memory below initial", (value) => { value.variants[1].memory.maximumPages = 255; }, "ENGINE_MANIFEST_INVALID", "variants[1].memory.maximumPages"),
|
|
invalidCase("maximum memory above ceiling", (value) => { value.variants[1].memory.maximumPages = 32_769; }, "ENGINE_MANIFEST_INVALID", "variants[1].memory.maximumPages"),
|
|
invalidCase("aliased variant WASM", (value) => { value.variants[1].resources.wasm = structuredClone(value.variants[0].resources.wasm); }, "ENGINE_MANIFEST_INVALID", "variants.wasm"),
|
|
invalidCase("worker aliases single JS", (value) => { value.variants[1].resources.pthreadWorker = structuredClone(value.variants[0].resources.js); }, "ENGINE_MANIFEST_INVALID", "variants.pthreadWorker"),
|
|
invalidCase("same worker URL with another hash", (value) => { value.variants[1].resources.pthreadWorker.sha256 = "5".repeat(64); }, "ENGINE_MANIFEST_INVALID", "variants.pthreadWorker.sha256"),
|
|
invalidCase("unknown manifest field", (value) => { value.defaultVariant = "pthread"; }, "ENGINE_MANIFEST_INVALID", "manifest.defaultVariant"),
|
|
];
|
|
|
|
for (const item of cases) {
|
|
await t.test(item.name, () => {
|
|
const input = structuredClone(golden);
|
|
item.mutate(input);
|
|
assert.throws(
|
|
() => validateWebEngineManifestV2(input),
|
|
(error) => error?.name === "WebEngineManifestValidationError" &&
|
|
error.code === item.code && error.path === item.errorPath,
|
|
);
|
|
});
|
|
}
|
|
});
|
|
|
|
test("M6-04B installs the production manifest with explicit variant paths", () => {
|
|
const production = JSON.parse(fs.readFileSync(path.join(repoRoot, "web/app/public/engine-manifest.json"), "utf8"));
|
|
const parsed = validateWebEngineManifestV2(production);
|
|
assert.equal(parsed.releaseId, `blender-wasm-${packageJson.version}`);
|
|
assert.equal(parsed.variants[0].resources.wasm.url, "/vendor/blender/single/web_engine.wasm");
|
|
assert.equal(parsed.variants[1].resources.wasm.url, "/vendor/blender/pthread/web_engine.wasm");
|
|
assert.notEqual(parsed.variants[0].resources.wasm.sha256, parsed.variants[1].resources.wasm.sha256);
|
|
});
|