Skip to content

Commit

Permalink
test_runner: support timeout for tests
Browse files Browse the repository at this point in the history
  • Loading branch information
MoLow committed Jun 20, 2022
1 parent 6ac55fa commit 73e1a03
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 8 deletions.
6 changes: 6 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ added: v18.0.0
* `todo` {boolean|string} If truthy, the test marked as `TODO`. If a string
is provided, that string is displayed in the test results as the reason why
the test is `TODO`. **Default:** `false`.
* `timeout` {number} A number of milliseconds the test will fail after.
If unspecified, subtests inherit this value from their parent.
**Default:** `30_000`.
* `fn` {Function|AsyncFunction} The function under test. This first argument
to this function is a [`TestContext`][] object. If the test uses callbacks,
the callback function is passed as the second argument. **Default:** A no-op
Expand Down Expand Up @@ -449,6 +452,9 @@ added: v18.0.0
* `todo` {boolean|string} If truthy, the test marked as `TODO`. If a string
is provided, that string is displayed in the test results as the reason why
the test is `TODO`. **Default:** `false`.
* `timeout` {number} A number of milliseconds the test will fail after.
If unspecified, subtests inherit this value from their parent.
**Default:** `30_000`.
* `fn` {Function|AsyncFunction} The function under test. This first argument
to this function is a [`TestContext`][] object. If the test uses callbacks,
the callback function is passed as the second argument. **Default:** A no-op
Expand Down
30 changes: 26 additions & 4 deletions lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
FunctionPrototype,
Number,
SafeMap,
PromiseRace,
} = primordials;
const { AsyncResource } = require('async_hooks');
const {
Expand All @@ -29,12 +30,27 @@ const kMultipleCallbackInvocations = 'multipleCallbackInvocations';
const kParentAlreadyFinished = 'parentAlreadyFinished';
const kSubtestsFailed = 'subtestsFailed';
const kTestCodeFailure = 'testCodeFailure';
const kTestTimeoutFailure = 'testTimeoutFailure';
const kDefaultIndent = ' ';
const noop = FunctionPrototype;
const isTestRunner = getOptionValue('--test');
const testOnlyFlag = !isTestRunner && getOptionValue('--test-only');
// TODO(cjihrig): Use uv_available_parallelism() once it lands.
const rootConcurrency = isTestRunner ? cpus().length : 1;
const rootTimeout = 30_0000;


function PromiseTimeout(promise, timeout) {
const deferred = createDeferredPromise();
setTimeout(() => deferred.reject(new ERR_TEST_FAILURE(
`test timed out after ${timeout}ms`,
kTestTimeoutFailure
)), timeout).unref();
return PromiseRace([
promise,
deferred.promise,
]);
}

class TestContext {
#test;
Expand Down Expand Up @@ -71,7 +87,7 @@ class Test extends AsyncResource {
super('Test');

let { fn, name, parent, skip } = options;
const { concurrency, only, todo } = options;
const { concurrency, only, todo, timeout } = options;

if (typeof fn !== 'function') {
fn = noop;
Expand All @@ -93,6 +109,7 @@ class Test extends AsyncResource {
this.reporter = new TapStream();
this.runOnlySubtests = this.only;
this.testNumber = 0;
this.timeout = rootTimeout;
} else {
const indent = parent.parent === null ? parent.indent :
parent.indent + parent.indentString;
Expand All @@ -104,12 +121,17 @@ class Test extends AsyncResource {
this.reporter = parent.reporter;
this.runOnlySubtests = !this.only;
this.testNumber = parent.subtests.length + 1;
this.timeout = parent.timeout;
}

if (isUint32(concurrency) && concurrency !== 0) {
this.concurrency = concurrency;
}

if (isUint32(timeout) && timeout !== 0) {
this.timeout = timeout;
}

if (testOnlyFlag && !this.only) {
skip = '\'only\' option not set';
}
Expand Down Expand Up @@ -337,13 +359,13 @@ class Test extends AsyncResource {
'passed a callback but also returned a Promise',
kCallbackAndPromisePresent
));
await ret;
await PromiseTimeout(ret, this.timeout);
} else {
await promise;
await PromiseTimeout(promise, this.timeout);
}
} else {
// This test is synchronous or using Promises.
await this.runInAsyncScope(this.fn, ctx, ctx);
await PromiseTimeout(this.runInAsyncScope(this.fn, ctx, ctx), this.timeout);
}

this.pass();
Expand Down
10 changes: 10 additions & 0 deletions test/message/test_runner_output.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,13 @@ test('subtest sync throw fails', async (t) => {
throw new Error('thrown from subtest sync throw fails at second');
});
});

test('timed out test', { timeout: 500 }, async (t) => {
return new Promise((resolve) => {
setTimeout(resolve, 1000)
});
});

test('callback timed out test', { timeout: 500 }, (t, done) => {
setTimeout(done, 1000)
});
24 changes: 20 additions & 4 deletions test/message/test_runner_output.out
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,24 @@ not ok 56 - subtest sync throw fails
error: '2 subtests failed'
code: 'ERR_TEST_FAILURE'
...
# Subtest: timed out test
not ok 57 - timed out test
---
duration_ms: *
failureType: 'testTimeoutFailure'
error: 'test timed out after 500ms'
code: 'ERR_TEST_FAILURE'
...
# Subtest: callback timed out test
not ok 58 - callback timed out test
---
duration_ms: *
failureType: 'testTimeoutFailure'
error: 'test timed out after 500ms'
code: 'ERR_TEST_FAILURE'
...
# Subtest: invalid subtest fail
not ok 57 - invalid subtest fail
not ok 59 - invalid subtest fail
---
duration_ms: *
failureType: 'parentAlreadyFinished'
Expand All @@ -572,16 +588,16 @@ not ok 57 - invalid subtest fail
stack: |-
*
...
1..57
1..59
# Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
# Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
# Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event.
# Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
# Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: callback invoked multiple times" and would have caused the test to fail, but instead triggered an uncaughtException event.
# Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event.
# tests 57
# tests 59
# pass 24
# fail 18
# fail 20
# skipped 10
# todo 5
# duration_ms *

0 comments on commit 73e1a03

Please sign in to comment.