mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-21 20:30:47 +02:00
176 lines
7.0 KiB
TypeScript
176 lines
7.0 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
import { EventEmitter } from 'node:events';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import {
|
|
BunTestOutputClassifier,
|
|
classifyBunTestOutputLine,
|
|
exactTestFileSelectors,
|
|
installChildSignalForwarding,
|
|
parseBunTerminalSummaryLine,
|
|
planDefaultFreeTestShards,
|
|
strictTestExitCode,
|
|
terminationSignalExitCode,
|
|
type TerminationTimerApi,
|
|
} from '../scripts/test-free-strict';
|
|
|
|
describe('strict default free-test runner', () => {
|
|
test('plans exactly one directly owned child per free test file', () => {
|
|
const shards = planDefaultFreeTestShards([
|
|
'test/z.test.ts',
|
|
'browse/test/a.test.ts',
|
|
'test/m.test.ts',
|
|
]);
|
|
expect(shards).toEqual([
|
|
['browse/test/a.test.ts'],
|
|
['test/m.test.ts'],
|
|
['test/z.test.ts'],
|
|
]);
|
|
expect(shards.every((shard) => shard.length === 1)).toBe(true);
|
|
});
|
|
|
|
test('uses absolute selectors so Bun substring matching cannot add a namesake file', () => {
|
|
const root = path.join(path.parse(process.cwd()).root, 'repo');
|
|
const [topLevel, nested] = exactTestFileSelectors([
|
|
'test/learnings-injection.test.ts',
|
|
'browse/test/learnings-injection.test.ts',
|
|
], root);
|
|
expect(path.isAbsolute(topLevel)).toBe(true);
|
|
expect(path.isAbsolute(nested)).toBe(true);
|
|
expect(nested.includes(topLevel)).toBe(false);
|
|
});
|
|
|
|
test('keeps the Windows package entrypoint on singleton shards', () => {
|
|
const packageJson = JSON.parse(fs.readFileSync(path.resolve(import.meta.dir, '..', 'package.json'), 'utf8'));
|
|
expect(packageJson.scripts['test:windows']).toBe(
|
|
'bun run scripts/test-free-shards.ts --windows-only --shards 10000',
|
|
);
|
|
});
|
|
|
|
test('recognizes Bun failed-test results with ANSI and CRLF', () => {
|
|
expect(classifyBunTestOutputLine('(fail) suite > case [158.31ms]')).toBe('failed-test');
|
|
expect(classifyBunTestOutputLine('\u001b[31m(fail) suite > case [1.00s]\u001b[0m\r')).toBe('failed-test');
|
|
expect(classifyBunTestOutputLine('(fail) suite > case [12\u00b5s]')).toBe('failed-test');
|
|
});
|
|
|
|
test('recognizes the exact between-tests unhandled error banner', () => {
|
|
expect(classifyBunTestOutputLine('# Unhandled error between tests')).toBe('unhandled-between-tests');
|
|
expect(classifyBunTestOutputLine('\u001b[1m# Unhandled error between tests\u001b[0m\r')).toBe('unhandled-between-tests');
|
|
});
|
|
|
|
test('parses only exact Bun terminal summaries and their file count', () => {
|
|
expect(parseBunTerminalSummaryLine('Ran 1 test across 1 file. [34.00ms]')).toBe(1);
|
|
expect(parseBunTerminalSummaryLine('\u001b[32mRan 361 tests across 5 files. [1.89s]\u001b[0m\r')).toBe(5);
|
|
expect(parseBunTerminalSummaryLine('Expected: Ran 1 test across 1 file. [1ms]')).toBeNull();
|
|
expect(parseBunTerminalSummaryLine('12 | Ran 1 test across 1 file. [1ms]')).toBeNull();
|
|
expect(parseBunTerminalSummaryLine('Ran tests across 1 file.')).toBeNull();
|
|
});
|
|
|
|
test('ignores source excerpts, assertion text, and ordinary test names', () => {
|
|
const nonResults = [
|
|
'42 | const sample = "(fail) suite > case [1ms]";',
|
|
'Expected: "(fail) suite > case [1ms]"',
|
|
'(pass) classifier > expected (fail) text [0.12ms]',
|
|
'log: (fail) suite > case [1ms]',
|
|
'# Unhandled error between tests is the expected fixture text',
|
|
' # Unhandled error between tests',
|
|
'(fail) missing Bun duration',
|
|
];
|
|
for (const line of nonResults) expect(classifyBunTestOutputLine(line)).toBeNull();
|
|
});
|
|
|
|
test('classifies markers split across arbitrary process chunks', () => {
|
|
const classifier = new BunTestOutputClassifier();
|
|
classifier.write('bun test v1\n(fa');
|
|
classifier.write('il) suite > case [2.50');
|
|
classifier.write('ms]\n# Unhandled error bet');
|
|
classifier.write('ween tests\r\nRan 2 tests across 1 fi');
|
|
classifier.write('le. [3.00ms]\n(pass) next [1ms]');
|
|
expect(classifier.end()).toEqual({
|
|
failedTests: 1,
|
|
unhandledBetweenTests: 1,
|
|
terminalFileCounts: [1],
|
|
});
|
|
});
|
|
|
|
test('fails closed on markers or incomplete summaries while preserving child exits', () => {
|
|
const clean = { failedTests: 0, unhandledBetweenTests: 0, terminalFileCounts: [] };
|
|
const complete = { failedTests: 0, unhandledBetweenTests: 0, terminalFileCounts: [3] };
|
|
const wrongCount = { failedTests: 0, unhandledBetweenTests: 0, terminalFileCounts: [2] };
|
|
const failed = { failedTests: 1, unhandledBetweenTests: 0, terminalFileCounts: [3] };
|
|
const unhandled = { failedTests: 0, unhandledBetweenTests: 1, terminalFileCounts: [3] };
|
|
|
|
expect(strictTestExitCode(0, clean)).toBe(0);
|
|
expect(strictTestExitCode(0, clean, 3)).toBe(1);
|
|
expect(strictTestExitCode(0, complete, 3)).toBe(0);
|
|
expect(strictTestExitCode(0, wrongCount, 3)).toBe(1);
|
|
expect(strictTestExitCode(0, failed, 3)).toBe(1);
|
|
expect(strictTestExitCode(0, unhandled, 3)).toBe(1);
|
|
expect(strictTestExitCode(7, failed, 3)).toBe(7);
|
|
expect(strictTestExitCode(130, clean)).toBe(130);
|
|
});
|
|
|
|
test('forwards termination, escalates deterministically, and reports signal exits', () => {
|
|
const source = new EventEmitter();
|
|
const kills: string[] = [];
|
|
const scheduled: Array<{ callback: () => void; delayMs: number; cancelled: boolean }> = [];
|
|
const timer: TerminationTimerApi = {
|
|
schedule(callback, delayMs) {
|
|
const handle = { callback, delayMs, cancelled: false };
|
|
scheduled.push(handle);
|
|
return handle;
|
|
},
|
|
cancel(handle) {
|
|
(handle as (typeof scheduled)[number]).cancelled = true;
|
|
},
|
|
};
|
|
const forwarding = installChildSignalForwarding(
|
|
{ kill: (signal) => { kills.push(String(signal)); return true; } },
|
|
source,
|
|
timer,
|
|
1234,
|
|
);
|
|
|
|
source.emit('SIGTERM');
|
|
expect(kills).toEqual(['SIGTERM']);
|
|
expect(forwarding.receivedSignal).toBe('SIGTERM');
|
|
expect(terminationSignalExitCode(forwarding.receivedSignal!)).toBe(143);
|
|
expect(scheduled.map(({ delayMs }) => delayMs)).toEqual([1234]);
|
|
|
|
scheduled[0].callback();
|
|
expect(kills).toEqual(['SIGTERM', 'SIGKILL']);
|
|
|
|
source.emit('SIGTERM');
|
|
expect(kills).toEqual(['SIGTERM', 'SIGKILL', 'SIGKILL']);
|
|
forwarding.dispose();
|
|
source.emit('SIGTERM');
|
|
expect(kills).toHaveLength(3);
|
|
});
|
|
|
|
test('hard-kills on parent exit and removes pending cleanup when disposed', () => {
|
|
const source = new EventEmitter();
|
|
const kills: string[] = [];
|
|
let timerCancelled = false;
|
|
const timer: TerminationTimerApi = {
|
|
schedule: () => ({ pending: true }),
|
|
cancel: () => { timerCancelled = true; },
|
|
};
|
|
const forwarding = installChildSignalForwarding(
|
|
{ kill: (signal) => { kills.push(String(signal)); return true; } },
|
|
source,
|
|
timer,
|
|
);
|
|
|
|
source.emit('exit');
|
|
expect(kills).toEqual(['SIGKILL']);
|
|
source.emit('SIGINT');
|
|
expect(kills).toEqual(['SIGKILL', 'SIGINT']);
|
|
expect(terminationSignalExitCode('SIGINT')).toBe(130);
|
|
|
|
forwarding.dispose();
|
|
expect(timerCancelled).toBe(true);
|
|
source.emit('exit');
|
|
expect(kills).toEqual(['SIGKILL', 'SIGINT']);
|
|
});
|
|
});
|