Advance M7 workflows and release operations
This commit is contained in:
184
tools/web/check-deployment-http.mjs
Normal file
184
tools/web/check-deployment-http.mjs
Normal file
@@ -0,0 +1,184 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import http from "node:http";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
createDeploymentHttpServer,
|
||||
listenDeploymentHttpServer,
|
||||
loadDeploymentContract,
|
||||
} from "./deployment-http-server.mjs";
|
||||
|
||||
const contract = loadDeploymentContract();
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "blender-deployment-http-"));
|
||||
|
||||
function write(relative, bytes) {
|
||||
const filePath = path.join(root, relative);
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, bytes);
|
||||
return Buffer.from(bytes);
|
||||
}
|
||||
|
||||
function sha256(bytes) {
|
||||
return crypto.createHash("sha256").update(bytes).digest("hex");
|
||||
}
|
||||
|
||||
function deterministicBytes(size, seed) {
|
||||
const bytes = Buffer.alloc(size);
|
||||
for (let index = 0; index < bytes.length; index += 1) bytes[index] = (index * 31 + seed) & 0xff;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
const fixtures = new Map([
|
||||
["/index.html", write("index.html", "<!doctype html><title>Web Blender</title>")],
|
||||
["/assets/app.js", write("assets/app.js", "export const app = true;\n")],
|
||||
["/assets/app.css", write("assets/app.css", "body { color: black; }\n")],
|
||||
["/vendor/blender/single/web_engine.js", write("vendor/blender/single/web_engine.js", "export default () => {};\n")],
|
||||
["/vendor/blender/single/web_engine.wasm", write("vendor/blender/single/web_engine.wasm", deterministicBytes(1024, 7))],
|
||||
["/vendor/blender/pthread/web_engine.js", write("vendor/blender/pthread/web_engine.js", "export default () => {}; // pthread\n")],
|
||||
["/engine-manifest.json", write("engine-manifest.json", "{\"schemaVersion\":2}\n")],
|
||||
["/media/frame.png", write("media/frame.png", Buffer.from([0x89, 0x50, 0x4e, 0x47]))],
|
||||
["/media/silence.wav", write("media/silence.wav", Buffer.from("RIFF0000WAVE"))],
|
||||
["/fixtures/basic.blend", write("fixtures/basic.blend", deterministicBytes(4096, 11))],
|
||||
["/volumes/smoke.nvdb", write("volumes/smoke.nvdb", deterministicBytes(1024 * 1024, 19))],
|
||||
]);
|
||||
|
||||
const server = createDeploymentHttpServer({ root, contract });
|
||||
let origin;
|
||||
|
||||
async function body(response) {
|
||||
return Buffer.from(await response.arrayBuffer());
|
||||
}
|
||||
|
||||
async function expectMime(url, expected, method = "GET") {
|
||||
const response = await fetch(`${origin}${url}`, { method });
|
||||
const actual = response.headers.get("content-type");
|
||||
assert.equal(actual, expected, `MIME mismatch url=${url} expected=${expected} actual=${actual}`);
|
||||
assert.equal(response.status, 200, `HTTP status mismatch url=${url} expected=200 actual=${response.status}`);
|
||||
return response;
|
||||
}
|
||||
|
||||
function expectedMime(url) {
|
||||
const extension = path.extname(new URL(url, "http://deployment.local").pathname);
|
||||
const mime = contract.mimeTypes[extension];
|
||||
assert.ok(mime, `deployment contract has no MIME for url=${url}`);
|
||||
return mime;
|
||||
}
|
||||
|
||||
async function expectRange(url, header, expectedStart, expectedEnd) {
|
||||
const source = fixtures.get(url);
|
||||
const response = await fetch(`${origin}${url}`, { headers: { Range: header } });
|
||||
assert.equal(response.status, contract.rangeRequests.satisfiedStatus, `range status url=${url} range=${header}`);
|
||||
const bytes = await body(response);
|
||||
assert.deepEqual(bytes, source.subarray(expectedStart, expectedEnd + 1), `range bytes url=${url} range=${header}`);
|
||||
for (const name of contract.rangeRequests.requiredSatisfiedHeaders) {
|
||||
assert.ok(response.headers.has(name), `missing ${name} url=${url} range=${header}`);
|
||||
}
|
||||
assert.equal(response.headers.get("content-range"), `bytes ${expectedStart}-${expectedEnd}/${source.length}`);
|
||||
assert.equal(Number(response.headers.get("content-length")), expectedEnd - expectedStart + 1);
|
||||
assert.match(response.headers.get("etag"), /^"sha256-[a-f0-9]{64}"$/);
|
||||
return response;
|
||||
}
|
||||
|
||||
async function interruptedPrefix(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
const request = http.get(`${origin}${url}`, (response) => {
|
||||
response.once("data", (chunk) => {
|
||||
chunks.push(chunk);
|
||||
response.destroy();
|
||||
resolve({ bytes: Buffer.concat(chunks), etag: response.headers.etag });
|
||||
});
|
||||
response.on("error", (error) => {
|
||||
if (error.code !== "ECONNRESET") reject(error);
|
||||
});
|
||||
});
|
||||
request.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
origin = await listenDeploymentHttpServer(server);
|
||||
|
||||
const mimeCases = [
|
||||
["/index.html", ".html"],
|
||||
["/assets/app.js", ".js"],
|
||||
["/assets/app.css", ".css"],
|
||||
["/vendor/blender/single/web_engine.wasm", ".wasm"],
|
||||
["/engine-manifest.json", ".json"],
|
||||
["/media/frame.png", ".png"],
|
||||
["/media/silence.wav", ".wav"],
|
||||
["/fixtures/basic.blend", ".blend"],
|
||||
["/volumes/smoke.nvdb", ".nvdb"],
|
||||
];
|
||||
for (const [url, extension] of mimeCases) {
|
||||
assert.equal(expectedMime(url), contract.mimeTypes[extension]);
|
||||
const response = await expectMime(url, contract.mimeTypes[extension]);
|
||||
assert.deepEqual(await body(response), fixtures.get(url));
|
||||
}
|
||||
|
||||
for (const url of [
|
||||
"/vendor/blender/single/web_engine.js",
|
||||
"/vendor/blender/single/web_engine.wasm",
|
||||
"/vendor/blender/pthread/web_engine.js",
|
||||
"/fixtures/basic.blend",
|
||||
]) {
|
||||
const response = await expectMime(url, expectedMime(url), "HEAD");
|
||||
assert.equal((await response.arrayBuffer()).byteLength, 0, `HEAD returned a body url=${url}`);
|
||||
assert.equal(Number(response.headers.get("content-length")), fixtures.get(url).length);
|
||||
}
|
||||
|
||||
await expectRange("/vendor/blender/single/web_engine.wasm", "bytes=100-199", 100, 199);
|
||||
await expectRange("/fixtures/basic.blend", "bytes=200-399", 200, 399);
|
||||
await expectRange("/volumes/smoke.nvdb", "bytes=300-599", 300, 599);
|
||||
await expectRange("/fixtures/basic.blend", "bytes=400-", 400, fixtures.get("/fixtures/basic.blend").length - 1);
|
||||
await expectRange("/fixtures/basic.blend", "bytes=-128", fixtures.get("/fixtures/basic.blend").length - 128, fixtures.get("/fixtures/basic.blend").length - 1);
|
||||
|
||||
for (const range of ["items=0-1", "bytes=", "bytes=20-10", "bytes=0-999999", "bytes=0-1,4-5", "bytes=-0"]) {
|
||||
const response = await fetch(`${origin}/fixtures/basic.blend`, { headers: { Range: range } });
|
||||
assert.equal(response.status, contract.rangeRequests.unsatisfiedStatus, `invalid range accepted range=${range}`);
|
||||
assert.equal((await response.arrayBuffer()).byteLength, 0);
|
||||
assert.equal(response.headers.get("content-range"), `bytes */${fixtures.get("/fixtures/basic.blend").length}`);
|
||||
}
|
||||
|
||||
const etagFirst = (await fetch(`${origin}/fixtures/basic.blend`)).headers.get("etag");
|
||||
const etagSecond = (await fetch(`${origin}/fixtures/basic.blend`)).headers.get("etag");
|
||||
assert.equal(etagFirst, etagSecond);
|
||||
const matchingIfRange = await fetch(`${origin}/fixtures/basic.blend`, {
|
||||
headers: { Range: "bytes=0-31", "If-Range": etagFirst },
|
||||
});
|
||||
assert.equal(matchingIfRange.status, 206);
|
||||
const mismatchingIfRange = await fetch(`${origin}/fixtures/basic.blend`, {
|
||||
headers: { Range: "bytes=0-31", "If-Range": '"sha256-old-revision"' },
|
||||
});
|
||||
assert.equal(mismatchingIfRange.status, 200);
|
||||
assert.deepEqual(await body(mismatchingIfRange), fixtures.get("/fixtures/basic.blend"));
|
||||
|
||||
const interrupted = await interruptedPrefix("/volumes/smoke.nvdb");
|
||||
assert.ok(interrupted.bytes.length > 0 && interrupted.bytes.length < fixtures.get("/volumes/smoke.nvdb").length);
|
||||
const resumed = await fetch(`${origin}/volumes/smoke.nvdb`, {
|
||||
headers: {
|
||||
Range: `bytes=${interrupted.bytes.length}-`,
|
||||
"If-Range": interrupted.etag,
|
||||
},
|
||||
});
|
||||
assert.equal(resumed.status, 206);
|
||||
const merged = Buffer.concat([interrupted.bytes, await body(resumed)]);
|
||||
assert.equal(sha256(merged), sha256(fixtures.get("/volumes/smoke.nvdb")));
|
||||
|
||||
const replacement = deterministicBytes(fixtures.get("/fixtures/basic.blend").length, 23);
|
||||
fs.writeFileSync(path.join(root, "fixtures/basic.blend"), replacement);
|
||||
const changed = await fetch(`${origin}/fixtures/basic.blend`, {
|
||||
headers: { Range: "bytes=32-", "If-Range": etagFirst },
|
||||
});
|
||||
assert.equal(changed.status, 200);
|
||||
assert.notEqual(changed.headers.get("etag"), etagFirst);
|
||||
assert.deepEqual(await body(changed), replacement);
|
||||
|
||||
process.stdout.write("deployment-http-ok mime=9 head=4 ranges=5 invalidRanges=6 resumeSha256=verified\n");
|
||||
}
|
||||
finally {
|
||||
if (server.listening) await new Promise((resolve) => server.close(resolve));
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
Reference in New Issue
Block a user