Skip to content

Commit d26efe3

Browse files
committed
Implement registerCompletionHandler()
Register a function to be called when AVA has completed a test run without uncaught exceptions or unhandled rejections. Fixes #3279. * * Completion handlers are invoked in order of registration. Results are not awaited.
1 parent 783f62b commit d26efe3

File tree

12 files changed

+98
-2
lines changed

12 files changed

+98
-2
lines changed

docs/01-writing-tests.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ AVA lets you register hooks that are run before and after your tests. This allow
154154

155155
If a test is skipped with the `.skip` modifier, the respective `.beforeEach()`, `.afterEach()` and `.afterEach.always()` hooks are not run. Likewise, if all tests in a test file are skipped `.before()`, `.after()` and `.after.always()` hooks for the file are not run.
156156

157-
*You may not need to use `.afterEach.always()` hooks to clean up after a test.* You can use [`t.teardown()`](./02-execution-context.md#tteardownfn) to undo side-effects *within* a particular test.
157+
*You may not need to use `.afterEach.always()` hooks to clean up after a test.* You can use [`t.teardown()`](./02-execution-context.md#tteardownfn) to undo side-effects *within* a particular test. Or use [`registerCompletionHandler()`](./08-common-pitfalls.md#timeouts-because-a-file-failed-to-exit) to run cleanup code after AVA has completed its work.
158158

159159
Like `test()` these methods take an optional title and an implementation function. The title is shown if your hook fails to execute. The implementation is called with an [execution object](./02-execution-context.md). You can use assertions in your hooks. You can also pass a [macro function](#reusing-test-logic-through-macros) and additional arguments.
160160

docs/07-test-timeouts.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Translations: [Français](https://github.com/avajs/ava-docs/blob/main/fr_FR/docs
44

55
[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/avajs/ava/tree/main/examples/timeouts?file=test.js&terminal=test&view=editor)
66

7-
Timeouts in AVA behave differently than in other test frameworks. AVA resets a timer after each test, forcing tests to quit if no new test results were received within the specified timeout. This can be used to handle stalled tests.
7+
Timeouts in AVA behave differently than in other test frameworks. AVA resets a timer after each test, forcing tests to quit if no new test results were received within the specified timeout. This can be used to handle stalled tests. This same mechanism is used to determine when a test file is preventing a clean exit.
88

99
The default timeout is 10 seconds.
1010

docs/08-common-pitfalls.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,37 @@ Error [ERR_WORKER_INVALID_EXEC_ARGV]: Initiated Worker with invalid execArgv fla
8181

8282
If possible don't specify the command line option when running AVA. Alternatively you could [disable worker threads in AVA](./06-configuration.md#options).
8383

84+
## Timeouts because a file failed to exit
85+
86+
You may get a "Timed out while running tests" error because AVA failed to exit when running a particular file.
87+
88+
AVA waits for Node.js to exit the worker thread or child process. If this takes too long, AVA counts it as a timeout.
89+
90+
It is best practice to make sure your code exits cleanly. We've also seen occurrences where an explicit `process.exit()` call inside a worker thread could not be observed in AVA's main process.
91+
92+
For these reasons we're not providing an option to disable this timeout behavior. However, it is possible to register a callback for when AVA has completed the test run without uncaught exceptions or unhandled rejections. From inside this callback you can do whatever you need to do, including calling `process.exit()`.
93+
94+
Create a `_force-exit.mjs` file:
95+
96+
```js
97+
import process from 'node:process';
98+
import { registerCompletionHandler } from 'ava';
99+
100+
registerCompletionHandler(() => {
101+
process.exit();
102+
});
103+
```
104+
105+
Completion handlers are invoked in order of registration. Results are not awaited.
106+
107+
Load it for all test files through AVA's `require` option:
108+
109+
```js
110+
export default {
111+
require: ['./_force-exit.mjs'],
112+
};
113+
```
114+
84115
## Sharing variables between asynchronous tests
85116

86117
By default AVA executes tests concurrently. This can cause problems if your tests are asynchronous and share variables.

entrypoints/main.d.mts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,11 @@ declare const test: TestFn;
1010

1111
/** Call to declare a test, or chain to declare hooks or test modifiers */
1212
export default test;
13+
14+
/**
15+
* Register a function to be called when AVA has completed a test run without uncaught exceptions or unhandled rejections.
16+
*
17+
* Completion handlers are invoked in order of registration. Results are not awaited.
18+
*/
19+
declare const registerCompletionHandler: (handler: () => void) => void;
20+
export {registerCompletionHandler};

entrypoints/main.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export {default} from '../lib/worker/main.cjs';
2+
export {registerCompletionHandler} from '../lib/worker/completion-handlers.js';

lib/worker/base.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Runner from '../runner.js';
1515
import serializeError from '../serialize-error.js';
1616

1717
import channel from './channel.cjs';
18+
import {runCompletionHandlers} from './completion-handlers.js';
1819
import lineNumberSelection from './line-numbers.js';
1920
import {set as setOptions} from './options.cjs';
2021
import {flags, refs, sharedWorkerTeardowns} from './state.cjs';
@@ -118,6 +119,7 @@ const run = async options => {
118119
nowAndTimers.setImmediate(() => {
119120
const unhandled = currentlyUnhandled();
120121
if (unhandled.length === 0) {
122+
runCompletionHandlers();
121123
return;
122124
}
123125

lib/worker/completion-handlers.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import process from 'node:process';
2+
3+
import state from './state.cjs';
4+
5+
export function runCompletionHandlers() {
6+
for (const handler of state.completionHandlers) {
7+
process.nextTick(() => handler());
8+
}
9+
}
10+
11+
export function registerCompletionHandler(handler) {
12+
state.completionHandlers.push(handler);
13+
}

lib/worker/state.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict';
22
exports.flags = {loadedMain: false};
33
exports.refs = {runnerChain: null};
4+
exports.completionHandlers = [];
45
exports.sharedWorkerTeardowns = [];
56
exports.waitForReady = [];
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import test, { registerCompletionHandler } from 'ava'
2+
3+
registerCompletionHandler(() => {
4+
console.error('one')
5+
})
6+
7+
test('pass', t => {
8+
t.pass()
9+
})
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"type": "module",
3+
"ava": {
4+
"files": [
5+
"*.js"
6+
]
7+
}
8+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import test, { registerCompletionHandler } from 'ava'
2+
3+
registerCompletionHandler(() => {
4+
console.error('one')
5+
})
6+
registerCompletionHandler(() => {
7+
console.error('two')
8+
})
9+
10+
test('pass', t => t.pass())

test/completion-handlers/test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import test from '@ava/test';
2+
3+
import {cleanOutput, fixture} from '../helpers/exec.js';
4+
5+
test('runs a single completion handler', async t => {
6+
const result = await fixture(['one.js']);
7+
t.is(cleanOutput(result.stderr), 'one');
8+
});
9+
10+
test('runs multiple completion handlers in registration order', async t => {
11+
const result = await fixture(['two.js']);
12+
t.deepEqual(cleanOutput(result.stderr).split('\n'), ['one', 'two']);
13+
});

0 commit comments

Comments
 (0)