test.ts
| 1 | import { readdirSync } from 'fs'; |
| 2 | import { spawn } from 'child_process'; |
| 3 | import path from 'path'; |
| 4 | |
| 5 | const testsDir = path.resolve('tests'); |
| 6 | const files = readdirSync(testsDir) |
| 7 | .filter(f => f.endsWith('.test.ts')) |
| 8 | .sort(); |
| 9 | |
| 10 | const STALL_TIMEOUT = 20_000; // kill if no output for 20s |
| 11 | const MAX_RETRIES = 3; |
| 12 | // retry logic needed because tests get randomly get stuck on startup with bun |
| 13 | // strace shows bun completely spinning in futex and not doing anything else. |
| 14 | // Only a stall-kill is retried — a genuine assertion failure returns |
| 15 | // {ok: false, stalled: false} and must NOT be retried, or a real product bug |
| 16 | // that fails intermittently would be laundered into a pass. |
| 17 | function runTest(filePath: string): Promise<{ ok: boolean; stalled: boolean }> { |
| 18 | return new Promise((resolve) => { |
| 19 | const child = spawn('bun', ['test', '--bail=1', '--timeout', '30000', filePath], { |
| 20 | stdio: ['ignore', 'pipe', 'pipe'], |
| 21 | }); |
| 22 | |
| 23 | let stalled = false; |
| 24 | let timer = setTimeout(onStall, STALL_TIMEOUT); |
| 25 | |
| 26 | function onStall() { |
| 27 | stalled = true; |
| 28 | console.error(`\n[test-runner] stall detected, killing ${path.basename(filePath)} (no output for ${STALL_TIMEOUT / 1000}s)`); |
| 29 | child.kill('SIGKILL'); |
| 30 | } |
| 31 | |
| 32 | function resetTimer() { |
| 33 | clearTimeout(timer); |
| 34 | timer = setTimeout(onStall, STALL_TIMEOUT); |
| 35 | } |
| 36 | |
| 37 | child.stdout!.on('data', (chunk: Buffer) => { |
| 38 | process.stdout.write(chunk); |
| 39 | resetTimer(); |
| 40 | }); |
| 41 | child.stderr!.on('data', (chunk: Buffer) => { |
| 42 | process.stderr.write(chunk); |
| 43 | resetTimer(); |
| 44 | }); |
| 45 | |
| 46 | child.on('close', (code) => { |
| 47 | clearTimeout(timer); |
| 48 | resolve({ ok: code === 0, stalled }); |
| 49 | }); |
| 50 | }); |
| 51 | } |
| 52 | |
| 53 | let passed = 0; |
| 54 | let failed = 0; |
| 55 | |
| 56 | for (const file of files) { |
| 57 | const filePath = path.join(testsDir, file); |
| 58 | let ok = false; |
| 59 | |
| 60 | for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { |
| 61 | if (attempt > 1) { |
| 62 | console.log(`[test-runner] retrying ${file} (attempt ${attempt}/${MAX_RETRIES})`); |
| 63 | } |
| 64 | const result = await runTest(filePath); |
| 65 | ok = result.ok; |
| 66 | // Retry only the futex stall — a genuine failure is final. |
| 67 | if (ok || !result.stalled) break; |
| 68 | } |
| 69 | |
| 70 | if (ok) passed++; |
| 71 | else failed++; |
| 72 | } |
| 73 | |
| 74 | console.log(`\n${passed + failed} test files: ${passed} passed${failed ? `, ${failed} failed` : ''}`); |
| 75 | if (failed > 0) process.exit(1); |
| 76 |