mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
test(producer): extend cross-worker idempotency to non-zero chunks + dedupe soft-skip regex
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Shared soft-skip regex used by every `services/distributed/*.test.ts` that
|
||||||
|
* drives `renderChunk()` through a real chrome-headless-shell. Matches the
|
||||||
|
* failure signatures we've observed on dev/CI hosts whose GL stack can't
|
||||||
|
* initialize:
|
||||||
|
*
|
||||||
|
* - `chrome://gpu` / `BROWSER_GPU_NOT_SOFTWARE` / SwiftShader text:
|
||||||
|
* `assertSwiftShader` can't read the gpu info table.
|
||||||
|
* - `HeadlessExperimental.beginFrame` / `Target closed`:
|
||||||
|
* chrome-headless-shell's GL process exited because the build doesn't
|
||||||
|
* honor `--use-gl=swiftshader` on the host distro (`gl_factory.cc:111`
|
||||||
|
* errors out before BeginFrame can run).
|
||||||
|
*
|
||||||
|
* Production-shaped Docker images (`Dockerfile.test` / `Dockerfile.chunk-runner`)
|
||||||
|
* carry a chrome-headless-shell build matched to the planDir's `ffmpegVersion`,
|
||||||
|
* so the determinism contract is exercised there. Tests that fail to render
|
||||||
|
* on the host soft-skip and rely on the Docker harness for ground truth.
|
||||||
|
*
|
||||||
|
* This module lives under `__test_utils__/` and is excluded from `tsc`'s
|
||||||
|
* declaration output via `tsconfig.json` so it never ships in the published
|
||||||
|
* package.
|
||||||
|
*/
|
||||||
|
export const HOST_CHROME_FAILURE_PATTERNS =
|
||||||
|
/chrome:\/\/gpu|BROWSER_GPU_NOT_SOFTWARE|SwiftShader|HeadlessExperimental\.beginFrame|Target closed/i;
|
||||||
@@ -25,13 +25,11 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync }
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { dirname, join, resolve } from "node:path";
|
import { dirname, join, resolve } from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { HOST_CHROME_FAILURE_PATTERNS } from "./__test_utils__/hostChromeFailures.js";
|
||||||
import { assemble } from "./assemble.js";
|
import { assemble } from "./assemble.js";
|
||||||
import { plan } from "./plan.js";
|
import { plan } from "./plan.js";
|
||||||
import { renderChunk } from "./renderChunk.js";
|
import { renderChunk } from "./renderChunk.js";
|
||||||
|
|
||||||
const HOST_CHROME_FAILURE_PATTERNS =
|
|
||||||
/chrome:\/\/gpu|BROWSER_GPU_NOT_SOFTWARE|SwiftShader|HeadlessExperimental\.beginFrame|Target closed/i;
|
|
||||||
|
|
||||||
// Per-adapter fixture directories under `packages/producer/tests/distributed/`.
|
// Per-adapter fixture directories under `packages/producer/tests/distributed/`.
|
||||||
// Each must hold `src/index.html`; this test owns the planning + render +
|
// Each must hold `src/index.html`; this test owns the planning + render +
|
||||||
// assemble pipeline so no `output/` baseline is required.
|
// assemble pipeline so no `output/` baseline is required.
|
||||||
|
|||||||
@@ -26,12 +26,17 @@ import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|||||||
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { HOST_CHROME_FAILURE_PATTERNS } from "./__test_utils__/hostChromeFailures.js";
|
||||||
import { plan } from "./plan.js";
|
import { plan } from "./plan.js";
|
||||||
import { renderChunk } from "./renderChunk.js";
|
import { renderChunk } from "./renderChunk.js";
|
||||||
|
|
||||||
// Tiny composition shared by both subtests. 5 frames at 30fps lands a chunk
|
// Tiny composition shared by every subtest. 5 frames at 30fps + chunkSize=2
|
||||||
// within a few seconds inside Docker and keeps host runs (where this test
|
// lands three chunks of sizes [2, 2, 1] within a few seconds inside Docker
|
||||||
// soft-skips most of the time) cheap when they do exercise the full path.
|
// and keeps host runs (where this test soft-skips most of the time) cheap
|
||||||
|
// when they do exercise the full path. We assert byte-identity on chunk 0
|
||||||
|
// (the always-special first chunk) and chunk 1 (the smallest non-zero
|
||||||
|
// chunk index, exercising the seek-offset + frame-indexing code path that
|
||||||
|
// a regression specific to chunks N>0 would land in).
|
||||||
const FIXTURE_HTML = `<!doctype html>
|
const FIXTURE_HTML = `<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head><meta charset="utf-8"><title>cross-worker idempotency fixture</title></head>
|
<head><meta charset="utf-8"><title>cross-worker idempotency fixture</title></head>
|
||||||
@@ -42,11 +47,16 @@ const FIXTURE_HTML = `<!doctype html>
|
|||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
|
|
||||||
// Patterns that indicate a host's chrome-headless-shell can't render — same
|
// Force multi-chunk plans on a tiny fixture. With 5 frames the resolver
|
||||||
// set `renderChunk.test.ts` uses. We soft-skip rather than fail; the docker
|
// produces chunks of sizes [2, 2, 1] — enough to cover chunkIndex 0 + a
|
||||||
// harness covers the determinism contract against a known-good image.
|
// non-zero chunk without inflating render wall time.
|
||||||
const HOST_CHROME_FAILURE_PATTERNS =
|
const PLAN_CHUNK_SIZE = 2;
|
||||||
/chrome:\/\/gpu|BROWSER_GPU_NOT_SOFTWARE|SwiftShader|HeadlessExperimental\.beginFrame|Target closed/i;
|
|
||||||
|
// Subtests render this set of chunk indices, twice each, and assert every
|
||||||
|
// pair is byte-identical. Chunk 0 is the always-present special case;
|
||||||
|
// chunk 1 catches regressions in the seek-offset / frame-indexing logic
|
||||||
|
// that would only fire for chunks N>0.
|
||||||
|
const CHUNK_INDICES_UNDER_TEST = [0, 1] as const;
|
||||||
|
|
||||||
let runRoot: string;
|
let runRoot: string;
|
||||||
let projectDir: string;
|
let projectDir: string;
|
||||||
@@ -75,7 +85,7 @@ beforeAll(async () => {
|
|||||||
try {
|
try {
|
||||||
await plan(
|
await plan(
|
||||||
projectDir,
|
projectDir,
|
||||||
{ fps: 30, width: 160, height: 120, format: "png-sequence" },
|
{ fps: 30, width: 160, height: 120, format: "png-sequence", chunkSize: PLAN_CHUNK_SIZE },
|
||||||
pngPlanDir,
|
pngPlanDir,
|
||||||
);
|
);
|
||||||
pngPlanReady = true;
|
pngPlanReady = true;
|
||||||
@@ -90,7 +100,11 @@ beforeAll(async () => {
|
|||||||
mp4PlanDir = join(runRoot, "plan-mp4");
|
mp4PlanDir = join(runRoot, "plan-mp4");
|
||||||
mkdirSync(mp4PlanDir, { recursive: true });
|
mkdirSync(mp4PlanDir, { recursive: true });
|
||||||
try {
|
try {
|
||||||
await plan(projectDir, { fps: 30, width: 160, height: 120, format: "mp4" }, mp4PlanDir);
|
await plan(
|
||||||
|
projectDir,
|
||||||
|
{ fps: 30, width: 160, height: 120, format: "mp4", chunkSize: PLAN_CHUNK_SIZE },
|
||||||
|
mp4PlanDir,
|
||||||
|
);
|
||||||
mp4PlanReady = true;
|
mp4PlanReady = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -140,88 +154,94 @@ function assertBytesEqual(
|
|||||||
|
|
||||||
describe("cross-worker idempotency", () => {
|
describe("cross-worker idempotency", () => {
|
||||||
// Generous timeout for slower CI: cold Chrome start + 5-frame capture +
|
// Generous timeout for slower CI: cold Chrome start + 5-frame capture +
|
||||||
// ffmpeg encode is the dominant cost, repeated twice.
|
// ffmpeg encode is the dominant cost, repeated twice per chunk index. With
|
||||||
|
// PLAN_CHUNK_SIZE=2 each chunk has at most 2 frames, so cold-start
|
||||||
|
// dominates even when iterating multiple indices.
|
||||||
const TIMEOUT_MS = 120_000;
|
const TIMEOUT_MS = 120_000;
|
||||||
|
|
||||||
it(
|
for (const chunkIndex of CHUNK_INDICES_UNDER_TEST) {
|
||||||
"png-sequence: chunk 0 is byte-identical across two distinct output dirs",
|
it(
|
||||||
async () => {
|
`png-sequence: chunk ${chunkIndex} is byte-identical across two distinct output dirs`,
|
||||||
if (!pngPlanReady) {
|
async () => {
|
||||||
console.warn(
|
if (!pngPlanReady) {
|
||||||
"[crossWorkerIdempotency.test] skipping png-sequence — plan() didn't complete on host",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const outA = join(runRoot, "pngseq-chunk-a");
|
|
||||||
const outB = join(runRoot, "pngseq-chunk-b");
|
|
||||||
let a, b;
|
|
||||||
try {
|
|
||||||
a = await renderChunk(pngPlanDir, 0, outA);
|
|
||||||
} catch (err) {
|
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
|
||||||
if (HOST_CHROME_FAILURE_PATTERNS.test(message)) {
|
|
||||||
console.warn(
|
console.warn(
|
||||||
"[crossWorkerIdempotency.test] skipping png-sequence — host Chrome can't render. ",
|
`[crossWorkerIdempotency.test] skipping png-sequence chunk ${chunkIndex} — plan() didn't complete on host`,
|
||||||
"Diagnostic:",
|
|
||||||
message.slice(0, 240),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw err;
|
const outA = join(runRoot, `pngseq-chunk-${chunkIndex}-a`);
|
||||||
}
|
const outB = join(runRoot, `pngseq-chunk-${chunkIndex}-b`);
|
||||||
b = await renderChunk(pngPlanDir, 0, outB);
|
let a, b;
|
||||||
|
try {
|
||||||
|
a = await renderChunk(pngPlanDir, chunkIndex, outA);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
if (HOST_CHROME_FAILURE_PATTERNS.test(message)) {
|
||||||
|
console.warn(
|
||||||
|
`[crossWorkerIdempotency.test] skipping png-sequence chunk ${chunkIndex} — host Chrome can't render. `,
|
||||||
|
"Diagnostic:",
|
||||||
|
message.slice(0, 240),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
b = await renderChunk(pngPlanDir, chunkIndex, outB);
|
||||||
|
|
||||||
expect(a.outputKind).toBe("frame-dir");
|
expect(a.outputKind).toBe("frame-dir");
|
||||||
expect(b.outputKind).toBe("frame-dir");
|
expect(b.outputKind).toBe("frame-dir");
|
||||||
expect(a.framesEncoded).toBeGreaterThan(0);
|
expect(a.framesEncoded).toBeGreaterThan(0);
|
||||||
expect(b.framesEncoded).toBe(a.framesEncoded);
|
expect(b.framesEncoded).toBe(a.framesEncoded);
|
||||||
|
|
||||||
// sha256 fingerprint match — the contract `ChunkResult.sha256` implies.
|
// sha256 fingerprint match — the contract `ChunkResult.sha256` implies.
|
||||||
expect(a.sha256).toBe(b.sha256);
|
expect(a.sha256).toBe(b.sha256);
|
||||||
// Independent byte-level verification. If the sha256 helper ever
|
// Independent byte-level verification. If the sha256 helper ever
|
||||||
// regresses (e.g. starts hashing metadata instead of pixels), this
|
// regresses (e.g. starts hashing metadata instead of pixels), this
|
||||||
// assertion still fails the test honestly.
|
// assertion still fails the test honestly.
|
||||||
assertBytesEqual(outA, outB, "frame-dir", "png-sequence chunk 0");
|
assertBytesEqual(outA, outB, "frame-dir", `png-sequence chunk ${chunkIndex}`);
|
||||||
},
|
},
|
||||||
TIMEOUT_MS,
|
TIMEOUT_MS,
|
||||||
);
|
);
|
||||||
|
|
||||||
it(
|
it(
|
||||||
"mp4: chunk 0 is byte-identical across two distinct output paths",
|
`mp4: chunk ${chunkIndex} is byte-identical across two distinct output paths`,
|
||||||
async () => {
|
async () => {
|
||||||
if (!mp4PlanReady) {
|
if (!mp4PlanReady) {
|
||||||
console.warn("[crossWorkerIdempotency.test] skipping mp4 — plan() didn't complete on host");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const outDir = join(runRoot, "mp4-chunks");
|
|
||||||
mkdirSync(outDir, { recursive: true });
|
|
||||||
const outA = join(outDir, "chunk-a.mp4");
|
|
||||||
const outB = join(outDir, "chunk-b.mp4");
|
|
||||||
let a, b;
|
|
||||||
try {
|
|
||||||
a = await renderChunk(mp4PlanDir, 0, outA);
|
|
||||||
} catch (err) {
|
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
|
||||||
if (HOST_CHROME_FAILURE_PATTERNS.test(message)) {
|
|
||||||
console.warn(
|
console.warn(
|
||||||
"[crossWorkerIdempotency.test] skipping mp4 — host Chrome can't render. ",
|
`[crossWorkerIdempotency.test] skipping mp4 chunk ${chunkIndex} — plan() didn't complete on host`,
|
||||||
"Diagnostic:",
|
|
||||||
message.slice(0, 240),
|
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw err;
|
const outDir = join(runRoot, "mp4-chunks");
|
||||||
}
|
mkdirSync(outDir, { recursive: true });
|
||||||
b = await renderChunk(mp4PlanDir, 0, outB);
|
const outA = join(outDir, `chunk-${chunkIndex}-a.mp4`);
|
||||||
|
const outB = join(outDir, `chunk-${chunkIndex}-b.mp4`);
|
||||||
|
let a, b;
|
||||||
|
try {
|
||||||
|
a = await renderChunk(mp4PlanDir, chunkIndex, outA);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
if (HOST_CHROME_FAILURE_PATTERNS.test(message)) {
|
||||||
|
console.warn(
|
||||||
|
`[crossWorkerIdempotency.test] skipping mp4 chunk ${chunkIndex} — host Chrome can't render. `,
|
||||||
|
"Diagnostic:",
|
||||||
|
message.slice(0, 240),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
b = await renderChunk(mp4PlanDir, chunkIndex, outB);
|
||||||
|
|
||||||
expect(a.outputKind).toBe("file");
|
expect(a.outputKind).toBe("file");
|
||||||
expect(b.outputKind).toBe("file");
|
expect(b.outputKind).toBe("file");
|
||||||
expect(a.framesEncoded).toBeGreaterThan(0);
|
expect(a.framesEncoded).toBeGreaterThan(0);
|
||||||
expect(b.framesEncoded).toBe(a.framesEncoded);
|
expect(b.framesEncoded).toBe(a.framesEncoded);
|
||||||
|
|
||||||
expect(a.sha256).toBe(b.sha256);
|
expect(a.sha256).toBe(b.sha256);
|
||||||
assertBytesEqual(outA, outB, "file", "mp4 chunk 0");
|
assertBytesEqual(outA, outB, "file", `mp4 chunk ${chunkIndex}`);
|
||||||
},
|
},
|
||||||
TIMEOUT_MS,
|
TIMEOUT_MS,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|||||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
import { HOST_CHROME_FAILURE_PATTERNS } from "./__test_utils__/hostChromeFailures.js";
|
||||||
import { plan } from "./plan.js";
|
import { plan } from "./plan.js";
|
||||||
import {
|
import {
|
||||||
CHUNK_INDEX_OUT_OF_RANGE,
|
CHUNK_INDEX_OUT_OF_RANGE,
|
||||||
@@ -168,23 +169,7 @@ describe("renderChunk()", () => {
|
|||||||
a = await renderChunk(planDir, 0, outA);
|
a = await renderChunk(planDir, 0, outA);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
// Soft-skip patterns we've observed on dev/CI hosts where Chrome's
|
if (HOST_CHROME_FAILURE_PATTERNS.test(message)) {
|
||||||
// GL stack can't initialize:
|
|
||||||
// - `BROWSER_GPU_NOT_SOFTWARE` / `chrome://gpu` / SwiftShader text:
|
|
||||||
// the SwiftShader assertion can't read the gpu info table.
|
|
||||||
// - `Target closed` during a `HeadlessExperimental.beginFrame`:
|
|
||||||
// chrome-headless-shell's GL process exited because the build
|
|
||||||
// doesn't honor `--use-gl=swiftshader` on this distro
|
|
||||||
// (`gl_factory.cc:111` errors out before BeginFrame can run).
|
|
||||||
// Production-shaped Docker images (`Dockerfile.test` /
|
|
||||||
// `Dockerfile.chunk-runner`) carry a chrome-headless-shell build
|
|
||||||
// matched to the planDir's `ffmpegVersion`, so the determinism
|
|
||||||
// contract is exercised there.
|
|
||||||
if (
|
|
||||||
/chrome:\/\/gpu|BROWSER_GPU_NOT_SOFTWARE|SwiftShader|HeadlessExperimental\.beginFrame|Target closed/i.test(
|
|
||||||
message,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
console.warn(
|
console.warn(
|
||||||
"[renderChunk.test] skipping byte-identical retry test — host Chrome stack can't render. ",
|
"[renderChunk.test] skipping byte-identical retry test — host Chrome stack can't render. ",
|
||||||
"Docker harness covers the determinism contract. Diagnostic:",
|
"Docker harness covers the determinism contract. Diagnostic:",
|
||||||
|
|||||||
@@ -15,5 +15,5 @@
|
|||||||
"types": ["@webgpu/types"]
|
"types": ["@webgpu/types"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
|
"exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/__test_utils__/**"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user