Export: the redundant guard after Blender is gone; tests for a lying exit code, a malformed result with an FBX, output after SIGTERM, and a hung request

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-24 21:17:07 +02:00
commit a65cc60113
3 changed files with 37 additions and 6 deletions

View file

@ -4,8 +4,9 @@
// like Blender, unless the copy is named blender-broken, which names itself and exits 1. An export
// (`... -- <bvh> <fbx> <clip>`) acts on the clip name:
// fail* prints a line to stderr, exits 1 silent* exits 0 with no result line
// garbage* exits 0 with a malformed result line nofile* exits 0 with a result line and no FBX
// hang* a grandchild, then never exits slow* runs 6 s, then behaves
// garbage* an FBX, then a malformed result line nofile* exits 0 with a result line and no FBX
// lies* an FBX and a result line, exits 3 slow* runs 6 s, then behaves
// hang* a grandchild, then never exits; after SIGTERM it prints a line every 100 ms
// orphan* behaves, but leaves a grandchild running when it exits
// other writes "FBX-STUB:" + the BVH's bytes to <fbx> and prints the VILI-FBX result line
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
@ -33,17 +34,24 @@ const [bvh, fbx, clip] = argv.slice(argv.indexOf("--") + 1);
console.error(`stub-blender-stderr ${clip}`);
if (clip.startsWith("fail")) process.exit(1);
if (clip.startsWith("silent")) process.exit(0);
const writeFbx = () => writeFileSync(fbx, Buffer.concat([Buffer.from("FBX-STUB:"), readFileSync(bvh)]));
if (clip.startsWith("garbage")) {
writeFbx();
console.log('VILI-FBX {"bones":"many","frames":[1],"fps":30}');
process.exit(0);
}
if (clip.startsWith("lies")) {
writeFbx();
console.log(result);
process.exit(3);
}
if (clip.startsWith("nofile")) {
console.log(result);
process.exit(0);
}
if (clip.startsWith("hang")) {
spawnGrandchild();
process.on("SIGTERM", () => {});
process.on("SIGTERM", () => setInterval(() => console.error("after-sigterm-line"), 100));
setInterval(() => {}, 1000);
} else {
if (clip.startsWith("slow")) await new Promise((resolve) => setTimeout(resolve, 6000));
@ -52,6 +60,6 @@ if (clip.startsWith("hang")) {
spawnGrandchild().unref(); // a live child would keep this process, and so the export, alive
for (let waited = 0; grandchildren() === before && waited < 3000; waited += 25) await new Promise((resolve) => setTimeout(resolve, 25));
}
writeFileSync(fbx, Buffer.concat([Buffer.from("FBX-STUB:"), readFileSync(bvh)]));
writeFbx();
console.log(result);
}

View file

@ -1464,6 +1464,9 @@ test("a failing or hung Blender fails the job, leaks nothing, and leaves nothing
assert.equal(odd.status, 502, clip);
assert.equal(odd.body.error, "fbx-export-failed (no result)", clip);
}
const lied = await generate(daemon, exportRequest({ clipName: "lies" }));
assert.equal(lied.status, 502, "an export that exits non-zero is no export, whatever it printed");
assert.equal(lied.body.error, "fbx-export-failed (exit 3)");
const known = new Set(daemon.blenderGrandchildren());
const orphaned = await generate(daemon, exportRequest({ clipName: "orphan" }));
@ -1473,7 +1476,10 @@ test("a failing or hung Blender fails the job, leaks nothing, and leaves nothing
const before = daemon.starts().length;
known.add(left);
const hung = await generate(daemon, exportRequest({ clipName: "hang", exportTimeoutMs: 2500 }));
const hung = await Promise.race([
generate(daemon, exportRequest({ clipName: "hang", exportTimeoutMs: 2500 })),
new Promise((_, reject) => setTimeout(() => reject(new Error("the hung export was never stopped")), 20000)),
]);
assert.equal(hung.status, 502);
assert.equal(hung.body.error, "fbx-export-failed (signal SIGKILL)");
const [run] = daemon.starts().slice(before);
@ -1503,6 +1509,24 @@ test("SIGTERM during an export stops its group, writes nothing more, and exits 0
}
});
test("what a stopping export still prints is not logged, and it is killed when it ignores SIGTERM", async () => {
const daemon = await startDaemon();
try {
const pending = generate(daemon, exportRequest({ clipName: "hang", exportTimeoutMs: 60000 }));
pending.catch(() => {});
const run = await until(() => exportStarts(daemon)[0], "the export to start");
daemon.proc.kill("SIGTERM");
await until(() => daemon.proc.exitCode !== null, "the daemon to exit", 30);
assert.equal(daemon.proc.exitCode, 0);
await until(() => !isAlive(run.pid), "the export that ignored SIGTERM to be killed");
const log = readFileSync(path.join(daemon.stateRoot, "logs", "export.log"), "utf8");
assert.match(log, /stub-blender-stderr hang/);
assert.ok(!log.includes("after-sigterm-line"), "a line printed after the signal reached the log");
} finally {
await daemon.stop();
}
});
test("losing the lease kills a running export within a second and records nothing", async () => {
const daemon = await idunnDaemon({ leased: true });
try {

View file

@ -571,7 +571,6 @@ async function exportClip(bvh, fbx, clipName, timeoutMs) {
"-b", "--factory-startup", "--python-use-system-env", "--python-exit-code", "1",
"--python", exportScript, "--", bvh, fbx, clipName,
], timeoutMs);
guard(); // the writer may have been lost while Blender ran
let info = null;
if (run.code === 0 && run.result !== null) {
try {