diff --git a/README.md b/README.md index e3a8dda..29c2c00 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ Note the use of `incremental: true`, which speed up compilation massively. * `--only` or `-o`, only run `node:test` with the `only` option set * `--watch` or `-w`, re-run tests on changes * `--timeout` or `-t`, timeouts the tests after a given time; default is 30000 ms +* `--no-timeout`, disables the timeout * `--coverage-exclude` or `-X`, a list of comma-separated patterns to exclude from the coverage report. All tests files are ignored by default. * `--ignore` or `-i`, ignore a glob pattern, and not look for tests there * `--expose-gc`, exposes the gc() function to tests diff --git a/borp.js b/borp.js index 6bfa37a..ea15911 100755 --- a/borp.js +++ b/borp.js @@ -29,6 +29,7 @@ const args = parseArgs({ concurrency: { type: 'string', short: 'c', default: os.availableParallelism() - 1 + '' }, coverage: { type: 'boolean', short: 'C' }, timeout: { type: 'string', short: 't', default: '30000' }, + 'no-timeout': { type: 'boolean' }, 'coverage-exclude': { type: 'string', short: 'X', multiple: true }, ignore: { type: 'string', short: 'i', multiple: true }, 'expose-gc': { type: 'boolean' }, @@ -74,6 +75,10 @@ if (args.values.concurrency) { args.values.concurrency = parseInt(args.values.concurrency) } +if (args.values['no-timeout']) { + delete args.values.timeout +} + if (args.values.timeout) { args.values.timeout = parseInt(args.values.timeout) } diff --git a/fixtures/long/test/long.test.js b/fixtures/long/test/long.test.js new file mode 100644 index 0000000..c6735c5 --- /dev/null +++ b/fixtures/long/test/long.test.js @@ -0,0 +1,10 @@ +import { ok } from 'node:assert' +import { test } from 'node:test' + +test('this will take a long time', (t, done) => { + setTimeout(() => { + ok(true) + done() + }, 1e3) + console.log('test:waiting') +}) diff --git a/package.json b/package.json index 0187825..2d3d50b 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "scripts": { "clean": "rm -rf fixtures/*/dist .test-*", "lint": "standard | snazzy", - "unit": "node borp.js --ignore \"fixtures/**/*\" --coverage --coverage-exclude \"fixtures/**/*\" --coverage-exclude \"test/**/*\"", + "unit": "node borp.js --ignore \"fixtures/**/*\" --coverage --coverage-exclude \"fixtures/**/*\" --coverage-exclude \"test*/**/*\"", "test": "npm run clean ; npm run lint && npm run unit" }, "keywords": [], @@ -23,6 +23,7 @@ "devDependencies": { "@matteo.collina/tspl": "^0.1.0", "@reporters/silent": "^1.2.4", + "@sinonjs/fake-timers": "^11.2.2", "@types/node": "^20.10.0", "desm": "^1.3.0", "snazzy": "^9.0.0", diff --git a/test-utils/clock.js b/test-utils/clock.js new file mode 100644 index 0000000..19da757 --- /dev/null +++ b/test-utils/clock.js @@ -0,0 +1,19 @@ +import FakeTimers from '@sinonjs/fake-timers' + +let clock + +if (process.argv[1].endsWith('borp.js')) { + clock = FakeTimers.install({ + node: Date.now(), + shouldAdvanceTime: true, + advanceTimeDelta: 100 + }) + process.on('message', listener) +} + +function listener ([fn, ...args]) { + clock[fn](...args) + if (fn === 'uninstall') { + process.off('message', listener) + } +} diff --git a/test/timeout.test.js b/test/timeout.test.js new file mode 100644 index 0000000..f954205 --- /dev/null +++ b/test/timeout.test.js @@ -0,0 +1,69 @@ +import { test } from 'node:test' +import { once } from 'node:events' +import { pathToFileURL } from 'node:url' +import { fork } from 'node:child_process' +import { tspl } from '@matteo.collina/tspl' +import { join } from 'desm' + +const borp = join(import.meta.url, '..', 'borp.js') +const clock = join(import.meta.url, '..', 'test-utils', 'clock.js') +const forkOpts = { + cwd: join(import.meta.url, '..', 'fixtures', 'long'), + env: { NODE_OPTIONS: `--import=${pathToFileURL(clock)}` }, + stdio: ['pipe', 'pipe', 'pipe', 'ipc'] +} + +test('times out after 30s by default', async (t) => { + const { ok, equal } = tspl(t, { plan: 4 }) + const borpProcess = fork(borp, forkOpts) + let stdout = '' + borpProcess.stdout.on('data', (data) => { + stdout += data + if (data.includes('test:waiting')) { + borpProcess.send(['tick', 30e3]) + borpProcess.send(['uninstall']) + } + }) + const [code] = await once(borpProcess, 'exit') + equal(code, 1) + ok(stdout.includes('test timed out after 30000ms')) + ok(stdout.includes('tests 1')) + ok(stdout.includes('cancelled 1')) +}) + +test('does not timeout when setting --no-timeout', async (t) => { + const { ok, equal } = tspl(t, { plan: 4 }) + const borpProcess = fork(borp, ['--no-timeout'], forkOpts) + borpProcess.stderr.pipe(process.stderr) + let stdout = '' + borpProcess.stdout.on('data', (data) => { + stdout += data + if (data.includes('test:waiting')) { + borpProcess.send(['tick', 30e3]) + borpProcess.send(['uninstall']) + } + }) + const [code] = await once(borpProcess, 'exit') + equal(code, 0) + ok(stdout.includes('✔ this will take a long time')) + ok(stdout.includes('tests 1')) + ok(stdout.includes('pass 1')) +}) + +test('timeout is configurable', async (t) => { + const { ok, equal } = tspl(t, { plan: 4 }) + const borpProcess = fork(borp, ['--timeout', '10000'], forkOpts) + let stdout = '' + borpProcess.stdout.on('data', (data) => { + stdout += data + if (data.includes('test:waiting')) { + borpProcess.send(['tick', 10e3]) + borpProcess.send(['uninstall']) + } + }) + const [code] = await once(borpProcess, 'exit') + equal(code, 1) + ok(stdout.includes('test timed out after 10000ms')) + ok(stdout.includes('tests 1')) + ok(stdout.includes('cancelled 1')) +})