diff --git a/lib/api.js b/lib/api.js index d82d2937c..03707eaf8 100644 --- a/lib/api.js +++ b/lib/api.js @@ -73,10 +73,16 @@ export default class Api extends Emittery { const pendingWorkers = new Set(); const timedOutWorkerFiles = new Set(); let restartTimer; + let ignoreTimeoutsUntil = 0; if (apiOptions.timeout && !apiOptions.debug) { const timeout = ms(apiOptions.timeout); restartTimer = debounce(() => { + if (Date.now() < ignoreTimeoutsUntil) { + restartTimer(); + return; + } + // If failFast is active, prevent new test files from running after // the current ones are exited. if (failFast) { @@ -233,6 +239,11 @@ export default class Api extends Emittery { } const worker = fork(file, options, apiOptions.nodeArguments); + worker.onStateChange(data => { + if (data.type === 'test-timeout-configured' && !apiOptions.debug) { + ignoreTimeoutsUntil = Math.max(ignoreTimeoutsUntil, Date.now() + data.period); + } + }); runStatus.observeWorker(worker, file, {selectingLines: lineNumbers.length > 0}); deregisteredSharedWorkers.push(observeWorkerProcess(worker, runStatus)); diff --git a/lib/runner.js b/lib/runner.js index 69af03f3a..ae375a277 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -66,6 +66,13 @@ export default class Runner extends Emittery { return true; }; + this.notifyTimeoutUpdate = timeoutMs => { + this.emit('stateChange', { + type: 'test-timeout-configured', + period: timeoutMs + }); + }; + let hasStarted = false; let scheduledStart = false; const meta = Object.freeze({ @@ -290,7 +297,8 @@ export default class Runner extends Emittery { powerAssert: this.powerAssert, title: `${task.title}${titleSuffix || ''}`, isHook: true, - testPassed + testPassed, + notifyTimeoutUpdate: this.notifyTimeoutUpdate })); const outcome = await this.runMultiple(hooks, this.serial); for (const result of outcome.storedResults) { @@ -341,7 +349,8 @@ export default class Runner extends Emittery { metadata: task.metadata, powerAssert: this.powerAssert, title: task.title, - registerUniqueTitle: this.registerUniqueTitle + registerUniqueTitle: this.registerUniqueTitle, + notifyTimeoutUpdate: this.notifyTimeoutUpdate }); const result = await this.runSingle(test); diff --git a/lib/test.js b/lib/test.js index f7fae4536..14d96475f 100644 --- a/lib/test.js +++ b/lib/test.js @@ -208,6 +208,7 @@ export default class Test { this.registerUniqueTitle = options.registerUniqueTitle; this.logs = []; this.teardowns = []; + this.notifyTimeoutUpdate = options.notifyTimeoutUpdate; const {snapshotBelongsTo = this.title, nextSnapshotIndex = 0} = options; this.snapshotBelongsTo = snapshotBelongsTo; @@ -431,6 +432,8 @@ export default class Test { this.finishDueToTimeout(); } }, ms); + + this.notifyTimeoutUpdate(this.timeoutMs); } refreshTimeout() { diff --git a/test-tap/api.js b/test-tap/api.js index 2d4137faa..afcf36b54 100644 --- a/test-tap/api.js +++ b/test-tap/api.js @@ -291,7 +291,8 @@ for (const opt of opts) { return api.run({files: [ path.join(__dirname, 'fixture/fail-fast/timeout/fails.cjs'), - path.join(__dirname, 'fixture/fail-fast/timeout/passes.cjs') + path.join(__dirname, 'fixture/fail-fast/timeout/passes.cjs'), + path.join(__dirname, 'fixture/fail-fast/timeout/passes-slow.cjs') ]}) .then(runStatus => { t.ok(api.options.failFast); diff --git a/test-tap/fixture/fail-fast/timeout/passes-slow.cjs b/test-tap/fixture/fail-fast/timeout/passes-slow.cjs new file mode 100644 index 000000000..e8eadb0c5 --- /dev/null +++ b/test-tap/fixture/fail-fast/timeout/passes-slow.cjs @@ -0,0 +1,9 @@ +const delay = require('delay'); + +const test = require('../../../../entrypoints/main.cjs'); + +test('slow pass with timeout', async t => { + t.timeout(120); + await delay(110); + t.pass(); +}); diff --git a/test-tap/helper/ava-test.js b/test-tap/helper/ava-test.js index 3e3ef13c1..dc4ac17df 100644 --- a/test-tap/helper/ava-test.js +++ b/test-tap/helper/ava-test.js @@ -20,7 +20,8 @@ export function withExperiments(experiments = {}) { fn, registerUniqueTitle, metadata: {type: 'test'}, - title + title, + notifyTimeoutUpdate() {} }); } @@ -32,7 +33,8 @@ export function withExperiments(experiments = {}) { fn, registerUniqueTitle, metadata: {type: 'test', failing: true}, - title: 'test.failing' + title: 'test.failing', + notifyTimeoutUpdate() {} }); }; diff --git a/test-tap/runner.js b/test-tap/runner.js index 91a85f826..f249158be 100644 --- a/test-tap/runner.js +++ b/test-tap/runner.js @@ -63,6 +63,21 @@ test('runner emits "stateChange" events', t => { }); }); +test('notifyTimeoutUpdate emits "stateChange" event', t => { + const runner = new Runner(); + + runner.on('stateChange', evt => { + if (evt.type === 'test-timeout-configured') { + t.same(evt, { + type: 'test-timeout-configured', + period: 120 + }); + t.end(); + } + }); + runner.notifyTimeoutUpdate(120); +}); + test('run serial tests before concurrent ones', t => { const array = []; return promiseEnd(new Runner(), runner => {