Skip to content

Commit 537da42

Browse files
committed
perf(test): make suite sharding configurable
1 parent 8924e0a commit 537da42

4 files changed

Lines changed: 122 additions & 27 deletions

File tree

.changeset/fast-checks-rest.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"prepare": "simple-git-hooks",
4141
"test": "bun run ./scripts/test/run-test-suite.ts",
4242
"test:theme-contrast": "bun test packages/hunk/src/ui/themes.test.ts --test-name-pattern contrast",
43-
"test:integration": "\"${npm_execpath:-bun}\" test ./test/pty",
43+
"test:integration": "bun run ./scripts/test/run-test-suite.ts --group=integration",
4444
"test:session-broker-node": "bun run ./scripts/test/test-session-broker-node.ts",
4545
"test:tty-smoke": "HUNK_RUN_TTY_SMOKE=1 \"${npm_execpath:-bun}\" test ./test/smoke",
4646
"test:install-vm": "bun run ./test/cli/install-vm/runner.ts",

scripts/test/run-test-suite.test.ts

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { describe, expect, test } from "bun:test";
22
import {
33
buildTestShardCommand,
44
DEFAULT_TEST_PATTERNS,
5+
requiresSerialTestExecution,
6+
resolveTestGroupShardCount,
7+
resolveTestInvocation,
58
resolveTestShardCount,
9+
TEST_PATTERN_GROUPS,
610
terminateTestShardProcesses,
711
} from "./run-test-suite";
812

@@ -18,12 +22,19 @@ describe("test suite sharding", () => {
1822
expect(resolveTestShardCount(2, "16", "linux")).toBe(16);
1923
});
2024

21-
test("keeps non-Linux suites serial", () => {
25+
test("keeps automatic non-Linux suites serial but accepts explicit CI sharding", () => {
2226
expect(resolveTestShardCount(32, undefined, "win32")).toBe(1);
23-
expect(resolveTestShardCount(32, "16", "darwin")).toBe(1);
27+
expect(resolveTestShardCount(32, "2", "win32")).toBe(2);
28+
expect(resolveTestShardCount(32, "16", "darwin")).toBe(16);
2429
});
2530

26-
test("rejects malformed or excessive Linux shard overrides", () => {
31+
test("keeps PTY integration serial by default while allowing measured overrides", () => {
32+
expect(resolveTestGroupShardCount(32, undefined, "linux", "integration")).toBe(1);
33+
expect(resolveTestGroupShardCount(32, "2", "linux", "integration")).toBe(2);
34+
expect(resolveTestGroupShardCount(32, undefined, "linux", "default")).toBe(2);
35+
});
36+
37+
test("rejects malformed or excessive shard overrides", () => {
2738
expect(() => resolveTestShardCount(8, "0", "linux")).toThrow(
2839
"HUNK_TEST_SHARDS must be a positive safe integer",
2940
);
@@ -38,6 +49,32 @@ describe("test suite sharding", () => {
3849
);
3950
});
4051

52+
test("resolves named test groups without forwarding the selector", () => {
53+
expect(resolveTestInvocation(["--group=integration", "--rerun-each=2"])).toEqual({
54+
forwardedArgs: ["--rerun-each=2"],
55+
group: "integration",
56+
patterns: TEST_PATTERN_GROUPS.integration,
57+
});
58+
expect(resolveTestInvocation([]).group).toBe("default");
59+
expect(() => resolveTestInvocation(["--group=missing"])).toThrow("Unknown test group: missing");
60+
expect(() => resolveTestInvocation(["--group=toString"])).toThrow(
61+
"Unknown test group: toString",
62+
);
63+
expect(() => resolveTestInvocation(["--group=default", "--group=integration"])).toThrow(
64+
"Only one --group argument may be provided",
65+
);
66+
});
67+
68+
test("keeps filtered and coverage-producing invocations serial", () => {
69+
expect(requiresSerialTestExecution([])).toBe(false);
70+
expect(requiresSerialTestExecution(["--rerun-each=2"])).toBe(false);
71+
expect(requiresSerialTestExecution(["-t", "one test"])).toBe(true);
72+
expect(requiresSerialTestExecution(["--only"])).toBe(true);
73+
expect(requiresSerialTestExecution(["--test-name-pattern=one test"])).toBe(true);
74+
expect(requiresSerialTestExecution(["--coverage"])).toBe(true);
75+
expect(requiresSerialTestExecution(["--coverage-dir=coverage/custom"])).toBe(true);
76+
});
77+
4178
test("builds serial and sharded Bun commands", () => {
4279
expect(buildTestShardCommand("/opt/bun", 1, 1, [], "linux")).toEqual([
4380
"/opt/bun",
@@ -53,11 +90,9 @@ describe("test suite sharding", () => {
5390
...DEFAULT_TEST_PATTERNS,
5491
"--rerun-each=2",
5592
]);
56-
expect(buildTestShardCommand("C:\\bun.exe", 1, 1, [], "win32")).toEqual([
57-
"C:\\bun.exe",
58-
"test",
59-
...DEFAULT_TEST_PATTERNS,
60-
]);
93+
expect(
94+
buildTestShardCommand("C:\\bun.exe", 1, 1, [], "win32", TEST_PATTERN_GROUPS.integration),
95+
).toEqual(["C:\\bun.exe", "test", ...TEST_PATTERN_GROUPS.integration]);
6196
});
6297

6398
test("forwards termination while tolerating an already stopped shard", () => {

scripts/test/run-test-suite.ts

Lines changed: 76 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
#!/usr/bin/env bun
22

33
/**
4-
* Runs Hunk's default tests concurrently without Bun's isolated parallel worker mode.
4+
* Runs Hunk's test groups concurrently without Bun's isolated parallel worker mode.
55
*
66
* Bun 1.3.14's `--parallel` implies `--isolate`, which makes OpenTUI's native FFI
77
* renderer fail to initialize with "Cannot access 'default' before initialization."
88
* Independent `--shard=N/M` processes avoid that failure, but Bun runs only the one
9-
* requested shard, so this module launches and supervises every shard. Sharding stays
10-
* Linux-only because the complete multi-process suite is validated and benchmarked there.
9+
* requested shard, so this module launches and supervises every shard. The default suite
10+
* shards automatically on Linux; resource-intensive PTY tests and non-Linux platforms
11+
* stay serial unless CI or a developer explicitly chooses a validated shard count.
1112
*/
1213

1314
import { availableParallelism } from "node:os";
1415

15-
export const DEFAULT_TEST_PATTERNS = [
16-
"./packages",
17-
"./scripts",
18-
"./examples",
19-
"./test/cli",
20-
"./test/session",
21-
] as const;
16+
export const TEST_PATTERN_GROUPS = {
17+
default: ["./packages", "./scripts", "./examples", "./test/cli", "./test/session"],
18+
integration: ["./test/pty"],
19+
} as const;
20+
21+
export const DEFAULT_TEST_PATTERNS = TEST_PATTERN_GROUPS.default;
22+
export type TestPatternGroup = keyof typeof TEST_PATTERN_GROUPS;
2223

2324
const MAX_AUTOMATIC_TEST_SHARDS = 2;
2425
const MAX_EXPLICIT_TEST_SHARDS = 64;
@@ -28,14 +29,12 @@ type KillableProcess = {
2829
kill(signal?: number | NodeJS.Signals): void;
2930
};
3031

31-
/** Resolve a Linux shard override or choose a bounded count from the available CPUs. */
32+
/** Resolve an explicit shard override or choose a bounded Linux count from available CPUs. */
3233
export function resolveTestShardCount(
3334
cpuCount: number,
3435
override?: string,
3536
platform: NodeJS.Platform = process.platform,
3637
) {
37-
if (platform !== "linux") return 1;
38-
3938
if (override !== undefined) {
4039
const count = Number(override);
4140
if (!/^\d+$/.test(override) || !Number.isSafeInteger(count) || count < 1) {
@@ -47,23 +46,66 @@ export function resolveTestShardCount(
4746
return count;
4847
}
4948

49+
if (platform !== "linux") return 1;
5050
return Math.min(MAX_AUTOMATIC_TEST_SHARDS, Math.max(1, Math.floor(cpuCount)));
5151
}
5252

53+
/** Keep resource-intensive PTY tests serial unless the caller explicitly chooses a shard count. */
54+
export function resolveTestGroupShardCount(
55+
cpuCount: number,
56+
override: string | undefined,
57+
platform: NodeJS.Platform,
58+
group: TestPatternGroup,
59+
) {
60+
if (group === "integration" && override === undefined) return 1;
61+
return resolveTestShardCount(cpuCount, override, platform);
62+
}
63+
64+
/** Resolve the selected test group while preserving arguments meant for Bun's test runner. */
65+
export function resolveTestInvocation(args: string[]) {
66+
const groupArguments = args.filter((arg) => arg.startsWith("--group="));
67+
if (groupArguments.length > 1) {
68+
throw new Error("Only one --group argument may be provided");
69+
}
70+
71+
const group = (groupArguments[0]?.slice("--group=".length) ?? "default") as TestPatternGroup;
72+
if (!Object.hasOwn(TEST_PATTERN_GROUPS, group)) {
73+
throw new Error(`Unknown test group: ${group}`);
74+
}
75+
76+
return {
77+
forwardedArgs: args.filter((arg) => !arg.startsWith("--group=")),
78+
group,
79+
patterns: TEST_PATTERN_GROUPS[group],
80+
};
81+
}
82+
83+
/** Keep filtered runs and shared coverage output on one process. */
84+
export function requiresSerialTestExecution(args: string[]) {
85+
return args.some(
86+
(arg) =>
87+
arg === "-t" ||
88+
arg === "--only" ||
89+
arg.startsWith("--test-name-pattern") ||
90+
arg.startsWith("--coverage"),
91+
);
92+
}
93+
5394
/** Build one Bun test command for an independent file shard. */
5495
export function buildTestShardCommand(
5596
bunExecutable: string,
5697
shard: number,
5798
shardCount: number,
5899
forwardedArgs: string[] = [],
59100
platform: NodeJS.Platform = process.platform,
101+
patterns: readonly string[] = DEFAULT_TEST_PATTERNS,
60102
) {
61103
return [
62104
bunExecutable,
63105
"test",
64106
...(platform === "win32" ? [] : ["--no-orphans"]),
65107
...(shardCount > 1 ? [`--shard=${shard}/${shardCount}`] : []),
66-
...DEFAULT_TEST_PATTERNS,
108+
...patterns,
67109
...forwardedArgs,
68110
];
69111
}
@@ -79,19 +121,35 @@ export function terminateTestShardProcesses(processes: KillableProcess[], signal
79121
}
80122
}
81123

82-
/** Run the default suite in independent Bun processes without enabling Bun's isolate mode. */
124+
/** Run one test group in independent Bun processes without enabling Bun's isolate mode. */
83125
export async function main(args = Bun.argv.slice(2)) {
84-
const shardCount = resolveTestShardCount(availableParallelism(), process.env.HUNK_TEST_SHARDS);
126+
const { forwardedArgs, group, patterns } = resolveTestInvocation(args);
127+
const resolvedShardCount = resolveTestGroupShardCount(
128+
availableParallelism(),
129+
process.env.HUNK_TEST_SHARDS,
130+
process.platform,
131+
group,
132+
);
133+
const shardCount = requiresSerialTestExecution(forwardedArgs) ? 1 : resolvedShardCount;
85134
const bunExecutable = process.execPath;
86135

87-
console.error(`Running the test suite in ${shardCount} shard${shardCount === 1 ? "" : "s"}...`);
136+
console.error(
137+
`Running the ${group} test group in ${shardCount} shard${shardCount === 1 ? "" : "s"}...`,
138+
);
88139

89140
const shards: Array<{ proc: ReturnType<typeof Bun.spawn>; shard: number }> = [];
90141
try {
91142
for (let index = 0; index < shardCount; index += 1) {
92143
const shard = index + 1;
93144
const proc = Bun.spawn(
94-
buildTestShardCommand(bunExecutable, shard, shardCount, args, process.platform),
145+
buildTestShardCommand(
146+
bunExecutable,
147+
shard,
148+
shardCount,
149+
forwardedArgs,
150+
process.platform,
151+
patterns,
152+
),
95153
{
96154
cwd: process.cwd(),
97155
env: { ...process.env, npm_execpath: bunExecutable },

0 commit comments

Comments
 (0)