Files
2026-09-02 13:50:03 +00:00

285 lines
8.9 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
createShardPlan,
discoverTestCatalog,
validateShardManifest,
type CatalogUnit,
} from "./plan-shards";
import type { TimingProfile } from "./e2e-timings";
const profile: TimingProfile = {
version: 1,
generatedAt: "2026-08-10T00:00:00.000Z",
source: { branch: "main", sha: "main-sha", runId: "12" },
entries: [
{
key: "chromium::tests/a.spec.ts::a",
project: "chromium",
file: "tests/a.spec.ts",
title: "a",
fileHash: "hash-a",
samples: [],
p50Seconds: 8,
p75Seconds: 10,
lastSeenSha: "main-sha",
lastSeenRunId: "12",
},
{
key: "chromium::tests/b.spec.ts::b",
project: "chromium",
file: "tests/b.spec.ts",
title: "b",
fileHash: "hash-old-b",
samples: [],
p50Seconds: 20,
p75Seconds: 20,
lastSeenSha: "main-sha",
lastSeenRunId: "12",
},
],
};
const units: CatalogUnit[] = [
{ project: "chromium", file: "tests/a.spec.ts", fileHash: "hash-a", testCount: 1 },
{ project: "chromium", file: "tests/b.spec.ts", fileHash: "hash-new-b", testCount: 1 },
{ project: "chromium", file: "tests/c.spec.ts", fileHash: "hash-c", testCount: 3 },
{ project: "mobile-chrome", file: "tests/mobile-c.spec.ts", fileHash: "hash-m", testCount: 2 },
];
describe("duration-aware shard planning", () => {
it("discovers specs below the web e2e test root and hashes their source", () => {
const webRoot = fs.mkdtempSync(path.join(os.tmpdir(), "kandev-planner-"));
const testDir = path.join(webRoot, "e2e", "tests", "chat");
fs.mkdirSync(testDir, { recursive: true });
fs.symlinkSync(path.resolve("node_modules"), path.join(webRoot, "node_modules"), "dir");
fs.writeFileSync(
path.join(webRoot, "e2e", "playwright.config.ts"),
[
'import { defineConfig } from "@playwright/test";',
"export default defineConfig({",
' testDir: "./tests",',
" projects: [",
' { name: "chromium", testIgnore: [/mobile-.*\\.spec\\.ts$/, /tests\\/(docker|ssh)\\//] },',
' { name: "mobile-chrome", testMatch: /mobile-.*\\.spec\\.ts$/ },',
' { name: "containers", testMatch: /tests\\/(docker|ssh)\\/.*\\.spec\\.ts$/ },',
" ],",
"});",
].join("\n"),
);
fs.writeFileSync(
path.join(testDir, "example.spec.ts"),
[
'import { test } from "@playwright/test";',
'test.describe("suite", () => {',
" test.beforeEach(async () => {});",
' test("works", async () => {});',
' test.skip("skipped", async () => {});',
' for (const name of ["generated one", "generated two"]) {',
" test(name, async () => {});",
" }",
"});",
].join("\n"),
);
expect(discoverTestCatalog(webRoot, "normal")).toEqual([
expect.objectContaining({
project: "chromium",
file: "tests/chat/example.spec.ts",
testCount: 4,
fileHash: expect.stringMatching(/^[a-f0-9]{64}$/),
}),
]);
});
it("assigns weighted units with stable longest-processing-time packing", () => {
const first = createShardPlan(units, {
cohort: "normal",
shardCount: 2,
profile,
generatedAt: "2026-08-10T00:00:00.000Z",
});
const second = createShardPlan(units, {
cohort: "normal",
shardCount: 2,
profile,
generatedAt: "2026-08-10T00:00:00.000Z",
});
expect(first).toEqual(second);
expect(first.shards).toHaveLength(2);
expect(first.shards.flatMap((shard) => shard.units)).toHaveLength(units.length);
expect(first.summary.warmUnits).toBe(1);
expect(first.summary.unknownUnits).toBe(2);
expect(first.shards[0]?.predictedSeconds).toBeGreaterThan(0);
});
it("validates exact coverage and rejects duplicate assignments", () => {
const plan = createShardPlan(units, {
cohort: "containers",
shardCount: 2,
generatedAt: "2026-08-10T00:00:00.000Z",
});
expect(() => validateShardManifest(plan, units)).not.toThrow();
const duplicate = structuredClone(plan);
duplicate.shards[1]?.units.push(plan.shards[0]!.units[0]!);
expect(() => validateShardManifest(duplicate, units)).toThrow(/duplicate/i);
});
it("rejects units whose immutable catalog fields were changed", () => {
const plan = createShardPlan(units, {
cohort: "normal",
shardCount: 2,
generatedAt: "2026-08-10T00:00:00.000Z",
});
const tampered = structuredClone(plan);
const unit = tampered.shards.find((shard) => shard.units.length > 0)?.units[0];
expect(unit).toBeDefined();
unit!.fileHash = "tampered";
expect(() => validateShardManifest(tampered, units)).toThrow(/catalog fields/i);
});
it("rejects duplicate and out-of-range shard indexes", () => {
const plan = createShardPlan(units, {
cohort: "normal",
shardCount: 2,
generatedAt: "2026-08-10T00:00:00.000Z",
});
const duplicate = structuredClone(plan);
duplicate.shards[1]!.index = duplicate.shards[0]!.index;
expect(() => validateShardManifest(duplicate, units)).toThrow(/shard index/i);
const outOfRange = structuredClone(plan);
outOfRange.shards[0]!.index = plan.shardCount + 1;
expect(() => validateShardManifest(outOfRange, units)).toThrow(/shard index/i);
});
it("uses count-based estimates when no profile exists", () => {
const plan = createShardPlan(units.slice(0, 2), {
cohort: "containers",
shardCount: 1,
generatedAt: "2026-08-10T00:00:00.000Z",
});
expect(plan.profile.mode).toBe("count-fallback");
expect(plan.summary.unknownUnits).toBe(2);
expect(plan.shards[0]?.predictedSeconds).toBe(40);
});
it("budgets unknown Kubernetes specs for real cluster lifecycle time", () => {
const plan = createShardPlan(
[
{
project: "containers",
file: "tests/kubernetes/kubernetes-executor.spec.ts",
fileHash: "hash-kubernetes",
testCount: 2,
},
],
{
cohort: "containers",
shardCount: 1,
generatedAt: "2026-08-10T00:00:00.000Z",
},
);
expect(plan.shards[0]?.predictedSeconds).toBe(360);
});
it("uses the Kubernetes fallback for tests missing from a timing profile", () => {
const kubernetesUnit: CatalogUnit = {
project: "containers",
file: "tests/kubernetes/kubernetes-executor.spec.ts",
fileHash: "hash-kubernetes",
testCount: 2,
};
const kubernetesProfile: TimingProfile = {
...profile,
entries: [
{
...profile.entries[0]!,
key: "containers::tests/kubernetes/kubernetes-executor.spec.ts::launches",
project: "containers",
file: kubernetesUnit.file,
fileHash: kubernetesUnit.fileHash,
p50Seconds: 120,
p75Seconds: 140,
},
],
};
const plan = createShardPlan([kubernetesUnit], {
cohort: "containers",
shardCount: 1,
profile: kubernetesProfile,
generatedAt: "2026-08-10T00:00:00.000Z",
});
expect(plan.shards[0]?.predictedSeconds).toBe(320);
expect(plan.shards[0]?.units[0]?.classification).toBe("unknown");
});
it("adds a fallback estimate for new tests in a profiled file", () => {
const plan = createShardPlan(
[{ project: "chromium", file: "tests/a.spec.ts", fileHash: "hash-a", testCount: 2 }],
{
cohort: "normal",
shardCount: 1,
profile,
generatedAt: "2026-08-10T00:00:00.000Z",
},
);
expect(plan.summary.unknownUnits).toBe(1);
expect(plan.shards[0]?.units[0]?.classification).toBe("unknown");
expect(plan.shards[0]?.predictedSeconds).toBe(20);
});
it("reports a file-level unit that dominates its shard", () => {
const plan = createShardPlan(
[
{ project: "chromium", file: "tests/heavy.spec.ts", fileHash: "heavy", testCount: 100 },
{ project: "chromium", file: "tests/light.spec.ts", fileHash: "light", testCount: 1 },
],
{
cohort: "normal",
shardCount: 2,
generatedAt: "2026-08-10T00:00:00.000Z",
},
);
expect(plan.summary.dominantUnits).toEqual([
expect.objectContaining({
id: "chromium::tests/heavy.spec.ts",
shard: 1,
shareOfShard: 1,
}),
]);
});
it("reports profiled files that are no longer in the catalog", () => {
const plan = createShardPlan(units.slice(0, 1), {
cohort: "normal",
shardCount: 1,
profile: {
...profile,
entries: [
...profile.entries,
{
...profile.entries[0]!,
key: "chromium::tests/removed.spec.ts::removed",
file: "tests/removed.spec.ts",
},
],
},
generatedAt: "2026-08-10T00:00:00.000Z",
});
expect(plan.summary.staleUnits).toBe(2);
});
});