Skip to content

Commit 5b6851f

Browse files
MoLowaduh95
authored andcommitted
feat: support timeout for tests
PR-URL: nodejs/node#43505 Refs: nodejs/node#43490 Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> (cherry picked from commit c1d659591d1c5a6002e762549ee46b69d495b70b)
1 parent 4ca48af commit 5b6851f

File tree

9 files changed

+163
-16
lines changed

9 files changed

+163
-16
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,9 @@ internally.
339339
- `todo` {boolean|string} If truthy, the test marked as `TODO`. If a string
340340
is provided, that string is displayed in the test results as the reason why
341341
the test is `TODO`. **Default:** `false`.
342+
- `timeout` {number} A number of milliseconds the test will fail after.
343+
If unspecified, subtests inherit this value from their parent.
344+
**Default:** `30_000`.
342345
- `fn` {Function|AsyncFunction} The function under test. The first argument
343346
to this function is a [`TestContext`][] object. If the test uses callbacks,
344347
the callback function is passed as the second argument. **Default:** A no-op
@@ -483,6 +486,9 @@ execution of the test function. This function does not return a value.
483486
- `todo` {boolean|string} If truthy, the test marked as `TODO`. If a string
484487
is provided, that string is displayed in the test results as the reason why
485488
the test is `TODO`. **Default:** `false`.
489+
- `timeout` {number} A number of milliseconds the test will fail after.
490+
If unspecified, subtests inherit this value from their parent.
491+
**Default:** `30_000`.
486492
- `fn` {Function|AsyncFunction} The function under test. The first argument
487493
to this function is a [`TestContext`][] object. If the test uses callbacks,
488494
the callback function is passed as the second argument. **Default:** A no-op

lib/internal/per_context/primordials.js

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ exports.ReflectApply = (target, self, args) => Reflect.apply(target, self, args)
3030
exports.Promise = Promise
3131
exports.PromiseAll = iterator => Promise.all(iterator)
3232
exports.PromiseResolve = val => Promise.resolve(val)
33+
exports.PromiseRace = val => Promise.race(val)
3334
exports.SafeArrayIterator = class ArrayIterator {constructor (array) { this.array = array }[Symbol.iterator] () { return this.array.values() }}
3435
exports.SafeMap = Map
3536
exports.SafeSet = Set

lib/internal/test_runner/test.js

+32-7
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ const {
1111
Number,
1212
PromiseResolve,
1313
ReflectApply,
14-
SafeMap
14+
SafeMap,
15+
PromiseRace
1516
} = require('#internal/per_context/primordials')
1617
const { AsyncResource } = require('async_hooks')
1718
const {
@@ -29,20 +30,38 @@ const {
2930
} = require('#internal/util')
3031
const { isPromise } = require('#internal/util/types')
3132
const { isUint32 } = require('#internal/validators')
33+
const { setTimeout } = require('#timers/promises')
3234
const { cpus } = require('os')
3335
const { bigint: hrtime } = process.hrtime
3436
const kCallbackAndPromisePresent = 'callbackAndPromisePresent'
3537
const kCancelledByParent = 'cancelledByParent'
3638
const kParentAlreadyFinished = 'parentAlreadyFinished'
3739
const kSubtestsFailed = 'subtestsFailed'
3840
const kTestCodeFailure = 'testCodeFailure'
41+
const kTestTimeoutFailure = 'testTimeoutFailure'
3942
const kDefaultIndent = ' '
43+
const kDefaultTimeout = null
4044
const noop = FunctionPrototype
4145
const isTestRunner = getOptionValue('--test')
4246
const testOnlyFlag = !isTestRunner && getOptionValue('--test-only')
4347
// TODO(cjihrig): Use uv_available_parallelism() once it lands.
4448
const rootConcurrency = isTestRunner ? cpus().length : 1
4549

50+
function testTimeout (promise, timeout) {
51+
if (timeout === kDefaultTimeout) {
52+
return promise
53+
}
54+
return PromiseRace([
55+
promise,
56+
setTimeout(timeout, null, { ref: false }).then(() => {
57+
throw new ERR_TEST_FAILURE(
58+
`test timed out after ${timeout}ms`,
59+
kTestTimeoutFailure
60+
)
61+
})
62+
])
63+
}
64+
4665
class TestContext {
4766
#test
4867

@@ -79,7 +98,7 @@ class Test extends AsyncResource {
7998
super('Test')
8099

81100
let { fn, name, parent, skip } = options
82-
const { concurrency, only, todo } = options
101+
const { concurrency, only, timeout, todo } = options
83102

84103
if (typeof fn !== 'function') {
85104
fn = noop
@@ -101,6 +120,7 @@ class Test extends AsyncResource {
101120
this.reporter = new TapStream()
102121
this.runOnlySubtests = this.only
103122
this.testNumber = 0
123+
this.timeout = kDefaultTimeout
104124
} else {
105125
const indent = parent.parent === null
106126
? parent.indent
@@ -113,12 +133,17 @@ class Test extends AsyncResource {
113133
this.reporter = parent.reporter
114134
this.runOnlySubtests = !this.only
115135
this.testNumber = parent.subtests.length + 1
136+
this.timeout = parent.timeout
116137
}
117138

118139
if (isUint32(concurrency) && concurrency !== 0) {
119140
this.concurrency = concurrency
120141
}
121142

143+
if (isUint32(timeout)) {
144+
this.timeout = timeout
145+
}
146+
122147
if (testOnlyFlag && !this.only) {
123148
skip = '\'only\' option not set'
124149
}
@@ -334,13 +359,13 @@ class Test extends AsyncResource {
334359
'passed a callback but also returned a Promise',
335360
kCallbackAndPromisePresent
336361
))
337-
await ret
362+
await testTimeout(ret, this.timeout)
338363
} else {
339-
await promise
364+
await testTimeout(promise, this.timeout)
340365
}
341366
} else {
342367
// This test is synchronous or using Promises.
343-
await ReflectApply(this.runInAsyncScope, this, args)
368+
await testTimeout(ReflectApply(this.runInAsyncScope, this, args), this.timeout)
344369
}
345370

346371
this.pass()
@@ -485,10 +510,10 @@ class Suite extends Test {
485510
this.parent.activeSubtests++
486511
this.startTime = hrtime()
487512
const subtests = this.skipped || this.error ? [] : this.subtests
488-
await ArrayPrototypeReduce(subtests, async (prev, subtest) => {
513+
await testTimeout(ArrayPrototypeReduce(subtests, async (prev, subtest) => {
489514
await prev
490515
await subtest.run()
491-
}, PromiseResolve())
516+
}, PromiseResolve()), this.timeout)
492517
this.pass()
493518
this.postRun()
494519
}

lib/timers/promises.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict'
2+
try {
3+
module.exports = require('node:timers/promises')
4+
} catch {
5+
const { promisify } = require('node:util')
6+
module.exports = {
7+
setImmediate: promisify(setImmediate),
8+
setInterval: promisify(setInterval),
9+
setTimeout: promisify(setTimeout)
10+
}
11+
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
},
1414
"imports": {
1515
"#internal/*": "./lib/internal/*.js",
16+
"#timers/*": "./lib/timers/*.js",
1617
"#node:test": "./lib/test.js"
1718
},
1819
"scripts": {

test/message/test_runner_desctibe_it.js

+22
Original file line numberDiff line numberDiff line change
@@ -316,3 +316,25 @@ describe('describe async throw fails', async () => {
316316
it('should not run', () => {})
317317
throw new Error('thrown from describe')
318318
})
319+
320+
describe('timeouts', () => {
321+
it('timed out async test', { timeout: 5 }, async () => {
322+
return new Promise((resolve) => {
323+
setTimeout(resolve, 1000)
324+
})
325+
})
326+
327+
it('timed out callback test', { timeout: 5 }, (done) => {
328+
setTimeout(done, 1000)
329+
})
330+
331+
it('large timeout async test is ok', { timeout: 30_000_000 }, async () => {
332+
return new Promise((resolve) => {
333+
setTimeout(resolve, 10)
334+
})
335+
})
336+
337+
it('large timeout callback test is ok', { timeout: 30_000_000 }, (done) => {
338+
setTimeout(done, 10)
339+
})
340+
})

test/message/test_runner_desctibe_it.out

+39-4
Original file line numberDiff line numberDiff line change
@@ -557,8 +557,43 @@ not ok 56 - describe async throw fails
557557
*
558558
*
559559
...
560+
# Subtest: timeouts
561+
# Subtest: timed out async test
562+
not ok 1 - timed out async test
563+
---
564+
duration_ms: *
565+
failureType: 'testTimeoutFailure'
566+
error: 'test timed out after 5ms'
567+
code: 'ERR_TEST_FAILURE'
568+
...
569+
# Subtest: timed out callback test
570+
not ok 2 - timed out callback test
571+
---
572+
duration_ms: *
573+
failureType: 'testTimeoutFailure'
574+
error: 'test timed out after 5ms'
575+
code: 'ERR_TEST_FAILURE'
576+
...
577+
# Subtest: large timeout async test is ok
578+
ok 3 - large timeout async test is ok
579+
---
580+
duration_ms: *
581+
...
582+
# Subtest: large timeout callback test is ok
583+
ok 4 - large timeout callback test is ok
584+
---
585+
duration_ms: *
586+
...
587+
1..4
588+
not ok 57 - timeouts
589+
---
590+
duration_ms: *
591+
failureType: 'subtestsFailed'
592+
error: '2 subtests failed'
593+
code: 'ERR_TEST_FAILURE'
594+
...
560595
# Subtest: invalid subtest fail
561-
not ok 57 - invalid subtest fail
596+
not ok 58 - invalid subtest fail
562597
---
563598
duration_ms: *
564599
failureType: 'parentAlreadyFinished'
@@ -567,16 +602,16 @@ not ok 57 - invalid subtest fail
567602
stack: |-
568603
*
569604
...
570-
1..57
605+
1..58
571606
# 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.
572607
# 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.
573608
# 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.
574609
# 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.
575610
# 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.
576611
# 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.
577-
# tests 57
612+
# tests 58
578613
# pass 23
579-
# fail 20
614+
# fail 21
580615
# cancelled 0
581616
# skipped 9
582617
# todo 5

test/message/test_runner_output.js

+20
Original file line numberDiff line numberDiff line change
@@ -329,3 +329,23 @@ test('subtest sync throw fails', async (t) => {
329329
throw new Error('thrown from subtest sync throw fails at second')
330330
})
331331
})
332+
333+
test('timed out async test', { timeout: 5 }, async (t) => {
334+
return new Promise((resolve) => {
335+
setTimeout(resolve, 1000)
336+
})
337+
})
338+
339+
test('timed out callback test', { timeout: 5 }, (t, done) => {
340+
setTimeout(done, 1000)
341+
})
342+
343+
test('large timeout async test is ok', { timeout: 30_000_000 }, async (t) => {
344+
return new Promise((resolve) => {
345+
setTimeout(resolve, 10)
346+
})
347+
})
348+
349+
test('large timeout callback test is ok', { timeout: 30_000_000 }, (t, done) => {
350+
setTimeout(done, 10)
351+
})

test/message/test_runner_output.out

+31-5
Original file line numberDiff line numberDiff line change
@@ -562,8 +562,34 @@ not ok 56 - subtest sync throw fails
562562
error: '2 subtests failed'
563563
code: 'ERR_TEST_FAILURE'
564564
...
565+
# Subtest: timed out async test
566+
not ok 57 - timed out async test
567+
---
568+
duration_ms: *
569+
failureType: 'testTimeoutFailure'
570+
error: 'test timed out after 5ms'
571+
code: 'ERR_TEST_FAILURE'
572+
...
573+
# Subtest: timed out callback test
574+
not ok 58 - timed out callback test
575+
---
576+
duration_ms: *
577+
failureType: 'testTimeoutFailure'
578+
error: 'test timed out after 5ms'
579+
code: 'ERR_TEST_FAILURE'
580+
...
581+
# Subtest: large timeout async test is ok
582+
ok 59 - large timeout async test is ok
583+
---
584+
duration_ms: *
585+
...
586+
# Subtest: large timeout callback test is ok
587+
ok 60 - large timeout callback test is ok
588+
---
589+
duration_ms: *
590+
...
565591
# Subtest: invalid subtest fail
566-
not ok 57 - invalid subtest fail
592+
not ok 61 - invalid subtest fail
567593
---
568594
duration_ms: *
569595
failureType: 'parentAlreadyFinished'
@@ -572,16 +598,16 @@ not ok 57 - invalid subtest fail
572598
stack: |-
573599
*
574600
...
575-
1..57
601+
1..61
576602
# 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.
577603
# 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.
578604
# 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.
579605
# 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.
580606
# 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.
581607
# 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.
582-
# tests 57
583-
# pass 24
584-
# fail 18
608+
# tests 61
609+
# pass 26
610+
# fail 20
585611
# cancelled 0
586612
# skipped 10
587613
# todo 5

0 commit comments

Comments
 (0)