99 lines
4.7 KiB
JavaScript
99 lines
4.7 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import test from "node:test";
|
|
import ts from "typescript";
|
|
|
|
const root = path.resolve(import.meta.dirname, "../../..");
|
|
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "physics-solver-probe-unit-"));
|
|
|
|
function transpile(sourceName, outputName, replacements = []) {
|
|
const sourcePath = path.join(root, "web/protocol", sourceName);
|
|
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
|
|
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
|
fileName: sourcePath,
|
|
reportDiagnostics: true,
|
|
});
|
|
assert.deepEqual(transpiled.diagnostics, []);
|
|
const output = replacements.reduce((source, [from, to]) => source.replaceAll(from, to), transpiled.outputText);
|
|
fs.writeFileSync(path.join(temporary, outputName), output);
|
|
}
|
|
|
|
transpile("capability-gates.ts", "capability-gates.mjs");
|
|
transpile("physics-simulation.ts", "physics-simulation.mjs", [
|
|
['from "./capability-gates"', 'from "./capability-gates.mjs"'],
|
|
]);
|
|
const physics = await import(pathToFileURL(path.join(temporary, "physics-simulation.mjs")));
|
|
|
|
const mib = 1024 * 1024;
|
|
|
|
test("M10-13 defaults every Physics family to desktop/server bake", async () => {
|
|
const capabilities = await physics.probePhysicsSolverCapabilities(undefined, {
|
|
threadMode: "PTHREAD",
|
|
memoryLimitBytes: 2_048 * mib,
|
|
});
|
|
assert.deepEqual(capabilities.map((entry) => entry.family), physics.PHYSICS_FAMILIES);
|
|
assert.ok(capabilities.every((entry) => entry.localSolver === "BLOCKED"));
|
|
assert.ok(capabilities.every((entry) => entry.solverProbe === "EXPORT_UNAVAILABLE"));
|
|
assert.ok(capabilities.every((entry) => entry.unsupportedRoute === "DESKTOP_SERVER_BAKE"));
|
|
});
|
|
|
|
test("M10-13 probes export, initialization, threads and memory per family", async () => {
|
|
const initialization = {
|
|
RIGID_BODY: { initialized: true, requiredThreadMode: "SINGLE", requiredMemoryBytes: 64 * mib },
|
|
SOFT_BODY: { initialized: false, requiredThreadMode: "SINGLE", requiredMemoryBytes: 64 * mib },
|
|
CLOTH: { initialized: true, requiredThreadMode: "PTHREAD", requiredMemoryBytes: 128 * mib },
|
|
FLUID: { initialized: true, requiredThreadMode: "SINGLE", requiredMemoryBytes: 512 * mib },
|
|
DYNAMIC_PAINT: { initialized: true, requiredThreadMode: "SINGLE", requiredMemoryBytes: Number.NaN },
|
|
};
|
|
const initialized = [];
|
|
const runtime = {
|
|
hasFamilyExport: (family) => family !== "HAIR",
|
|
initializeFamily: async (family) => {
|
|
initialized.push(family);
|
|
if (family === "PARTICLE") throw new Error("init failed");
|
|
return initialization[family];
|
|
},
|
|
};
|
|
const capabilities = await physics.probePhysicsSolverCapabilities(runtime, {
|
|
threadMode: "SINGLE",
|
|
memoryLimitBytes: 256 * mib,
|
|
});
|
|
assert.deepEqual(Object.fromEntries(capabilities.map((entry) => [entry.family, entry.solverProbe])), {
|
|
RIGID_BODY: "READY",
|
|
SOFT_BODY: "INITIALIZATION_FAILED",
|
|
CLOTH: "THREADS_UNAVAILABLE",
|
|
FLUID: "MEMORY_UNAVAILABLE",
|
|
DYNAMIC_PAINT: "INVALID_RESULT",
|
|
PARTICLE: "INITIALIZATION_FAILED",
|
|
HAIR: "EXPORT_UNAVAILABLE",
|
|
});
|
|
assert.deepEqual(initialized, ["RIGID_BODY", "SOFT_BODY", "CLOTH", "FLUID", "DYNAMIC_PAINT", "PARTICLE"]);
|
|
assert.deepEqual(physics.selectPhysicsExecutionRoute("RIGID_BODY", capabilities), {
|
|
family: "RIGID_BODY", mode: "LOCAL_SOLVER", probe: "READY",
|
|
});
|
|
assert.deepEqual(physics.selectPhysicsExecutionRoute("CLOTH", capabilities), {
|
|
family: "CLOTH", mode: "DESKTOP_SERVER_BAKE", probe: "THREADS_UNAVAILABLE",
|
|
});
|
|
assert.equal(physics.gatePhysicsExecution("RIGID_BODY", "LOCAL_SOLVER", capabilities).status, "READY");
|
|
const blocked = physics.gatePhysicsExecution("CLOTH", "LOCAL_SOLVER", capabilities);
|
|
assert.equal(blocked.status, "BLOCKED");
|
|
assert.equal(blocked.issues[0].code, "PHYSICS_SOLVER_UNAVAILABLE");
|
|
assert.match(blocked.issues[0].message, /desktop\/server bake/);
|
|
});
|
|
|
|
test("M10-13 fails closed on an invalid environment or forged probe result", async () => {
|
|
await assert.rejects(
|
|
physics.probePhysicsSolverCapabilities(undefined, { threadMode: "SINGLE", memoryLimitBytes: 0 }),
|
|
{ code: "PHYSICS_MANIFEST_INVALID" },
|
|
);
|
|
const capabilities = await physics.probePhysicsSolverCapabilities({
|
|
hasFamilyExport: () => true,
|
|
initializeFamily: () => ({ initialized: true, requiredThreadMode: "SINGLE", requiredMemoryBytes: -1 }),
|
|
}, { threadMode: "PTHREAD", memoryLimitBytes: 2_048 * mib });
|
|
assert.ok(capabilities.every((entry) => entry.solverProbe === "INVALID_RESULT"));
|
|
assert.ok(capabilities.every((entry) => entry.localSolver === "BLOCKED"));
|
|
});
|