ReviewOS

also looking at this

stacks/bun-query-builder

test(cli): run the CLI in one process instead of two

#1192
Merged glennmichael123 wants to merge test/cli-single-process-spawn into main
1 file +39 -27
packages/bun-query-builder/test/cli.test.tsmodified+39-27
Changes to packages/bun-query-builder/test/cli.test.ts
@@ -19,39 +19,51 @@ afterAll(async () => {
1919 await deleteMigrationFiles(EXAMPLES_MODELS_PATH, undefined, { dialect: 'postgres' })
2020})
2121
22function runCli(args: string[]) {
23 const outputDir = mkdtempSync(join(tmpdir(), 'qb-cli-output-'))
24 const runnerPath = join(outputDir, 'runner.ts')
25 const resultPath = join(outputDir, 'result.json')
26 writeFileSync(runnerPath, `
27const proc = Bun.spawnSync({
28 cmd: ${JSON.stringify([CLI_PATH, ...args])},
29 cwd: ${JSON.stringify(process.cwd())},
30 env: { ...process.env },
31 stdout: 'pipe',
32 stderr: 'pipe',
33})
34const decoder = new TextDecoder()
35await Bun.write(${JSON.stringify(resultPath)}, JSON.stringify({
36 code: proc.exitCode,
37 stdout: decoder.decode(proc.stdout).trim(),
38 stderr: decoder.decode(proc.stderr).trim(),
39}))
40`)
41 const runner = spawnSync(process.execPath, [runnerPath], {
22/**
23 * Run the compiled CLI and return its exit code and captured output.
24 *
25 * One process. This used to be two: it wrote a runner script to a temp dir,
26 * spawned Bun to execute it, had that process `Bun.spawnSync` the CLI and write
27 * the result to a JSON file, then read the file back and deleted the directory.
28 * Every one of the 64 tests here paid a whole extra Bun startup plus a
29 * filesystem round-trip for it, under bun:test's default 5000ms budget and with
30 * no explicit timeout anywhere in the file.
31 *
32 * That is what made this the suite's flakiest file. Under load the budget
33 * killed the OUTER process mid-flight, so the failure surfaced as
34 * `CLI test runner exited null` - a message about the harness, naming neither
35 * the command nor the timeout - and `bin/qbx --version` was never the thing
36 * that was slow.
37 *
38 * The indirection bought nothing. It arrived in 8b227d0, the commit that also
39 * switched CLI_PATH from `bin/cli.ts` to the compiled `bin/qbx`, with a
40 * one-line message and no comment, so it reads as scaffolding for that switch
41 * rather than a workaround: `bin/qbx` is a native executable, and node's
42 * `spawnSync` runs it directly from inside bun:test without complaint.
43 *
44 * `status` is null when a signal killed the process, which no longer happens
45 * here but would otherwise be reported as a passing exit code by callers that
46 * check `[0, 1]`, so it is surfaced rather than coerced.
47 */
48function runCli(args: string[]): { code: number | null, stdout: string, stderr: string } {
49 const out = spawnSync(CLI_PATH, args, {
4250 cwd: process.cwd(),
4351 encoding: 'utf8',
4452 env: { ...process.env },
4553 })
4654
47 try {
48 if (runner.status !== 0)
49 throw new Error(`CLI test runner exited ${runner.status}: ${runner.stderr || runner.stdout}`)
55 // ENOENT means nobody built the CLI. Worth separating from a non-zero exit:
56 // `bun run test` builds first, a bare `bun test` does not.
57 if (out.error)
58 throw new Error(`CLI did not run (${CLI_PATH}): ${out.error.message}. Run \`bun run build\`.`)
5059
51 return JSON.parse(readFileSync(resultPath, 'utf8')) as { code: number, stdout: string, stderr: string }
52 }
53 finally {
54 rmSync(outputDir, { recursive: true, force: true })
60 if (out.signal)
61 throw new Error(`CLI was killed by ${out.signal}: ${args.join(' ')}`)
62
63 return {
64 code: out.status,
65 stdout: (out.stdout ?? '').trim(),
66 stderr: (out.stderr ?? '').trim(),
5567 }
5668}
5769