Advance M8-M11 parity workflows
This commit is contained in:
98
web/tests/unit/physics-solver-probe.test.mjs
Normal file
98
web/tests/unit/physics-solver-probe.test.mjs
Normal file
@@ -0,0 +1,98 @@
|
||||
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"));
|
||||
});
|
||||
Reference in New Issue
Block a user