mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
Merge pull request #857 from heygen-com/05-15-test_producer_extend_cross-worker_idempotency_to_non-zero_chunks_dedupe_soft-skip_regex
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 { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { HOST_CHROME_FAILURE_PATTERNS } from "./__test_utils__/hostChromeFailures.js";
|
||||
import { assemble } from "./assemble.js";
|
||||
import { plan } from "./plan.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/`.
|
||||
// Each must hold `src/index.html`; this test owns the planning + render +
|
||||
// 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 { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { HOST_CHROME_FAILURE_PATTERNS } from "./__test_utils__/hostChromeFailures.js";
|
||||
import { plan } from "./plan.js";
|
||||
import { renderChunk } from "./renderChunk.js";
|
||||
|
||||
// Tiny composition shared by both subtests. 5 frames at 30fps lands a chunk
|
||||
// within a few seconds inside Docker and keeps host runs (where this test
|
||||
// soft-skips most of the time) cheap when they do exercise the full path.
|
||||
// Tiny composition shared by every subtest. 5 frames at 30fps + chunkSize=2
|
||||
// lands three chunks of sizes [2, 2, 1] within a few seconds inside Docker
|
||||
// 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>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title>cross-worker idempotency fixture</title></head>
|
||||
@@ -42,11 +47,16 @@ const FIXTURE_HTML = `<!doctype html>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// Patterns that indicate a host's chrome-headless-shell can't render — same
|
||||
// set `renderChunk.test.ts` uses. We soft-skip rather than fail; the docker
|
||||
// harness covers the determinism contract against a known-good image.
|
||||
const HOST_CHROME_FAILURE_PATTERNS =
|
||||
/chrome:\/\/gpu|BROWSER_GPU_NOT_SOFTWARE|SwiftShader|HeadlessExperimental\.beginFrame|Target closed/i;
|
||||
// Force multi-chunk plans on a tiny fixture. With 5 frames the resolver
|
||||
// produces chunks of sizes [2, 2, 1] — enough to cover chunkIndex 0 + a
|
||||
// non-zero chunk without inflating render wall time.
|
||||
const PLAN_CHUNK_SIZE = 2;
|
||||
|
||||
// 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 projectDir: string;
|
||||
@@ -75,7 +85,7 @@ beforeAll(async () => {
|
||||
try {
|
||||
await plan(
|
||||
projectDir,
|
||||
{ fps: 30, width: 160, height: 120, format: "png-sequence" },
|
||||
{ fps: 30, width: 160, height: 120, format: "png-sequence", chunkSize: PLAN_CHUNK_SIZE },
|
||||
pngPlanDir,
|
||||
);
|
||||
pngPlanReady = true;
|
||||
@@ -90,7 +100,11 @@ beforeAll(async () => {
|
||||
mp4PlanDir = join(runRoot, "plan-mp4");
|
||||
mkdirSync(mp4PlanDir, { recursive: true });
|
||||
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;
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
@@ -140,88 +154,94 @@ function assertBytesEqual(
|
||||
|
||||
describe("cross-worker idempotency", () => {
|
||||
// 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;
|
||||
|
||||
it(
|
||||
"png-sequence: chunk 0 is byte-identical across two distinct output dirs",
|
||||
async () => {
|
||||
if (!pngPlanReady) {
|
||||
console.warn(
|
||||
"[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)) {
|
||||
for (const chunkIndex of CHUNK_INDICES_UNDER_TEST) {
|
||||
it(
|
||||
`png-sequence: chunk ${chunkIndex} is byte-identical across two distinct output dirs`,
|
||||
async () => {
|
||||
if (!pngPlanReady) {
|
||||
console.warn(
|
||||
"[crossWorkerIdempotency.test] skipping png-sequence — host Chrome can't render. ",
|
||||
"Diagnostic:",
|
||||
message.slice(0, 240),
|
||||
`[crossWorkerIdempotency.test] skipping png-sequence chunk ${chunkIndex} — plan() didn't complete on host`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
b = await renderChunk(pngPlanDir, 0, outB);
|
||||
const outA = join(runRoot, `pngseq-chunk-${chunkIndex}-a`);
|
||||
const outB = join(runRoot, `pngseq-chunk-${chunkIndex}-b`);
|
||||
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(b.outputKind).toBe("frame-dir");
|
||||
expect(a.framesEncoded).toBeGreaterThan(0);
|
||||
expect(b.framesEncoded).toBe(a.framesEncoded);
|
||||
expect(a.outputKind).toBe("frame-dir");
|
||||
expect(b.outputKind).toBe("frame-dir");
|
||||
expect(a.framesEncoded).toBeGreaterThan(0);
|
||||
expect(b.framesEncoded).toBe(a.framesEncoded);
|
||||
|
||||
// sha256 fingerprint match — the contract `ChunkResult.sha256` implies.
|
||||
expect(a.sha256).toBe(b.sha256);
|
||||
// Independent byte-level verification. If the sha256 helper ever
|
||||
// regresses (e.g. starts hashing metadata instead of pixels), this
|
||||
// assertion still fails the test honestly.
|
||||
assertBytesEqual(outA, outB, "frame-dir", "png-sequence chunk 0");
|
||||
},
|
||||
TIMEOUT_MS,
|
||||
);
|
||||
// sha256 fingerprint match — the contract `ChunkResult.sha256` implies.
|
||||
expect(a.sha256).toBe(b.sha256);
|
||||
// Independent byte-level verification. If the sha256 helper ever
|
||||
// regresses (e.g. starts hashing metadata instead of pixels), this
|
||||
// assertion still fails the test honestly.
|
||||
assertBytesEqual(outA, outB, "frame-dir", `png-sequence chunk ${chunkIndex}`);
|
||||
},
|
||||
TIMEOUT_MS,
|
||||
);
|
||||
|
||||
it(
|
||||
"mp4: chunk 0 is byte-identical across two distinct output paths",
|
||||
async () => {
|
||||
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)) {
|
||||
it(
|
||||
`mp4: chunk ${chunkIndex} is byte-identical across two distinct output paths`,
|
||||
async () => {
|
||||
if (!mp4PlanReady) {
|
||||
console.warn(
|
||||
"[crossWorkerIdempotency.test] skipping mp4 — host Chrome can't render. ",
|
||||
"Diagnostic:",
|
||||
message.slice(0, 240),
|
||||
`[crossWorkerIdempotency.test] skipping mp4 chunk ${chunkIndex} — plan() didn't complete on host`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
b = await renderChunk(mp4PlanDir, 0, outB);
|
||||
const outDir = join(runRoot, "mp4-chunks");
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
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(b.outputKind).toBe("file");
|
||||
expect(a.framesEncoded).toBeGreaterThan(0);
|
||||
expect(b.framesEncoded).toBe(a.framesEncoded);
|
||||
expect(a.outputKind).toBe("file");
|
||||
expect(b.outputKind).toBe("file");
|
||||
expect(a.framesEncoded).toBeGreaterThan(0);
|
||||
expect(b.framesEncoded).toBe(a.framesEncoded);
|
||||
|
||||
expect(a.sha256).toBe(b.sha256);
|
||||
assertBytesEqual(outA, outB, "file", "mp4 chunk 0");
|
||||
},
|
||||
TIMEOUT_MS,
|
||||
);
|
||||
expect(a.sha256).toBe(b.sha256);
|
||||
assertBytesEqual(outA, outB, "file", `mp4 chunk ${chunkIndex}`);
|
||||
},
|
||||
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 { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { HOST_CHROME_FAILURE_PATTERNS } from "./__test_utils__/hostChromeFailures.js";
|
||||
import { plan } from "./plan.js";
|
||||
import {
|
||||
CHUNK_INDEX_OUT_OF_RANGE,
|
||||
@@ -168,23 +169,7 @@ describe("renderChunk()", () => {
|
||||
a = await renderChunk(planDir, 0, outA);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
// Soft-skip patterns we've observed on dev/CI hosts where Chrome's
|
||||
// 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,
|
||||
)
|
||||
) {
|
||||
if (HOST_CHROME_FAILURE_PATTERNS.test(message)) {
|
||||
console.warn(
|
||||
"[renderChunk.test] skipping byte-identical retry test — host Chrome stack can't render. ",
|
||||
"Docker harness covers the determinism contract. Diagnostic:",
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"types": ["@webgpu/types"]
|
||||
},
|
||||
"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