Advance M8-M11 parity workflows
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-17 04:37:07 -04:00
parent 7c16b279ae
commit 0fe8d2bb56
324 changed files with 31920 additions and 863 deletions

View File

@@ -0,0 +1,167 @@
import assert from "node:assert/strict";
import crypto from "node:crypto";
import fs from "node:fs";
import fsp from "node:fs/promises";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
const execFileAsync = promisify(execFile);
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const fixture = path.join(root, "tests/files/web/m11_render_reference.blend");
const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
const renderScript = path.join(root, "tools/web/render-server-job.py");
const golden = JSON.parse(await fsp.readFile(path.join(root, "tests/golden/M11-06/server-render-job.json"), "utf8"));
const sourceBytes = await fsp.readFile(fixture);
const sourceArrayBuffer = sourceBytes.buffer.slice(sourceBytes.byteOffset, sourceBytes.byteOffset + sourceBytes.byteLength);
const buildSha256 = crypto.createHash("sha256").update(await fsp.readFile(blender)).digest("hex");
const versionOutput = (await execFileAsync(blender, ["--version"], { cwd: root })).stdout;
const versionLine = versionOutput.split(/\r?\n/).find((line) => line.startsWith("Blender ")) ?? "";
const buildVersion = versionLine.match(/^Blender\s+(5\.2\.[0-9]+)\b/)?.[1];
assert.equal(buildVersion, "5.2.0", `unexpected Blender build: ${versionOutput}`);
assert.equal(golden.schemaVersion, 1);
assert.equal(golden.task, "M11-06");
assert.equal(golden.blenderVersion, buildVersion);
assert.ok(fs.existsSync(fixture), "M11-04 source fixture is missing");
const requireFromWeb = createRequire(path.join(root, "web/package.json"));
const ts = requireFromWeb("typescript");
const protocolSource = fs.readFileSync(path.join(root, "web/protocol/server-render-job.ts"), "utf8")
.replace('import type { ErrorCode } from "./error";\n', "");
const transpiled = ts.transpileModule(protocolSource, {
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
fileName: "server-render-job.ts",
reportDiagnostics: true,
});
assert.deepEqual(transpiled.diagnostics, []);
const protocol = await import(`data:text/javascript;base64,${Buffer.from(transpiled.outputText).toString("base64")}`);
const settings = {
renderEngine: "BLENDER_EEVEE",
frameStart: 1,
frameEnd: 1,
resolutionX: 256,
resolutionY: 256,
resolutionPercentage: 100,
samples: 1,
outputMime: "image/png",
transparent: false,
};
const build = { version: buildVersion, buildSha256 };
const temporary = await fsp.mkdtemp(path.join(os.tmpdir(), "m11-server-render-job-"));
function json(response, status, value) {
const body = Buffer.from(`${JSON.stringify(value)}\n`);
response.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store", "Content-Length": body.length });
response.end(body);
}
const server = http.createServer(async (request, response) => {
if (request.method !== "POST" || request.url !== "/v1/render/jobs") return json(response, 404, { code: "NOT_FOUND" });
const chunks = [];
let sourceByteLength = 0;
try {
for await (const chunk of request) {
chunks.push(chunk);
sourceByteLength += chunk.length;
if (sourceByteLength > 512 * 1024 * 1024) throw new Error("SERVER_RENDER_SOURCE_INVALID: upload exceeds budget");
}
const body = Buffer.concat(chunks);
const requestHeader = request.headers["x-render-request"];
if (typeof requestHeader !== "string") throw new Error("SERVER_RENDER_REQUEST_INVALID: missing request metadata");
const renderRequest = await protocol.verifyServerRenderJobRequest(JSON.parse(requestHeader), body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength));
if (renderRequest.blenderBuild.version !== build.version || renderRequest.blenderBuild.buildSha256 !== build.buildSha256) {
throw Object.assign(new Error("SERVER_RENDER_BUILD_MISMATCH: requested build is not the executable serving this job"), { code: "SERVER_RENDER_BUILD_MISMATCH" });
}
const fileStem = renderRequest.jobId.replace(/[^A-Za-z0-9_.-]/g, "_");
const sourcePath = path.join(temporary, `${fileStem}.blend`);
const outputPath = path.join(temporary, `${fileStem}.png`);
const settingsPath = path.join(temporary, `${fileStem}.json`);
await fsp.writeFile(sourcePath, body, { flag: "wx", mode: 0o600 });
await fsp.writeFile(settingsPath, `${JSON.stringify(renderRequest.settings)}\n`, { flag: "wx", mode: 0o600 });
await execFileAsync(blender, ["--background", "--factory-startup", "--python", renderScript, "--", sourcePath, outputPath, settingsPath], { cwd: root, maxBuffer: 2 * 1024 * 1024 });
const outputBytes = await fsp.readFile(outputPath);
const result = await protocol.createServerRenderJobResult(renderRequest, outputBytes.buffer.slice(outputBytes.byteOffset, outputBytes.byteOffset + outputBytes.byteLength));
json(response, 200, result);
} catch (error) {
json(response, 400, { code: error?.code ?? "SERVER_RENDER_FAILED", message: error instanceof Error ? error.message : String(error) });
}
});
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}`;
try {
const renderRequest = await protocol.createServerRenderJobRequest(sourceArrayBuffer, build, settings, { jobId: "render:m11-06", sourceRevision: 11 });
const response = await fetch(`${origin}/v1/render/jobs`, {
method: "POST",
headers: { "Content-Type": "application/x-blend", "X-Render-Request": JSON.stringify(renderRequest) },
body: sourceBytes,
duplex: "half",
});
const responseText = await response.text();
assert.equal(response.status, 200, responseText);
const result = JSON.parse(responseText);
const outputPath = path.join(temporary, "verified-output.png");
const outputBytes = await fsp.readFile(path.join(temporary, "render_m11-06.png"));
await fsp.writeFile(outputPath, outputBytes);
await protocol.verifyServerRenderJobResult(result, renderRequest, outputBytes.buffer.slice(outputBytes.byteOffset, outputBytes.byteOffset + outputBytes.byteLength));
assert.equal(result.sourceBlendSha256, renderRequest.sourceBlendSha256);
assert.equal(result.settingsSha256, renderRequest.settingsSha256);
assert.equal(result.sourceBlendSha256, golden.sourceBlendSha256);
assert.equal(result.settingsSha256, golden.settingsSha256);
assert.equal(result.outputMime, golden.output.mime);
assert.ok(result.outputByteLength >= golden.output.minimumByteLength);
assert.match(result.outputSha256, new RegExp(golden.output.sha256Pattern));
const tamperedSource = Buffer.from(sourceBytes);
tamperedSource[0] ^= 0xff;
const sourceFailure = await fetch(`${origin}/v1/render/jobs`, {
method: "POST",
headers: { "Content-Type": "application/x-blend", "X-Render-Request": JSON.stringify(renderRequest) },
body: tamperedSource,
duplex: "half",
});
assert.equal(sourceFailure.status, 400);
assert.equal((await sourceFailure.json()).code, golden.tamperCodes.source);
const wrongBuildRequest = await protocol.createServerRenderJobRequest(sourceArrayBuffer, { ...build, buildSha256: "b".repeat(64) }, settings, { jobId: "render:wrong-build", sourceRevision: 11 });
const buildFailure = await fetch(`${origin}/v1/render/jobs`, {
method: "POST",
headers: { "Content-Type": "application/x-blend", "X-Render-Request": JSON.stringify(wrongBuildRequest) },
body: sourceBytes,
duplex: "half",
});
assert.equal(buildFailure.status, 400);
assert.equal((await buildFailure.json()).code, golden.tamperCodes.build);
const settingsFailureRequest = { ...renderRequest, settings: { ...renderRequest.settings, samples: 2 } };
const settingsFailure = await fetch(`${origin}/v1/render/jobs`, {
method: "POST",
headers: { "Content-Type": "application/x-blend", "X-Render-Request": JSON.stringify(settingsFailureRequest) },
body: sourceBytes,
duplex: "half",
});
assert.equal(settingsFailure.status, 400);
assert.equal((await settingsFailure.json()).code, golden.tamperCodes.settings);
const tamperedOutput = Buffer.from(outputBytes);
tamperedOutput[tamperedOutput.length - 1] ^= 0xff;
await assert.rejects(
protocol.verifyServerRenderJobResult(result, renderRequest, tamperedOutput.buffer.slice(tamperedOutput.byteOffset, tamperedOutput.byteOffset + tamperedOutput.byteLength)),
{ code: golden.tamperCodes.output },
);
process.stdout.write(`server-render-job-ok blender=${buildVersion} source=${renderRequest.sourceBlendSha256} settings=${renderRequest.settingsSha256} output=${result.outputSha256} bytes=${result.outputByteLength} source-hash=1 build-hash=1 settings-hash=1 output-hash=1 tamper=4\n`);
} finally {
await new Promise((resolve) => server.close(resolve));
await fsp.rm(temporary, { recursive: true, force: true });
}