128 lines
6.6 KiB
JavaScript
128 lines
6.6 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import fsp from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
import { VDBJobService, createVDBJobHttpServer } from "../vdb/server/vdb-job-service.mjs";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const converter = path.join(root, "build_vdb_tools/vdb_to_nanovdb");
|
|
const resourceRoot = path.resolve(process.env.VDB_RESOURCE_ROOT ?? path.join(os.homedir(), "resource-library/blender-web-vdb"));
|
|
const source = path.join(resourceRoot, "generated/generated-smoke.vdb");
|
|
const secret = "vdb-server-test-signing-key-000000000000000000000000";
|
|
assert.ok(fs.existsSync(converter), "native converter is missing");
|
|
assert.ok(fs.existsSync(source), "real OpenVDB fixture is missing");
|
|
assert.equal(spawnSync("/usr/bin/bwrap", ["--version"], { encoding: "utf8" }).status, 0, "bwrap is unavailable");
|
|
|
|
const temporary = await fsp.mkdtemp(path.join(os.tmpdir(), "vdb-server-test-"));
|
|
const service = new VDBJobService({ converter, root: temporary, secret, timeoutMs: 30_000 });
|
|
const server = createVDBJobHttpServer(service);
|
|
await new Promise((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(0, "127.0.0.1", resolve);
|
|
});
|
|
const address = server.address();
|
|
assert.ok(address && typeof address !== "string");
|
|
const origin = `http://127.0.0.1:${address.port}`;
|
|
const sourceBytes = fs.readFileSync(source);
|
|
const sourceSha256 = crypto.createHash("sha256").update(sourceBytes).digest("hex");
|
|
const headers = {
|
|
"Content-Type": "application/x-openvdb",
|
|
"X-VDB-Project-Id": "vdb-server-test",
|
|
"X-VDB-Source-Path": "//volumes/generated-smoke.vdb",
|
|
"X-VDB-Source-SHA256": sourceSha256,
|
|
"X-VDB-Grids": "density",
|
|
"X-VDB-Quantization": "LOSSLESS",
|
|
"X-VDB-Chunk-Bytes": String(4 * 1024 * 1024),
|
|
};
|
|
|
|
async function submit() {
|
|
const response = await fetch(`${origin}/v1/vdb/jobs`, { method: "POST", headers, body: sourceBytes, duplex: "half" });
|
|
if (response.status !== 200 && response.status !== 202) throw new Error(await response.text());
|
|
return { status: response.status, body: await response.json() };
|
|
}
|
|
|
|
async function waitFor(id, states, timeoutMs = 30_000) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
while (Date.now() < deadline) {
|
|
const response = await fetch(`${origin}/v1/vdb/jobs/${id}`);
|
|
assert.equal(response.status, 200);
|
|
const body = await response.json();
|
|
if (states.includes(body.state)) return body;
|
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
}
|
|
throw new Error(`VDB server job ${id} timed out in test`);
|
|
}
|
|
|
|
try {
|
|
const health = await (await fetch(`${origin}/healthz`)).json();
|
|
assert.equal(health.sandbox, true);
|
|
assert.match(health.converterSha256, /^[a-f0-9]{64}$/);
|
|
|
|
const first = await submit();
|
|
assert.equal(first.status, 202);
|
|
assert.equal(first.body.deduplicated, false);
|
|
const completed = await waitFor(first.body.id, ["SUCCEEDED", "FAILED"]);
|
|
assert.equal(completed.state, "SUCCEEDED", completed.error);
|
|
assert.equal(completed.sandbox, "bwrap-unshare-all+readonly-root+prlimit");
|
|
|
|
const manifestResponse = await fetch(`${origin}${completed.artifacts.manifest}`);
|
|
const bundleResponse = await fetch(`${origin}${completed.artifacts.bundle}`);
|
|
assert.equal(manifestResponse.status, 200);
|
|
assert.equal(bundleResponse.status, 200);
|
|
const manifestBytes = Buffer.from(await manifestResponse.arrayBuffer());
|
|
const bundleBytes = Buffer.from(await bundleResponse.arrayBuffer());
|
|
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
assert.equal(manifest.converter.target, "SERVER");
|
|
assert.equal(manifest.sourceSha256, sourceSha256);
|
|
assert.deepEqual(manifest.grids.map((grid) => grid.name), ["density"]);
|
|
assert.equal(crypto.createHash("sha256").update(bundleBytes).digest("hex"), manifest.bundleSha256);
|
|
const expectedSignature = crypto.createHmac("sha256", secret).update(`${completed.id}:${crypto.createHash("sha256").update(manifestBytes).digest("hex")}:${manifest.bundleSha256}`).digest("hex");
|
|
assert.equal(manifestResponse.headers.get("x-vdb-signature"), expectedSignature);
|
|
assert.equal(bundleResponse.headers.get("x-vdb-signature"), expectedSignature);
|
|
|
|
const desktopBundle = path.join(temporary, "desktop-density.nvdb");
|
|
const desktopReport = path.join(temporary, "desktop-density.json");
|
|
const desktopConversion = spawnSync(converter, [
|
|
"--input", source,
|
|
"--output", desktopBundle,
|
|
"--report", desktopReport,
|
|
"--grid", "density",
|
|
"--quantization", "LOSSLESS",
|
|
], { encoding: "utf8" });
|
|
assert.equal(desktopConversion.status, 0, desktopConversion.stderr);
|
|
assert.equal(crypto.createHash("sha256").update(fs.readFileSync(desktopBundle)).digest("hex"), manifest.bundleSha256,
|
|
"desktop and isolated server conversion produced different NanoVDB bytes");
|
|
|
|
const repeated = await submit();
|
|
assert.equal(repeated.status, 200);
|
|
assert.equal(repeated.body.deduplicated, true);
|
|
assert.equal(repeated.body.id, completed.id);
|
|
|
|
const cancellationHeaders = { ...headers, "X-VDB-Grids": "color,density,temperature,velocity", "X-VDB-Quantization": "FP16" };
|
|
const cancellationResponse = await fetch(`${origin}/v1/vdb/jobs`, { method: "POST", headers: cancellationHeaders, body: sourceBytes, duplex: "half" });
|
|
assert.equal(cancellationResponse.status, 202);
|
|
const cancellation = await cancellationResponse.json();
|
|
const cancelledResponse = await fetch(`${origin}/v1/vdb/jobs/${cancellation.id}`, { method: "DELETE" });
|
|
assert.equal(cancelledResponse.status, 200);
|
|
const cancelled = await waitFor(cancellation.id, ["CANCELLED"]);
|
|
assert.equal(cancelled.state, "CANCELLED");
|
|
assert.equal(fs.existsSync(path.join(temporary, "jobs", cancellation.id, "bundle.nvdb")), false);
|
|
|
|
const cancelFile = path.join(temporary, "native-cancel");
|
|
fs.writeFileSync(cancelFile, "cancel\n");
|
|
const cancelledNative = spawnSync(converter, ["--input", source, "--output", path.join(temporary, "cancelled.nvdb"), "--report", path.join(temporary, "cancelled.json"), "--cancel-file", cancelFile, "--timeout-ms", "30000"], { encoding: "utf8" });
|
|
assert.notEqual(cancelledNative.status, 0);
|
|
assert.match(cancelledNative.stderr, /conversion cancelled/);
|
|
assert.equal(fs.existsSync(path.join(temporary, "cancelled.nvdb")), false);
|
|
|
|
process.stdout.write(`vdb-server-job-ok job=${completed.id} bytes=${bundleBytes.length} idempotent=1 signed=1 isolated=1 cancelled=1 atomic=1 desktop-server-hash=equal\n`);
|
|
}
|
|
finally {
|
|
await new Promise((resolve) => server.close(resolve));
|
|
await fsp.rm(temporary, { recursive: true, force: true });
|
|
}
|