Skip to content

Commit b7d8519

Browse files
authored
feat: Exit with 1 if a test failed (#58)
1 parent 706d3b2 commit b7d8519

File tree

10 files changed

+364
-264
lines changed

10 files changed

+364
-264
lines changed

examples/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"description": "Example perf tests.",
55
"type": "module",
66
"private": true,
7-
"packageManager": "pnpm@8.15.7",
87
"engines": {
98
"node": ">=18"
109
},

examples/src/fail.perf.mts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { suite } from 'perf-insight';
2+
3+
// Use 2 seconds as the default timeout for tests in the suite.
4+
// The `--timeout` option can override this value.
5+
const defaultTimeout = 100;
6+
7+
// ts-check
8+
suite('fail', 'Example with tests that fail or throw exceptions.', async (test) => {
9+
test('ok', () => {
10+
let a = '';
11+
for (let i = 0; i < 1000; ++i) {
12+
a = a + 'a';
13+
}
14+
});
15+
16+
test('fail', () => {
17+
throw new Error('This test failed.');
18+
});
19+
}).setTimeout(defaultTimeout); // set the default timeout for this suite.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"@eslint/js": "^9.7.0",
5050
"@tsconfig/node20": "^20.1.4",
5151
"@types/node": "^20.14.10",
52-
"@vitest/coverage-v8": "^1.6.0",
52+
"@vitest/coverage-v8": "^2.0.2",
5353
"cspell": "^8.10.4",
5454
"cspell-trie-lib": "^8.10.4",
5555
"eslint": "^9.7.0",
@@ -68,6 +68,6 @@
6868
"typescript": "^5.5.3",
6969
"typescript-eslint": "^7.16.0",
7070
"vite": "^5.3.3",
71-
"vitest": "^1.6.0"
71+
"vitest": "^2.0.2"
7272
}
7373
}

packages/perf-insight/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
"version": "1.1.1",
44
"description": "Performance benchmarking tool for NodeJS.",
55
"type": "module",
6-
"packageManager": "pnpm@8.15.7",
76
"engines": {
8-
"node": ">=18"
7+
"node": ">=18.18"
98
},
109
"bin": {
1110
"insight": "./bin.mjs",

packages/perf-insight/src/app.mts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface AppOptions {
1515
suite?: string[];
1616
test?: string[];
1717
register?: string[];
18+
failFast?: boolean;
1819
}
1920

2021
const urlRunnerCli = new URL('./runBenchmarkCli.mjs', import.meta.url).toString();
@@ -34,6 +35,7 @@ export async function app(program = defaultCommand): Promise<Command> {
3435
.option('-t, --timeout <timeout>', 'Override the timeout for each test suite.', (v) => Number(v))
3536
.option('-s, --suite <suite...>', 'Run only matching suites.', appendValue)
3637
.option('-T, --test <test...>', 'Run only matching test found in suites', appendValue)
38+
.option('--fail-fast', 'Stop on first failure.', false)
3739
.option('--repeat <count>', 'Repeat the tests.', (v) => Number(v), 1)
3840
.option('--register <loader>', 'Register a module loader. (e.g. ts-node/esm)', appendValue)
3941
.action(async (suiteNamesToRun: string[], options: AppOptions, command: Command) => {
@@ -62,7 +64,7 @@ export async function app(program = defaultCommand): Promise<Command> {
6264

6365
await spawnRunners(files, options);
6466

65-
console.log(chalk.green('done.'));
67+
process.exitCode ? console.log(chalk.red('failed.')) : console.log(chalk.green('done.'));
6668
});
6769

6870
program.showHelpAfterError();
@@ -97,9 +99,16 @@ async function spawnRunners(files: string[], options: AppOptions): Promise<void>
9799
for (const file of files) {
98100
try {
99101
const code = await spawnRunner([file, ...cliOptions]);
100-
code && console.error('Runner failed with "%s" code: %d', file, code);
102+
if (code) {
103+
// console.error('Runner failed with "%s" code: %d', file, code);
104+
process.exitCode ??= code;
105+
if (options.failFast) {
106+
break;
107+
}
108+
}
101109
} catch (e) {
102110
console.error('Failed to spawn runner.', e);
111+
process.exitCode ??= 1;
103112
}
104113
}
105114
}

packages/perf-insight/src/perfSuite.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export interface RunnerResult {
9696
name: string;
9797
description: string | undefined;
9898
results: TestResult[];
99+
hadFailures: boolean;
99100
}
100101

101102
type TestMethod = () => void | Promise<void> | unknown | Promise<unknown>;
@@ -473,6 +474,7 @@ async function runTests(
473474
name,
474475
description,
475476
results,
477+
hadFailures: results.some((r) => !!r.error),
476478
};
477479
}
478480

packages/perf-insight/src/run.mts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,33 @@ export interface RunOptions {
1111
tests?: string[] | undefined;
1212
}
1313

14+
export interface RunBenchmarkSuitesResult {
15+
hadFailures: boolean;
16+
numSuitesRun: number;
17+
}
18+
1419
/**
1520
*
1621
* @param suiteNames
1722
* @param options
1823
*/
19-
export async function runBenchmarkSuites(suiteToRun?: (string | PerfSuite)[], options?: RunOptions) {
24+
export async function runBenchmarkSuites(
25+
suiteToRun?: (string | PerfSuite)[],
26+
options?: RunOptions,
27+
): Promise<RunBenchmarkSuitesResult> {
2028
const suites = getActiveSuites();
2129

2230
let numSuitesRun = 0;
2331
let showRepeatMsg = false;
32+
let hadErrors = false;
2433

2534
for (let repeat = options?.repeat || 1; repeat > 0; repeat--) {
2635
if (showRepeatMsg) {
2736
console.log(chalk.yellow(`Repeating tests: ${repeat} more time${repeat > 1 ? 's' : ''}.`));
2837
}
29-
numSuitesRun = await runTestSuites(suites, suiteToRun || suites, options || {});
38+
const r = await runTestSuites(suites, suiteToRun || suites, options || {});
39+
numSuitesRun = r.numSuitesRun;
40+
hadErrors ||= r.hadFailures;
3041
if (!numSuitesRun) break;
3142
showRepeatMsg = true;
3243
}
@@ -44,18 +55,32 @@ export async function runBenchmarkSuites(suiteToRun?: (string | PerfSuite)[], op
4455
.map((line) => ` ${line}`)
4556
.join('\n'),
4657
);
58+
59+
hadErrors = true;
4760
}
61+
62+
return { hadFailures: hadErrors, numSuitesRun };
63+
}
64+
65+
interface Result {
66+
hadFailures: boolean;
67+
}
68+
69+
interface RunTestSuitesResults extends Result {
70+
numSuitesRun: number;
4871
}
4972

5073
async function runTestSuites(
5174
suites: PerfSuite[],
5275
suitesToRun: (string | PerfSuite)[],
5376
options: RunOptions,
54-
): Promise<number> {
77+
): Promise<RunTestSuitesResults> {
5578
const timeout = options.timeout || undefined;
5679
const suitesRun = new Set<PerfSuite>();
80+
let hadFailures = false;
5781

58-
async function _runSuite(suites: PerfSuite[]) {
82+
async function _runSuite(suites: PerfSuite[]): Promise<Result> {
83+
let hadFailures = false;
5984
for (const suite of suites) {
6085
if (suitesRun.has(suite)) continue;
6186
if (!filterSuite(suite)) {
@@ -64,32 +89,36 @@ async function runTestSuites(
6489
}
6590
suitesRun.add(suite);
6691
console.log(chalk.green(`Running Perf Suite: ${suite.name}`));
67-
await suite.setTimeout(timeout).runTests({ tests: options.tests });
92+
const result = await suite.setTimeout(timeout).runTests({ tests: options.tests });
93+
if (result.hadFailures) {
94+
hadFailures = true;
95+
}
6896
}
97+
98+
return { hadFailures: hadFailures };
6999
}
70100

71-
async function runSuite(name: string | PerfSuite) {
101+
async function runSuite(name: string | PerfSuite): Promise<Result> {
72102
if (typeof name !== 'string') {
73103
return await _runSuite([name]);
74104
}
75105

76106
if (name === 'all') {
77-
await _runSuite(suites);
78-
return;
107+
return await _runSuite(suites);
79108
}
80109
const matching = suites.filter((suite) => suite.name.toLowerCase().startsWith(name.toLowerCase()));
81110
if (!matching.length) {
82111
console.log(chalk.red(`Unknown test method: ${name}`));
83-
return;
112+
return { hadFailures: true };
84113
}
85-
await _runSuite(matching);
114+
return await _runSuite(matching);
86115
}
87116

88117
for (const name of suitesToRun) {
89-
await runSuite(name);
118+
hadFailures ||= (await runSuite(name)).hadFailures;
90119
}
91120

92-
return suitesRun.size;
121+
return { hadFailures, numSuitesRun: suitesRun.size };
93122

94123
function filterSuite(suite: PerfSuite): boolean {
95124
const { suites } = options;

packages/perf-insight/src/runBenchmarkCli.mts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@ import { parseArgs } from 'node:util';
99

1010
import { runBenchmarkSuites } from './run.mjs';
1111

12-
const cwdUrl = pathToFileURL(process.cwd() + '/');
12+
interface RunResult {
13+
error?: Error;
14+
/**
15+
* Indicates if there were any failures in the benchmark suites.
16+
* This is not an error, since nothing went wrong in the insight code.
17+
*/
18+
hadFailures: boolean;
19+
}
1320

14-
async function run(args: string[]) {
21+
export async function run(args: string[]): Promise<RunResult> {
1522
const parseConfig = {
1623
args,
1724
strict: true,
@@ -22,16 +29,18 @@ async function run(args: string[]) {
2229
test: { type: 'string', short: 'T', multiple: true },
2330
suite: { type: 'string', short: 'S', multiple: true },
2431
register: { type: 'string', multiple: true },
32+
root: { type: 'string' },
2533
},
2634
} as const satisfies ParseArgsConfig;
2735

2836
const parsed = parseArgs(parseConfig);
37+
const cwdUrl = parsed.values.root ? new URL(parsed.values.root) : pathToFileURL(process.cwd() + '/');
2938

3039
const repeat = Number(parsed.values['repeat'] || '0') || undefined;
3140
const timeout = Number(parsed.values['timeout'] || '0') || undefined;
3241
const tests = parsed.values['test'];
3342
const suites = parsed.values['suite'];
34-
await registerLoaders(parsed.values['register']);
43+
await registerLoaders(parsed.values['register'], cwdUrl);
3544

3645
const errors: Error[] = [];
3746

@@ -54,13 +63,21 @@ async function run(args: string[]) {
5463
console.error('Errors:');
5564
errors.forEach((err) => console.error('- %s\n%o', err.message, err.cause));
5665
process.exitCode = 1;
57-
return;
66+
return { error: errors[0], hadFailures: true };
5867
}
5968

60-
await runBenchmarkSuites(undefined, { repeat, timeout, tests, suites });
69+
try {
70+
const r = await runBenchmarkSuites(undefined, { repeat, timeout, tests, suites });
71+
process.exitCode = process.exitCode || (r.hadFailures ? 1 : 0);
72+
return { error: errors[0], hadFailures: r.hadFailures };
73+
} catch (e) {
74+
// console.error('Failed to run benchmark suites.', e);
75+
process.exitCode = 1;
76+
return { error: e as Error, hadFailures: true };
77+
}
6178
}
6279

63-
async function registerLoaders(loaders: string[] | undefined) {
80+
async function registerLoaders(loaders: string[] | undefined, cwdUrl: URL) {
6481
if (!loaders?.length) return;
6582

6683
const module = await import('module');
@@ -79,4 +96,10 @@ async function registerLoaders(loaders: string[] | undefined) {
7996
loaders.forEach(registerLoader);
8097
}
8198

82-
run(process.argv.slice(2));
99+
// console.error('args: %o', process.argv);
100+
101+
if ((process.argv[1] ?? '').endsWith('runBenchmarkCli.mjs')) {
102+
run(process.argv.slice(2)).then((result) => {
103+
if (result.error) process.exitCode = 1;
104+
});
105+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { describe, expect, test } from 'vitest';
2+
3+
import { run } from './runBenchmarkCli.mjs';
4+
5+
describe('runBenchmarkCli', () => {
6+
test.each`
7+
file | args | root
8+
${'src/exampleMap.perf.mts'} | ${'--suite map -t 500'} | ${'../../../examples/'}
9+
`('runBenchmarkCli $file $args', async ({ file, args, root }) => {
10+
expect(run).toBeTypeOf('function');
11+
12+
args = typeof args === 'string' ? args.split(/\s+/g) : args;
13+
const r = new URL(root, import.meta.url).href;
14+
const fileUrl = new URL(file, r).href;
15+
16+
await expect(run([fileUrl, '--root', r, ...args])).resolves.toEqual({
17+
error: undefined,
18+
hadFailures: true,
19+
});
20+
});
21+
});

0 commit comments

Comments
 (0)