diff --git a/api.js b/api.js index 5a9c2b128..52feef3fb 100644 --- a/api.js +++ b/api.js @@ -5,12 +5,10 @@ const fs = require('fs'); const os = require('os'); const commonPathPrefix = require('common-path-prefix'); const uniqueTempDir = require('unique-temp-dir'); -const findCacheDir = require('find-cache-dir'); const isCi = require('is-ci'); const resolveCwd = require('resolve-cwd'); const debounce = require('lodash.debounce'); -const autoBind = require('auto-bind'); -const Promise = require('bluebird'); +const Bluebird = require('bluebird'); const getPort = require('get-port'); const arrify = require('arrify'); const ms = require('ms'); @@ -33,167 +31,214 @@ function resolveModules(modules) { }); } -function getBlankResults() { - return { - stats: { - knownFailureCount: 0, - testCount: 0, - passCount: 0, - skipCount: 0, - todoCount: 0, - failCount: 0 - }, - tests: [] - }; -} - class Api extends EventEmitter { constructor(options) { super(); - autoBind(this); this.options = Object.assign({match: []}, options); this.options.require = resolveModules(this.options.require); } - _runFile(file, runStatus, execArgv) { - const precompiled = {}; - if (this.precompiler) { - Object.assign(precompiled, this._precompiledHelpers); - const hash = this.precompiler.precompileFile(file); - const resolvedfpath = fs.realpathSync(file); - precompiled[resolvedfpath] = hash; - } - - const options = Object.assign({}, this.options, {precompiled}); - if (runStatus.updateSnapshots) { - // Don't use in Object.assign() since it'll override options.updateSnapshots even when false. - options.updateSnapshots = true; - } - const emitter = fork(file, options, execArgv); - runStatus.observeFork(emitter); + run(files, runtimeOptions) { + const apiOptions = this.options; + runtimeOptions = runtimeOptions || {}; - return emitter; - } + // Each run will have its own status. It can only be created when test files + // have been found. + let runStatus; - run(files, options) { - return new AvaFiles({cwd: this.options.resolveTestsFrom, files}) - .findTestFiles() - .then(files => this._run(files, options)); - } + // Irrespectively, perform some setup now, before finding test files. + const handleError = exception => { + runStatus.handleExceptions({ + exception, + file: exception.file ? path.relative(process.cwd(), exception.file) : undefined + }); + }; + + // Track active forks and manage timeouts. + const failFast = apiOptions.failFast === true; + let bailed = false; + const pendingForks = new Set(); + let restartTimer; + if (apiOptions.timeout) { + const timeout = ms(apiOptions.timeout); + + restartTimer = debounce(() => { + // If failFast is active, prevent new test files from running after + // the current ones are exited. + if (failFast) { + bailed = true; + } - _onTimeout(runStatus) { - const timeout = ms(this.options.timeout); - const err = new AvaError(`Exited because no new tests completed within the last ${timeout}ms of inactivity`); - this._handleError(runStatus, err); - runStatus.emit('timeout'); - } + for (const fork of pendingForks) { + fork.exit(); + } - _setupTimeout(runStatus) { - const timeout = ms(this.options.timeout); + handleError(new AvaError(`Exited because no new tests completed within the last ${timeout}ms of inactivity`)); + }, timeout); + } else { + restartTimer = Object.assign(() => {}, {cancel() {}}); + } - runStatus._restartTimer = debounce(() => { - this._onTimeout(runStatus); - }, timeout); + // Find all test files. + return new AvaFiles({cwd: apiOptions.resolveTestsFrom, files}).findTestFiles() + .then(files => { + runStatus = new RunStatus({ + runOnlyExclusive: runtimeOptions.runOnlyExclusive, + prefixTitles: apiOptions.explicitTitles || files.length > 1, + base: path.relative(process.cwd(), commonPathPrefix(files)) + path.sep, + failFast, + fileCount: files.length, + updateSnapshots: runtimeOptions.updateSnapshots + }); - runStatus._restartTimer(); - runStatus.on('test', runStatus._restartTimer); - } + runStatus.on('test', restartTimer); + if (failFast) { + // Prevent new test files from running once a test has failed. + runStatus.on('test', test => { + if (test.error) { + bailed = true; + } + + for (const fork of pendingForks) { + fork.notifyOfPeerFailure(); + } + }); + } - _cancelTimeout(runStatus) { - runStatus._restartTimer.cancel(); - } + this.emit('test-run', runStatus, files); - _setupPrecompiler(files) { - const isCacheEnabled = this.options.cacheEnabled !== false; - let cacheDir = uniqueTempDir(); + // Bail out early if no files were found. + if (files.length === 0) { + handleError(new AvaError('Couldn\'t find any files to test')); + return runStatus; + } - if (isCacheEnabled) { - const foundDir = findCacheDir({ - name: 'ava', - files + // Set up a fresh precompiler for each test run. + return this._setupPrecompiler() + .then(precompilation => { + if (!precompilation) { + return null; + } + + // Compile all test and helper files. Assumes the tests only load + // helpers from within the `resolveTestsFrom` directory. Without + // arguments this is the `projectDir`, else it's `process.cwd()` + // which may be nested too deeply. + return new AvaFiles({cwd: this.options.resolveTestsFrom}).findTestHelpers().then(helpers => { + return { + cacheDir: precompilation.cacheDir, + map: files.concat(helpers).reduce((acc, file) => { + try { + const realpath = fs.realpathSync(file); + const hash = precompilation.precompiler.precompileFile(realpath); + acc[realpath] = hash; + } catch (err) { + throw Object.assign(err, {file}); + } + return acc; + }, {}) + }; + }); + }) + .then(precompilation => { + // Resolve the correct concurrency value. + let concurrency = Math.min(os.cpus().length, isCi ? 2 : Infinity); + if (apiOptions.concurrency > 0) { + concurrency = apiOptions.concurrency; + } + if (apiOptions.serial) { + concurrency = 1; + } + + // Try and run each file, limited by `concurrency`. + return Bluebird.map(files, file => { + // No new files should be run once a test has timed out or failed, + // and failFast is enabled. + if (bailed) { + return null; + } + + let forked; + return Bluebird.resolve( + this._computeForkExecArgv().then(execArgv => { + const options = Object.assign({}, apiOptions, { + // If we're looking for matches, run every single test process in exclusive-only mode + runOnlyExclusive: apiOptions.match.length > 0 || runtimeOptions.runOnlyExclusive === true + }); + if (precompilation) { + options.cacheDir = precompilation.cacheDir; + options.precompiled = precompilation.map; + } else { + options.precompiled = {}; + } + if (runtimeOptions.updateSnapshots) { + // Don't use in Object.assign() since it'll override options.updateSnapshots even when false. + options.updateSnapshots = true; + } + + forked = fork(file, options, execArgv); + pendingForks.add(forked); + runStatus.observeFork(forked); + restartTimer(); + return forked; + }).catch(err => { + // Prevent new test files from running. + if (failFast) { + bailed = true; + } + handleError(Object.assign(err, {file})); + return null; + }) + ).finally(() => { + pendingForks.delete(forked); + }); + }, {concurrency}); + }) + .catch(err => { + handleError(err); + return []; + }) + .then(results => { + restartTimer.cancel(); + + // Filter out undefined results (e.g. for files that were skipped after a timeout) + results = results.filter(Boolean); + if (apiOptions.match.length > 0 && !runStatus.hasExclusive) { + handleError(new AvaError('Couldn\'t find any matching tests')); + } + + runStatus.processResults(results); + return runStatus; + }); }); - if (foundDir !== null) { - cacheDir = foundDir; - } - } + } - this.options.cacheDir = cacheDir; + _setupPrecompiler() { + const cacheDir = this.options.cacheEnabled === false ? + uniqueTempDir() : + path.join(this.options.projectDir, 'node_modules', '.cache', 'ava'); const compileEnhancements = this.options.compileEnhancements !== false; return babelConfigHelper.build(this.options.projectDir, cacheDir, this.options.babelConfig, compileEnhancements) .then(result => { - if (result) { - this.precompiler = new CachingPrecompiler({ + return result ? { + cacheDir, + precompiler: new CachingPrecompiler({ path: cacheDir, getBabelOptions: result.getOptions, babelCacheKeys: result.cacheKeys - }); - } + }) + } : null; }); } - _precompileHelpers() { - this._precompiledHelpers = {}; - if (!this.precompiler) { - return Promise.resolve(); - } - - // Assumes the tests only load helpers from within the `resolveTestsFrom` - // directory. Without arguments this is the `projectDir`, else it's - // `process.cwd()` which may be nested too deeply. This will be solved - // as we implement RFC 001 and move helper compilation into the worker - // processes, avoiding the need for precompilation. - return new AvaFiles({cwd: this.options.resolveTestsFrom}) - .findTestHelpers() - .each(file => { // eslint-disable-line array-callback-return - const hash = this.precompiler.precompileFile(file); - this._precompiledHelpers[file] = hash; - }); - } - - _run(files, options) { - options = options || {}; - - const runStatus = new RunStatus({ - runOnlyExclusive: options.runOnlyExclusive, - prefixTitles: this.options.explicitTitles || files.length > 1, - base: path.relative(process.cwd(), commonPathPrefix(files)) + path.sep, - failFast: this.options.failFast, - updateSnapshots: options.updateSnapshots - }); - - this.emit('test-run', runStatus, files); - - if (files.length === 0) { - const err = new AvaError('Couldn\'t find any files to test'); - this._handleError(runStatus, err); - return Promise.resolve(runStatus); + _computeForkExecArgv() { + const execArgv = this.options.testOnlyExecArgv || process.execArgv; + if (execArgv.length === 0) { + return Promise.resolve(execArgv); } - return this._setupPrecompiler(files) - .then(() => this._precompileHelpers()) - .then(() => { - if (this.options.timeout) { - this._setupTimeout(runStatus); - } - - let concurrency = Math.min(os.cpus().length, isCi ? 2 : Infinity); - - if (this.options.concurrency > 0) { - concurrency = this.options.concurrency; - } - - if (this.options.serial) { - concurrency = 1; - } - - return this._runWithPool(files, runStatus, concurrency); - }); - } - - _computeForkExecArgs(files) { - const execArgv = this.options.testOnlyExecArgv || process.execArgv; let debugArgIndex = -1; // --inspect-brk is used in addition to --inspect to break on first line and wait @@ -219,85 +264,21 @@ class Api extends EventEmitter { } if (debugArgIndex === -1) { - return Promise.resolve([]); + return Promise.resolve(execArgv); } - return Promise - .map(files, () => getPort()) - .map(port => { - const forkExecArgv = execArgv.slice(); - let flagName = isInspect ? '--inspect' : '--debug'; - const oldValue = forkExecArgv[debugArgIndex]; - if (oldValue.indexOf('brk') > 0) { - flagName += '-brk'; - } - - forkExecArgv[debugArgIndex] = `${flagName}=${port}`; - - return forkExecArgv; - }); - } - - _handleError(runStatus, err) { - runStatus.handleExceptions({ - exception: err, - file: err.file ? path.relative(process.cwd(), err.file) : undefined - }); - } + return getPort().then(port => { + const forkExecArgv = execArgv.slice(); + let flagName = isInspect ? '--inspect' : '--debug'; + const oldValue = forkExecArgv[debugArgIndex]; + if (oldValue.indexOf('brk') > 0) { + flagName += '-brk'; + } - _runWithPool(files, runStatus, concurrency) { - const tests = []; - let execArgvList; + forkExecArgv[debugArgIndex] = `${flagName}=${port}`; - runStatus.on('timeout', () => { - tests.forEach(fork => { - fork.exit(); - }); + return forkExecArgv; }); - - return this._computeForkExecArgs(files) - .then(argvList => { - execArgvList = argvList; - }) - .return(files) - .map((file, index) => { - return new Promise(resolve => { - const forkArgs = execArgvList[index]; - const test = this._runFile(file, runStatus, forkArgs); - tests.push(test); - - // If we're looking for matches, run every single test process in exclusive-only mode - const options = { - runOnlyExclusive: this.options.match.length > 0 - }; - - resolve(test.run(options)); - }).catch(err => { - err.file = file; - this._handleError(runStatus, err); - return getBlankResults(); - }); - }, {concurrency}) - .then(results => { - // Filter out undefined results (usually result of caught exceptions) - results = results.filter(Boolean); - - // Cancel debounced _onTimeout() from firing - if (this.options.timeout) { - this._cancelTimeout(runStatus); - } - - if (this.options.match.length > 0 && !runStatus.hasExclusive) { - results = []; - - const err = new AvaError('Couldn\'t find any matching tests'); - this._handleError(runStatus, err); - } - - runStatus.processResults(results); - - return runStatus; - }); } } diff --git a/index.d.ts b/index.d.ts index a9580e251..72c4a9e1b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -168,6 +168,10 @@ export interface SerialInterface { (title: string, macro: Macro | Macro[], ...args: Array): void; (macro: Macro | Macro[], ...args: Array): void; + after: AfterInterface; + afterEach: AfterInterface; + before: BeforeInterface; + beforeEach: BeforeInterface; cb: CbInterface; failing: FailingInterface; only: OnlyInterface; diff --git a/index.js.flow b/index.js.flow index 42a578e6e..6c460f9c6 100644 --- a/index.js.flow +++ b/index.js.flow @@ -171,6 +171,10 @@ export interface SerialInterface { (title: string, macro: Macro | Macro[], ...args: Array): void; (macro: Macro | Macro[], ...args: Array): void; + after: AfterInterface; + afterEach: AfterInterface; + before: BeforeInterface; + beforeEach: BeforeInterface; cb: CbInterface; failing: FailingInterface; only: OnlyInterface; diff --git a/lib/concordance-options.js b/lib/concordance-options.js index 18b4b0c77..113d07335 100644 --- a/lib/concordance-options.js +++ b/lib/concordance-options.js @@ -4,7 +4,7 @@ const chalk = require('chalk'); const stripAnsi = require('strip-ansi'); const cloneDeepWith = require('lodash.clonedeepwith'); const reactPlugin = require('@concordance/react'); -const options = require('./globals').options; +const options = require('./worker-options').get(); // Wrap Concordance's React plugin. Change the name to avoid collisions if in // the future users can register plugins themselves. diff --git a/lib/concurrent.js b/lib/concurrent.js deleted file mode 100644 index 3cdbb41c3..000000000 --- a/lib/concurrent.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -class Concurrent { - constructor(runnables, bail) { - if (!Array.isArray(runnables)) { - throw new TypeError('Expected an array of runnables'); - } - - this.runnables = runnables; - this.bail = bail || false; - } - - run() { - let allPassed = true; - - let pending; - let rejectPending; - let resolvePending; - const allPromises = []; - const handlePromise = promise => { - if (!pending) { - pending = new Promise((resolve, reject) => { - rejectPending = reject; - resolvePending = resolve; - }); - } - - allPromises.push(promise.then(passed => { - if (!passed) { - allPassed = false; - - if (this.bail) { - // Stop if the test failed and bail mode is on. - resolvePending(); - } - } - }, rejectPending)); - }; - - for (const runnable of this.runnables) { - const passedOrPromise = runnable.run(); - - if (!passedOrPromise) { - if (this.bail) { - // Stop if the test failed and bail mode is on. - return false; - } - - allPassed = false; - } else if (passedOrPromise !== true) { - handlePromise(passedOrPromise); - } - } - - if (pending) { - Promise.all(allPromises).then(resolvePending); - return pending.then(() => allPassed); - } - - return allPassed; - } -} - -module.exports = Concurrent; diff --git a/lib/context-ref.js b/lib/context-ref.js new file mode 100644 index 000000000..5529bfc3d --- /dev/null +++ b/lib/context-ref.js @@ -0,0 +1,41 @@ +'use strict'; +const clone = require('lodash.clone'); + +class ContextRef { + constructor() { + this.value = {}; + } + + get() { + return this.value; + } + + set(newValue) { + this.value = newValue; + } + + copy() { + return new LateBinding(this); // eslint-disable-line no-use-before-define + } +} +module.exports = ContextRef; + +class LateBinding extends ContextRef { + constructor(ref) { + super(); + this.ref = ref; + this.bound = false; + } + + get() { + if (!this.bound) { + this.set(clone(this.ref.get())); + } + return super.get(); + } + + set(newValue) { + this.bound = true; + super.set(newValue); + } +} diff --git a/lib/create-chain.js b/lib/create-chain.js new file mode 100644 index 000000000..c303b9a59 --- /dev/null +++ b/lib/create-chain.js @@ -0,0 +1,108 @@ +'use strict'; +const chainRegistry = new WeakMap(); + +function startChain(name, call, defaults) { + const fn = function () { + call(Object.assign({}, defaults), Array.from(arguments)); + }; + Object.defineProperty(fn, 'name', {value: name}); + chainRegistry.set(fn, {call, defaults, fullName: name}); + return fn; +} + +function extendChain(prev, name, flag) { + if (!flag) { + flag = name; + } + + const fn = function () { + callWithFlag(prev, flag, Array.from(arguments)); + }; + const fullName = `${chainRegistry.get(prev).fullName}.${name}`; + Object.defineProperty(fn, 'name', {value: fullName}); + prev[name] = fn; + + chainRegistry.set(fn, {flag, fullName, prev}); + return fn; +} + +function callWithFlag(prev, flag, args) { + const combinedFlags = {[flag]: true}; + do { + const step = chainRegistry.get(prev); + if (step.call) { + step.call(Object.assign({}, step.defaults, combinedFlags), args); + prev = null; + } else { + combinedFlags[step.flag] = true; + prev = step.prev; + } + } while (prev); +} + +function createHookChain(hook, isAfterHook) { + // Hook chaining rules: + // * `always` comes immediately after "after hooks" + // * `skip` must come at the end + // * no `only` + // * no repeating + extendChain(hook, 'cb', 'callback'); + extendChain(hook, 'skip', 'skipped'); + extendChain(hook.cb, 'skip', 'skipped'); + if (isAfterHook) { + extendChain(hook, 'always'); + extendChain(hook.always, 'cb', 'callback'); + extendChain(hook.always, 'skip', 'skipped'); + extendChain(hook.always.cb, 'skip', 'skipped'); + } + return hook; +} + +function createChain(fn, defaults) { + // Test chaining rules: + // * `serial` must come at the start + // * `only` and `skip` must come at the end + // * `failing` must come at the end, but can be followed by `only` and `skip` + // * `only` and `skip` cannot be chained together + // * no repeating + const root = startChain('test', fn, Object.assign({}, defaults, {type: 'test'})); + extendChain(root, 'cb', 'callback'); + extendChain(root, 'failing'); + extendChain(root, 'only', 'exclusive'); + extendChain(root, 'serial'); + extendChain(root, 'skip', 'skipped'); + extendChain(root.cb, 'failing'); + extendChain(root.cb, 'only', 'exclusive'); + extendChain(root.cb, 'skip', 'skipped'); + extendChain(root.cb.failing, 'only', 'exclusive'); + extendChain(root.cb.failing, 'skip', 'skipped'); + extendChain(root.failing, 'only', 'exclusive'); + extendChain(root.failing, 'skip', 'skipped'); + extendChain(root.serial, 'cb', 'callback'); + extendChain(root.serial, 'failing'); + extendChain(root.serial, 'only', 'exclusive'); + extendChain(root.serial, 'skip', 'skipped'); + extendChain(root.serial.cb, 'failing'); + extendChain(root.serial.cb, 'only', 'exclusive'); + extendChain(root.serial.cb, 'skip', 'skipped'); + extendChain(root.serial.cb.failing, 'only', 'exclusive'); + extendChain(root.serial.cb.failing, 'skip', 'skipped'); + + root.after = createHookChain(startChain('test.after', fn, Object.assign({}, defaults, {type: 'after'})), true); + root.afterEach = createHookChain(startChain('test.afterEach', fn, Object.assign({}, defaults, {type: 'afterEach'})), true); + root.before = createHookChain(startChain('test.before', fn, Object.assign({}, defaults, {type: 'before'})), false); + root.beforeEach = createHookChain(startChain('test.beforeEach', fn, Object.assign({}, defaults, {type: 'beforeEach'})), false); + + root.serial.after = createHookChain(startChain('test.after', fn, Object.assign({}, defaults, {serial: true, type: 'after'})), true); + root.serial.afterEach = createHookChain(startChain('test.afterEach', fn, Object.assign({}, defaults, {serial: true, type: 'afterEach'})), true); + root.serial.before = createHookChain(startChain('test.before', fn, Object.assign({}, defaults, {serial: true, type: 'before'})), false); + root.serial.beforeEach = createHookChain(startChain('test.beforeEach', fn, Object.assign({}, defaults, {serial: true, type: 'beforeEach'})), false); + + // "todo" tests cannot be chained. Allow todo tests to be flagged as needing + // to be serial. + root.todo = startChain('test.todo', fn, Object.assign({}, defaults, {type: 'test', todo: true})); + root.serial.todo = startChain('test.serial.todo', fn, Object.assign({}, defaults, {serial: true, type: 'test', todo: true})); + + return root; +} +module.exports = createChain; diff --git a/lib/fork.js b/lib/fork.js index 0ca0f45a4..7a7d4f73a 100644 --- a/lib/fork.js +++ b/lib/fork.js @@ -59,6 +59,7 @@ module.exports = (file, opts, execArgv) => { } }; + let loadedFile = false; const testResults = []; let results; @@ -100,21 +101,20 @@ module.exports = (file, opts, execArgv) => { if (results) { resolve(results); + } else if (loadedFile) { + reject(new AvaError(`No tests found in ${relFile}`)); } else { reject(new AvaError(`Test results were not received from ${relFile}`)); } }); - ps.on('no-tests', data => { - send('teardown'); - - let message = `No tests found in ${relFile}`; + ps.on('loaded-file', data => { + loadedFile = true; if (!data.avaRequired) { - message += ', make sure to import "ava" at the top of your test file'; + send('teardown'); + reject(new AvaError(`No tests found in ${relFile}, make sure to import "ava" at the top of your test file`)); } - - reject(new AvaError(message)); }); }); @@ -142,34 +142,13 @@ module.exports = (file, opts, execArgv) => { return promise; }; - promise.send = (name, data) => { - send(name, data); - return promise; - }; - promise.exit = () => { send('init-exit'); return promise; }; - // Send 'run' event only when fork is listening for it - let isReady = false; - - ps.on('stats', () => { - isReady = true; - }); - - promise.run = options => { - if (isReady) { - send('run', options); - return promise; - } - - ps.on('stats', () => { - send('run', options); - }); - - return promise; + promise.notifyOfPeerFailure = () => { + send('peer-failed'); }; return promise; diff --git a/lib/globals.js b/lib/globals.js deleted file mode 100644 index 51176c113..000000000 --- a/lib/globals.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -// Global objects / functions to be bound before requiring test file, so tests do not interfere - -const x = module.exports; -x.now = Date.now; -x.setTimeout = setTimeout; -x.clearTimeout = clearTimeout; -x.setImmediate = setImmediate; -x.options = {}; diff --git a/lib/logger.js b/lib/logger.js index e8edb120f..9256f4979 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -24,7 +24,7 @@ class Logger { } test(test, runStatus) { - this.write(this.reporter.test(test, runStatus), runStatus); + this.write(this.reporter.test(test), runStatus); } unhandledError(err, runStatus) { diff --git a/lib/main.js b/lib/main.js index 3ab8d5a97..0b7c325ab 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,100 +1,22 @@ 'use strict'; const worker = require('./test-worker'); -const adapter = require('./process-adapter'); -const serializeError = require('./serialize-error'); -const globals = require('./globals'); const Runner = require('./runner'); +const opts = require('./worker-options').get(); -const opts = globals.options; const runner = new Runner({ - bail: opts.failFast, + failFast: opts.failFast, failWithoutAssertions: opts.failWithoutAssertions, file: opts.file, match: opts.match, projectDir: opts.projectDir, + runOnlyExclusive: opts.runOnlyExclusive, serial: opts.serial, - updateSnapshots: opts.updateSnapshots, - snapshotDir: opts.snapshotDir + snapshotDir: opts.snapshotDir, + updateSnapshots: opts.updateSnapshots }); worker.setRunner(runner); -// If fail-fast is enabled, use this variable to detect -// that no more tests should be logged -let isFailed = false; - -function test(props) { - if (isFailed) { - return; - } - - const hasError = typeof props.error !== 'undefined'; - - // Don't display anything if it's a passed hook - if (!hasError && props.type !== 'test') { - return; - } - - if (hasError) { - props.error = serializeError(props.error); - } else { - props.error = null; - } - - adapter.send('test', props); - - if (hasError && opts.failFast) { - isFailed = true; - exit(); - } -} - -function exit() { - // Reference the IPC channel now that tests have finished running. - adapter.ipcChannel.ref(); - - const stats = runner.buildStats(); - adapter.send('results', {stats}); -} - -globals.setImmediate(() => { - const hasExclusive = runner.tests.hasExclusive; - const numberOfTests = runner.tests.testCount; - - if (numberOfTests === 0) { - adapter.send('no-tests', {avaRequired: true}); - return; - } - - adapter.send('stats', { - testCount: numberOfTests, - hasExclusive - }); - - runner.on('test', test); - - process.on('ava-run', options => { - // Unreference the IPC channel. This stops it from keeping the event loop - // busy, which means the `beforeExit` event can be used to detect when tests - // stall. - adapter.ipcChannel.unref(); - - runner.run(options) - .then(() => { - runner.saveSnapshotState(); - - return exit(); - }) - .catch(err => { - process.emit('uncaughtException', err); - }); - }); - - process.on('ava-init-exit', () => { - exit(); - }); -}); - const makeCjsExport = () => { function test() { return runner.chain.apply(null, arguments); diff --git a/lib/now-and-timers.js b/lib/now-and-timers.js new file mode 100644 index 000000000..2576d7ef9 --- /dev/null +++ b/lib/now-and-timers.js @@ -0,0 +1,5 @@ +'use strict'; +const timers = require('timers'); + +Object.assign(exports, timers); +exports.now = Date.now; diff --git a/lib/process-adapter.js b/lib/process-adapter.js index fe733eb72..e44105950 100644 --- a/lib/process-adapter.js +++ b/lib/process-adapter.js @@ -24,10 +24,20 @@ exports.send = (name, data) => { // `process.channel` was added in Node.js 7.1.0, but the channel was available // through an undocumented API as `process._channel`. -exports.ipcChannel = process.channel || process._channel; +const ipcChannel = process.channel || process._channel; +let allowUnref = true; +exports.unrefChannel = () => { + if (allowUnref) { + ipcChannel.unref(); + } +}; +exports.forceRefChannel = () => { + allowUnref = false; + ipcChannel.ref(); +}; const opts = JSON.parse(process.argv[2]); -exports.opts = opts; +require('./worker-options').set(opts); // Fake TTY support if (opts.tty) { diff --git a/lib/reporters/mini.js b/lib/reporters/mini.js index a21d02a6b..5dae12ff8 100644 --- a/lib/reporters/mini.js +++ b/lib/reporters/mini.js @@ -236,12 +236,12 @@ class MiniReporter { if (err.type === 'exception' && err.name === 'AvaError') { status += ' ' + colors.error(cross + ' ' + err.message) + '\n\n'; } else { - const title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception'; - status += ' ' + colors.title(title) + '\n'; + const title = err.type === 'rejection' ? 'Unhandled rejection' : 'Uncaught exception'; + status += ' ' + colors.title(`${title} in ${err.file}`) + '\n'; if (err.name) { - status += ' ' + colors.stack(err.summary) + '\n'; - status += colors.errorStack(err.stack) + '\n\n'; + status += indentString(colors.stack(err.summary), 2) + '\n'; + status += indentString(colors.errorStack(err.stack), 2) + '\n\n'; } else { status += ' Threw non-error: ' + err.summary + '\n'; } @@ -249,9 +249,22 @@ class MiniReporter { }); } - if (runStatus.failFastEnabled === true && runStatus.remainingCount > 0 && runStatus.failCount > 0) { - const remaining = 'At least ' + runStatus.remainingCount + ' ' + plur('test was', 'tests were', runStatus.remainingCount) + ' skipped.'; - status += ' ' + colors.information('`--fail-fast` is on. ' + remaining) + '\n\n'; + if (runStatus.failFastEnabled === true && runStatus.failCount > 0 && (runStatus.remainingCount > 0 || runStatus.fileCount > runStatus.observationCount)) { + let remaining = ''; + if (runStatus.remainingCount > 0) { + remaining += `At least ${runStatus.remainingCount} ${plur('test was', 'tests were', runStatus.remainingCount)} skipped`; + if (runStatus.fileCount > runStatus.observationCount) { + remaining += ', as well as '; + } + } + if (runStatus.fileCount > runStatus.observationCount) { + const skippedFileCount = runStatus.fileCount - runStatus.observationCount; + remaining += `${skippedFileCount} ${plur('test file', 'test files', skippedFileCount)}`; + if (runStatus.remainingCount === 0) { + remaining += ` ${plur('was', 'were', skippedFileCount)} skipped`; + } + } + status += ' ' + colors.information('`--fail-fast` is on. ' + remaining + '.') + '\n\n'; } if (runStatus.hasExclusive === true && runStatus.remainingCount > 0) { diff --git a/lib/reporters/verbose.js b/lib/reporters/verbose.js index c58d8db3b..d0083c8fe 100644 --- a/lib/reporters/verbose.js +++ b/lib/reporters/verbose.js @@ -24,7 +24,7 @@ class VerboseReporter { return ''; } - test(test, runStatus) { + test(test) { const lines = []; if (test.error) { lines.push(' ' + colors.error(figures.cross) + ' ' + test.title + ' ' + colors.error(test.error.message)); @@ -34,8 +34,6 @@ class VerboseReporter { lines.push(' ' + colors.skip('- ' + test.title)); } else if (test.failing) { lines.push(' ' + colors.error(figures.tick) + ' ' + colors.error(test.title)); - } else if (runStatus.fileCount === 1 && runStatus.testCount === 1 && test.title === '[anonymous]') { - // No output } else { // Display duration only over a threshold const threshold = 100; @@ -64,18 +62,14 @@ class VerboseReporter { return colors.error(' ' + figures.cross + ' ' + err.message); } - const types = { - rejection: 'Unhandled Rejection', - exception: 'Uncaught Exception' - }; + const title = err.type === 'rejection' ? 'Unhandled rejection' : 'Uncaught exception'; + let output = ' ' + colors.title(`${title} in ${err.file}`) + '\n'; - let output = colors.error(types[err.type] + ':', err.file) + '\n'; - - if (err.stack) { - output += ' ' + colors.stack(err.title || err.summary) + '\n'; - output += ' ' + colors.stack(err.stack) + '\n'; + if (err.name) { + output += indentString(colors.stack(err.summary), 2) + '\n'; + output += indentString(colors.errorStack(err.stack), 2) + '\n\n'; } else { - output += ' ' + colors.stack(JSON.stringify(err)) + '\n'; + output += ' Threw non-error: ' + err.summary + '\n'; } output += '\n'; @@ -172,9 +166,22 @@ class VerboseReporter { }); } - if (runStatus.failFastEnabled === true && runStatus.remainingCount > 0 && runStatus.failCount > 0) { - const remaining = 'At least ' + runStatus.remainingCount + ' ' + plur('test was', 'tests were', runStatus.remainingCount) + ' skipped.'; - output += ' ' + colors.information('`--fail-fast` is on. ' + remaining) + '\n\n'; + if (runStatus.failFastEnabled === true && runStatus.failCount > 0 && (runStatus.remainingCount > 0 || runStatus.fileCount > runStatus.observationCount)) { + let remaining = ''; + if (runStatus.remainingCount > 0) { + remaining += `At least ${runStatus.remainingCount} ${plur('test was', 'tests were', runStatus.remainingCount)} skipped`; + if (runStatus.fileCount > runStatus.observationCount) { + remaining += ', as well as '; + } + } + if (runStatus.fileCount > runStatus.observationCount) { + const skippedFileCount = runStatus.fileCount - runStatus.observationCount; + remaining += `${skippedFileCount} ${plur('test file', 'test files', skippedFileCount)}`; + if (runStatus.remainingCount === 0) { + remaining += ` ${plur('was', 'were', skippedFileCount)} skipped`; + } + } + output += ' ' + colors.information('`--fail-fast` is on. ' + remaining + '.') + '\n\n'; } if (runStatus.hasExclusive === true && runStatus.remainingCount > 0) { diff --git a/lib/run-status.js b/lib/run-status.js index 461ab8f90..aae0367c8 100644 --- a/lib/run-status.js +++ b/lib/run-status.js @@ -31,7 +31,7 @@ class RunStatus extends EventEmitter { this.skipCount = 0; this.todoCount = 0; this.failCount = 0; - this.fileCount = 0; + this.fileCount = opts.fileCount || 0; this.testCount = 0; this.remainingCount = 0; this.previousFailCount = 0; @@ -41,11 +41,13 @@ class RunStatus extends EventEmitter { this.tests = []; this.failFastEnabled = opts.failFast || false; this.updateSnapshots = opts.updateSnapshots || false; + this.observationCount = 0; autoBind(this); } observeFork(emitter) { + this.observationCount++; emitter .on('teardown', this.handleTeardown) .on('stats', this.handleStats) diff --git a/lib/runner.js b/lib/runner.js index d425a0399..bb7817a38 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -1,267 +1,162 @@ 'use strict'; const EventEmitter = require('events'); const path = require('path'); -const Bluebird = require('bluebird'); const matcher = require('matcher'); +const ContextRef = require('./context-ref'); +const createChain = require('./create-chain'); const snapshotManager = require('./snapshot-manager'); -const TestCollection = require('./test-collection'); -const validateTest = require('./validate-test'); - -const chainRegistry = new WeakMap(); - -function startChain(name, call, defaults) { - const fn = function () { - call(Object.assign({}, defaults), Array.from(arguments)); - }; - Object.defineProperty(fn, 'name', {value: name}); - chainRegistry.set(fn, {call, defaults, fullName: name}); - return fn; -} - -function extendChain(prev, name, flag) { - if (!flag) { - flag = name; - } - - const fn = function () { - callWithFlag(prev, flag, Array.from(arguments)); - }; - const fullName = `${chainRegistry.get(prev).fullName}.${name}`; - Object.defineProperty(fn, 'name', {value: fullName}); - prev[name] = fn; - - chainRegistry.set(fn, {flag, fullName, prev}); - return fn; -} - -function callWithFlag(prev, flag, args) { - const combinedFlags = {[flag]: true}; - do { - const step = chainRegistry.get(prev); - if (step.call) { - step.call(Object.assign({}, step.defaults, combinedFlags), args); - prev = null; - } else { - combinedFlags[step.flag] = true; - prev = step.prev; - } - } while (prev); -} - -function createHookChain(hook, isAfterHook) { - // Hook chaining rules: - // * `always` comes immediately after "after hooks" - // * `skip` must come at the end - // * no `only` - // * no repeating - extendChain(hook, 'cb', 'callback'); - extendChain(hook, 'skip', 'skipped'); - extendChain(hook.cb, 'skip', 'skipped'); - if (isAfterHook) { - extendChain(hook, 'always'); - extendChain(hook.always, 'cb', 'callback'); - extendChain(hook.always, 'skip', 'skipped'); - extendChain(hook.always.cb, 'skip', 'skipped'); - } - return hook; -} - -function createChain(fn, defaults) { - // Test chaining rules: - // * `serial` must come at the start - // * `only` and `skip` must come at the end - // * `failing` must come at the end, but can be followed by `only` and `skip` - // * `only` and `skip` cannot be chained together - // * no repeating - const root = startChain('test', fn, Object.assign({}, defaults, {type: 'test'})); - extendChain(root, 'cb', 'callback'); - extendChain(root, 'failing'); - extendChain(root, 'only', 'exclusive'); - extendChain(root, 'serial'); - extendChain(root, 'skip', 'skipped'); - extendChain(root.cb, 'failing'); - extendChain(root.cb, 'only', 'exclusive'); - extendChain(root.cb, 'skip', 'skipped'); - extendChain(root.cb.failing, 'only', 'exclusive'); - extendChain(root.cb.failing, 'skip', 'skipped'); - extendChain(root.failing, 'only', 'exclusive'); - extendChain(root.failing, 'skip', 'skipped'); - extendChain(root.serial, 'cb', 'callback'); - extendChain(root.serial, 'failing'); - extendChain(root.serial, 'only', 'exclusive'); - extendChain(root.serial, 'skip', 'skipped'); - extendChain(root.serial.cb, 'failing'); - extendChain(root.serial.cb, 'only', 'exclusive'); - extendChain(root.serial.cb, 'skip', 'skipped'); - extendChain(root.serial.cb.failing, 'only', 'exclusive'); - extendChain(root.serial.cb.failing, 'skip', 'skipped'); - - root.after = createHookChain(startChain('test.after', fn, Object.assign({}, defaults, {type: 'after'})), true); - root.afterEach = createHookChain(startChain('test.afterEach', fn, Object.assign({}, defaults, {type: 'afterEach'})), true); - root.before = createHookChain(startChain('test.before', fn, Object.assign({}, defaults, {type: 'before'})), false); - root.beforeEach = createHookChain(startChain('test.beforeEach', fn, Object.assign({}, defaults, {type: 'beforeEach'})), false); - - // Todo tests cannot be chained. Allow todo tests to be flagged as needing to - // be serial. - root.todo = startChain('test.todo', fn, Object.assign({}, defaults, {type: 'test', todo: true})); - root.serial.todo = startChain('test.serial.todo', fn, Object.assign({}, defaults, {serial: true, type: 'test', todo: true})); - - return root; -} - -function wrapFunction(fn, args) { - return function (t) { - return fn.apply(this, [t].concat(args)); - }; -} +const Runnable = require('./test'); class Runner extends EventEmitter { constructor(options) { super(); options = options || {}; - + this.failFast = options.failFast === true; + this.failWithoutAssertions = options.failWithoutAssertions !== false; this.file = options.file; this.match = options.match || []; this.projectDir = options.projectDir; - this.serial = options.serial; - this.updateSnapshots = options.updateSnapshots; + this.runOnlyExclusive = options.runOnlyExclusive === true; + this.serial = options.serial === true; this.snapshotDir = options.snapshotDir; + this.updateSnapshots = options.updateSnapshots; - this.hasStarted = false; - this.results = []; + this.activeRunnables = new Set(); + this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this); + this.interrupted = false; this.snapshots = null; - this.tests = new TestCollection({ - bail: options.bail, - failWithoutAssertions: options.failWithoutAssertions, - compareTestSnapshot: this.compareTestSnapshot.bind(this) - }); - - this.chain = createChain((opts, args) => { - let title; - let fn; - let macroArgIndex; - - if (this.hasStarted) { - throw new Error('All tests and hooks must be declared synchronously in your ' + - 'test file, and cannot be nested within other tests or hooks.'); - } - - if (typeof args[0] === 'string') { - title = args[0]; - fn = args[1]; - macroArgIndex = 2; - } else { - fn = args[0]; - title = null; - macroArgIndex = 1; - } - - if (this.serial) { - opts.serial = true; - } + this.stats = { + failCount: 0, + failedHookCount: 0, + hasExclusive: false, + knownFailureCount: 0, + passCount: 0, + skipCount: 0, + testCount: 0, + todoCount: 0 + }; + this.tasks = { + after: [], + afterAlways: [], + afterEach: [], + afterEachAlways: [], + before: [], + beforeEach: [], + concurrent: [], + serial: [], + todo: [] + }; - if (args.length > macroArgIndex) { - args = args.slice(macroArgIndex); - } else { - args = null; + const uniqueTestTitles = new Set(); + let hasStarted = false; + let scheduledStart = false; + this.chain = createChain((metadata, args) => { // eslint-disable-line complexity + if (hasStarted) { + throw new Error('All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.'); } - - if (Array.isArray(fn)) { - fn.forEach(fn => { - this.addTest(title, opts, fn, args); + if (!scheduledStart) { + scheduledStart = true; + process.nextTick(() => { + hasStarted = true; + this.start(); }); - } else { - this.addTest(title, opts, fn, args); } - }, { - serial: false, - exclusive: false, - skipped: false, - todo: false, - failing: false, - callback: false, - always: false - }); - } - addTest(title, metadata, fn, args) { - if (args) { - if (fn.title) { - title = fn.title.apply(fn, [title || ''].concat(args)); - } + const specifiedTitle = typeof args[0] === 'string' ? + args.shift() : + ''; + const implementations = Array.isArray(args[0]) ? + args.shift() : + args.splice(0, 1); - fn = wrapFunction(fn, args); - } + if (metadata.todo) { + if (implementations.length > 0) { + throw new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.'); + } - if (metadata.type === 'test' && this.match.length > 0) { - metadata.exclusive = matcher([title || ''], this.match).length === 1; - } + if (specifiedTitle === '') { + throw new TypeError('`todo` tests require a title'); + } - const validationError = validateTest(title, fn, metadata); - if (validationError !== null) { - throw new TypeError(validationError); - } + if (uniqueTestTitles.has(specifiedTitle)) { + throw new Error(`Duplicate test title: ${specifiedTitle}`); + } else { + uniqueTestTitles.add(specifiedTitle); + } - this.tests.add({ - metadata, - fn, - title - }); - } + if (this.match.length > 0) { + // --match selects TODO tests. + if (matcher([specifiedTitle], this.match).length === 1) { + metadata.exclusive = true; + this.stats.hasExclusive = true; + } + } - addTestResult(result) { - const test = result.result; - const props = { - logs: test.logs, - duration: test.duration, - title: test.title, - error: result.reason, - type: test.metadata.type, - skip: test.metadata.skipped, - todo: test.metadata.todo, - failing: test.metadata.failing - }; + this.tasks.todo.push({title: specifiedTitle, metadata}); + } else { + if (implementations.length === 0) { + throw new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.'); + } - this.results.push(result); - this.emit('test', props); - } + for (const implementation of implementations) { + let title = implementation.title ? + implementation.title.apply(implementation, [specifiedTitle].concat(args)) : + specifiedTitle; - buildStats() { - const stats = { - failCount: 0, - knownFailureCount: 0, - passCount: 0, - skipCount: 0, - testCount: 0, - todoCount: 0 - }; + if (typeof title !== 'string') { + throw new TypeError('Test & hook titles must be strings'); + } - for (const result of this.results) { - if (!result.passed) { - // Includes hooks - stats.failCount++; - } + if (title === '') { + if (metadata.type === 'test') { + throw new TypeError('Tests must have a title'); + } else if (metadata.always) { + title = `${metadata.type}.always hook`; + } else { + title = `${metadata.type} hook`; + } + } + + if (metadata.type === 'test') { + if (uniqueTestTitles.has(title)) { + throw new Error(`Duplicate test title: ${title}`); + } else { + uniqueTestTitles.add(title); + } + } - const metadata = result.result.metadata; - if (metadata.type === 'test') { - stats.testCount++; - - if (metadata.skipped) { - stats.skipCount++; - } else if (metadata.todo) { - stats.todoCount++; - } else if (result.passed) { - if (metadata.failing) { - stats.knownFailureCount++; - } else { - stats.passCount++; + const task = { + title, + implementation, + args, + metadata: Object.assign({}, metadata) + }; + + if (metadata.type === 'test') { + if (this.match.length > 0) { + // --match overrides .only() + task.metadata.exclusive = matcher([title], this.match).length === 1; + } + if (task.metadata.exclusive) { + this.stats.hasExclusive = true; + } + + this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task); + } else if (!metadata.skipped) { + this.tasks[metadata.type + (metadata.always ? 'Always' : '')].push(task); } } } - } - - return stats; + }, { + serial: false, + exclusive: false, + skipped: false, + todo: false, + failing: false, + callback: false, + always: false + }); } compareTestSnapshot(options) { @@ -294,20 +189,279 @@ class Runner extends EventEmitter { } } - run(options) { - if (options.runOnlyExclusive && !this.tests.hasExclusive) { - return Promise.resolve(null); + onRun(runnable) { + this.activeRunnables.add(runnable); + } + + onRunComplete(runnable) { + this.activeRunnables.delete(runnable); + } + + attributeLeakedError(err) { + for (const runnable of this.activeRunnables) { + if (runnable.attributeLeakedError(err)) { + return true; + } } + return false; + } + + beforeExitHandler() { + for (const runnable of this.activeRunnables) { + runnable.finishDueToInactivity(); + } + } + + runMultiple(runnables) { + let allPassed = true; + const storedResults = []; + const runAndStoreResult = runnable => { + return this.runSingle(runnable).then(result => { + if (!result.passed) { + allPassed = false; + } + storedResults.push(result); + }); + }; + + let waitForSerial = Promise.resolve(); + return runnables.reduce((prev, runnable) => { + if (runnable.metadata.serial || this.serial) { + waitForSerial = prev.then(() => { + // Serial runnables run as long as there was no previous failure, unless + // the runnable should always be run. + return (allPassed || runnable.metadata.always) && runAndStoreResult(runnable); + }); + return waitForSerial; + } - this.hasStarted = true; - this.tests.on('test', result => { - this.addTestResult(result); + return Promise.all([ + prev, + waitForSerial.then(() => { + // Concurrent runnables are kicked off after the previous serial + // runnables have completed, as long as there was no previous failure + // (or if the runnable should always be run). One concurrent runnable's + // failure does not prevent the next runnable from running. + return (allPassed || runnable.metadata.always) && runAndStoreResult(runnable); + }) + ]); + }, waitForSerial).then(() => ({allPassed, storedResults})); + } + + runSingle(runnable) { + this.onRun(runnable); + return runnable.run().then(result => { + // If run() throws or rejects then the entire test run crashes, so + // onRunComplete() doesn't *have* to be inside a finally(). + this.onRunComplete(runnable); + return result; }); - return Bluebird.try(() => this.tests.build().run()); } - attributeLeakedError(err) { - return this.tests.attributeLeakedError(err); + runHooks(tasks, contextRef, titleSuffix) { + const hooks = tasks.map(task => new Runnable({ + contextRef, + failWithoutAssertions: false, + fn: task.args.length === 0 ? + task.implementation : + t => task.implementation.apply(null, [t].concat(task.args)), + compareTestSnapshot: this.boundCompareTestSnapshot, + metadata: task.metadata, + title: `${task.title}${titleSuffix || ''}` + })); + return this.runMultiple(hooks, this.serial).then(outcome => { + if (outcome.allPassed) { + return true; + } + + // Only emit results for failed hooks. + for (const result of outcome.storedResults) { + if (!result.passed) { + this.stats.failedHookCount++; + this.emit('hook-failed', result); + } + } + return false; + }); + } + + runTest(task, contextRef) { + const hookSuffix = ` for ${task.title}`; + return this.runHooks(this.tasks.beforeEach, contextRef, hookSuffix).then(hooksOk => { + // Don't run the test if a `beforeEach` hook failed. + if (!hooksOk) { + return false; + } + + const test = new Runnable({ + contextRef, + failWithoutAssertions: this.failWithoutAssertions, + fn: task.args.length === 0 ? + task.implementation : + t => task.implementation.apply(null, [t].concat(task.args)), + compareTestSnapshot: this.boundCompareTestSnapshot, + metadata: task.metadata, + title: task.title + }); + return this.runSingle(test).then(result => { + if (!result.passed) { + this.stats.failCount++; + this.emit('test', result); + // Don't run `afterEach` hooks if the test failed. + return false; + } + + if (result.metadata.failing) { + this.stats.knownFailureCount++; + } else { + this.stats.passCount++; + } + this.emit('test', result); + return this.runHooks(this.tasks.afterEach, contextRef, hookSuffix); + }); + }).then(hooksAndTestOk => { + return this.runHooks(this.tasks.afterEachAlways, contextRef, hookSuffix).then(alwaysOk => { + return hooksAndTestOk && alwaysOk; + }); + }); + } + + start() { + const runOnlyExclusive = this.stats.hasExclusive || this.runOnlyExclusive; + + const todoTitles = []; + for (const task of this.tasks.todo) { + if (runOnlyExclusive && !task.metadata.exclusive) { + continue; + } + + this.stats.testCount++; + this.stats.todoCount++; + todoTitles.push(task.title); + } + + const concurrentTests = []; + const serialTests = []; + const skippedTests = []; + for (const task of this.tasks.serial) { + if (runOnlyExclusive && !task.metadata.exclusive) { + continue; + } + + this.stats.testCount++; + if (task.metadata.skipped) { + this.stats.skipCount++; + skippedTests.push({ + failing: task.metadata.failing, + title: task.title + }); + } else { + serialTests.push(task); + } + } + for (const task of this.tasks.concurrent) { + if (runOnlyExclusive && !task.metadata.exclusive) { + continue; + } + + this.stats.testCount++; + if (task.metadata.skipped) { + this.stats.skipCount++; + skippedTests.push({ + failing: task.metadata.failing, + title: task.title + }); + } else if (this.serial) { + serialTests.push(task); + } else { + concurrentTests.push(task); + } + } + + if (concurrentTests.length === 0 && serialTests.length === 0) { + this.emit('start', { + // `ended` is always resolved with `undefined`. + ended: Promise.resolve(undefined), + skippedTests, + stats: this.stats, + todoTitles + }); + // Don't run any hooks if there are no tests to run. + return; + } + + const contextRef = new ContextRef(); + + // Note that the hooks and tests always begin running asynchronously. + const beforePromise = this.runHooks(this.tasks.before, contextRef); + const serialPromise = beforePromise.then(beforeHooksOk => { + // Don't run tests if a `before` hook failed. + if (!beforeHooksOk) { + return false; + } + + return serialTests.reduce((prev, task) => { + return prev.then(prevOk => { + // Don't start tests after an interrupt. + if (this.interrupted) { + return prevOk; + } + + // Prevent subsequent tests from running if `failFast` is enabled and + // the previous test failed. + if (!prevOk && this.failFast) { + return false; + } + + return this.runTest(task, contextRef.copy()); + }); + }, Promise.resolve(true)); + }); + const concurrentPromise = Promise.all([beforePromise, serialPromise]).then(prevOkays => { + const beforeHooksOk = prevOkays[0]; + const serialOk = prevOkays[1]; + // Don't run tests if a `before` hook failed, or if `failFast` is enabled + // and a previous serial test failed. + if (!beforeHooksOk || (!serialOk && this.failFast)) { + return false; + } + + // Don't start tests after an interrupt. + if (this.interrupted) { + return true; + } + + // If a concurrent test fails, even if `failFast` is enabled it won't + // stop other concurrent tests from running. + return Promise.all(concurrentTests.map(task => { + return this.runTest(task, contextRef.copy()); + })).then(allOkays => allOkays.every(ok => ok)); + }); + + const beforeExitHandler = this.beforeExitHandler.bind(this); + process.on('beforeExit', beforeExitHandler); + + const ended = concurrentPromise + // Only run `after` hooks if all hooks and tests passed. + .then(ok => ok && this.runHooks(this.tasks.after, contextRef)) + // Always run `after.always` hooks. + .then(() => this.runHooks(this.tasks.afterAlways, contextRef)) + .then(() => { + process.removeListener('beforeExit', beforeExitHandler); + // `ended` is always resolved with `undefined`. + return undefined; + }); + + this.emit('start', { + ended, + skippedTests, + stats: this.stats, + todoTitles + }); + } + + interrupt() { + this.interrupted = true; } } diff --git a/lib/sequence.js b/lib/sequence.js deleted file mode 100644 index 1e5960a98..000000000 --- a/lib/sequence.js +++ /dev/null @@ -1,94 +0,0 @@ -'use strict'; - -const beforeExitSubscribers = new Set(); -const beforeExitHandler = () => { - for (const subscriber of beforeExitSubscribers) { - subscriber(); - } -}; -const onBeforeExit = subscriber => { - if (beforeExitSubscribers.size === 0) { - // Only listen for the event once, no matter how many Sequences are run - // concurrently. - process.on('beforeExit', beforeExitHandler); - } - - beforeExitSubscribers.add(subscriber); - return { - dispose() { - beforeExitSubscribers.delete(subscriber); - if (beforeExitSubscribers.size === 0) { - process.removeListener('beforeExit', beforeExitHandler); - } - } - }; -}; - -class Sequence { - constructor(runnables, bail) { - if (!Array.isArray(runnables)) { - throw new TypeError('Expected an array of runnables'); - } - - this.runnables = runnables; - this.bail = bail || false; - } - - run() { - const iterator = this.runnables[Symbol.iterator](); - - let activeRunnable; - const beforeExit = onBeforeExit(() => { - if (activeRunnable.finishDueToInactivity) { - activeRunnable.finishDueToInactivity(); - } - }); - - let allPassed = true; - const finish = () => { - beforeExit.dispose(); - return allPassed; - }; - - const runNext = () => { - let promise; - - for (let next = iterator.next(); !next.done; next = iterator.next()) { - activeRunnable = next.value; - const passedOrPromise = activeRunnable.run(); - if (!passedOrPromise) { - allPassed = false; - - if (this.bail) { - // Stop if the test failed and bail mode is on. - break; - } - } else if (passedOrPromise !== true) { - promise = passedOrPromise; - break; - } - } - - if (!promise) { - return finish(); - } - - return promise.then(passed => { - if (!passed) { - allPassed = false; - - if (this.bail) { - // Stop if the test failed and bail mode is on. - return finish(); - } - } - - return runNext(); - }); - }; - - return runNext(); - } -} - -module.exports = Sequence; diff --git a/lib/serialize-error.js b/lib/serialize-error.js index 13146ff42..969f78e02 100644 --- a/lib/serialize-error.js +++ b/lib/serialize-error.js @@ -90,7 +90,19 @@ module.exports = error => { } if (typeof error.stack === 'string') { - retval.summary = error.stack.split('\n')[0]; + const lines = error.stack.split('\n'); + if (error.name === 'SyntaxError' && !lines[0].startsWith('SyntaxError')) { + retval.summary = ''; + for (const line of lines) { + retval.summary += line + '\n'; + if (line.startsWith('SyntaxError')) { + break; + } + } + retval.summary = retval.summary.trim(); + } else { + retval.summary = lines[0]; + } } else { retval.summary = JSON.stringify(error); } diff --git a/lib/test-collection.js b/lib/test-collection.js deleted file mode 100644 index 0569e88e6..000000000 --- a/lib/test-collection.js +++ /dev/null @@ -1,250 +0,0 @@ -'use strict'; -const EventEmitter = require('events'); -const clone = require('lodash.clone'); -const Concurrent = require('./concurrent'); -const Sequence = require('./sequence'); -const Test = require('./test'); - -class ContextRef { - constructor() { - this.value = {}; - } - - get() { - return this.value; - } - - set(newValue) { - this.value = newValue; - } - - copy() { - return new LateBinding(this); // eslint-disable-line no-use-before-define - } -} - -class LateBinding extends ContextRef { - constructor(ref) { - super(); - this.ref = ref; - this.bound = false; - } - - get() { - if (!this.bound) { - this.set(clone(this.ref.get())); - } - return super.get(); - } - - set(newValue) { - this.bound = true; - super.set(newValue); - } -} - -class TestCollection extends EventEmitter { - constructor(options) { - super(); - - this.bail = options.bail; - this.failWithoutAssertions = options.failWithoutAssertions; - this.compareTestSnapshot = options.compareTestSnapshot; - this.hasExclusive = false; - this.testCount = 0; - - this.tests = { - concurrent: [], - serial: [] - }; - - this.hooks = { - before: [], - beforeEach: [], - after: [], - afterAlways: [], - afterEach: [], - afterEachAlways: [] - }; - - this.pendingTestInstances = new Set(); - this.uniqueTestTitles = new Set(); - - this._emitTestResult = this._emitTestResult.bind(this); - } - - add(test) { - const metadata = test.metadata; - const type = metadata.type; - - if (test.title === '' || typeof test.title !== 'string') { - if (type === 'test') { - throw new TypeError('Tests must have a title'); - } else { - test.title = `${type} hook`; - } - } - - if (type === 'test') { - if (this.uniqueTestTitles.has(test.title)) { - throw new Error(`Duplicate test title: ${test.title}`); - } else { - this.uniqueTestTitles.add(test.title); - } - } - - // Add a hook - if (type !== 'test') { - this.hooks[type + (metadata.always ? 'Always' : '')].push(test); - return; - } - - this.testCount++; - - // Add `.only()` tests if `.only()` was used previously - if (this.hasExclusive && !metadata.exclusive) { - return; - } - - if (metadata.exclusive && !this.hasExclusive) { - this.tests.concurrent = []; - this.tests.serial = []; - this.hasExclusive = true; - } - - if (metadata.serial) { - this.tests.serial.push(test); - } else { - this.tests.concurrent.push(test); - } - } - - _skippedTest(test) { - return { - run: () => { - this._emitTestResult({ - passed: true, - result: test - }); - - return true; - } - }; - } - - _emitTestResult(result) { - this.pendingTestInstances.delete(result.result); - this.emit('test', result); - } - - _buildHooks(hooks, testTitle, contextRef) { - return hooks.map(hook => { - const test = this._buildHook(hook, testTitle, contextRef); - - if (hook.metadata.skipped || hook.metadata.todo) { - return this._skippedTest(test); - } - - return test; - }); - } - - _buildHook(hook, testTitle, contextRef) { - let title = hook.title; - - if (testTitle) { - title += ` for ${testTitle}`; - } - - const test = new Test({ - contextRef, - failWithoutAssertions: false, - fn: hook.fn, - compareTestSnapshot: this.compareTestSnapshot, - metadata: hook.metadata, - onResult: this._emitTestResult, - title - }); - this.pendingTestInstances.add(test); - return test; - } - - _buildTest(test, contextRef) { - test = new Test({ - contextRef, - failWithoutAssertions: this.failWithoutAssertions, - fn: test.fn, - compareTestSnapshot: this.compareTestSnapshot, - metadata: test.metadata, - onResult: this._emitTestResult, - title: test.title - }); - this.pendingTestInstances.add(test); - return test; - } - - _buildTestWithHooks(test, contextRef) { - if (test.metadata.skipped || test.metadata.todo) { - return new Sequence([this._skippedTest(this._buildTest(test))], true); - } - - const copiedRef = contextRef.copy(); - - const beforeHooks = this._buildHooks(this.hooks.beforeEach, test.title, copiedRef); - const afterHooks = this._buildHooks(this.hooks.afterEach, test.title, copiedRef); - - let sequence = new Sequence([].concat(beforeHooks, this._buildTest(test, copiedRef), afterHooks), true); - if (this.hooks.afterEachAlways.length > 0) { - const afterAlwaysHooks = new Sequence(this._buildHooks(this.hooks.afterEachAlways, test.title, copiedRef)); - sequence = new Sequence([sequence, afterAlwaysHooks], false); - } - return sequence; - } - - _buildTests(tests, contextRef) { - return tests.map(test => this._buildTestWithHooks(test, contextRef)); - } - - _hasUnskippedTests() { - return this.tests.serial.concat(this.tests.concurrent) - .some(test => { - return !(test.metadata && test.metadata.skipped === true); - }); - } - - build() { - const contextRef = new ContextRef(); - - const serialTests = new Sequence(this._buildTests(this.tests.serial, contextRef), this.bail); - const concurrentTests = new Concurrent(this._buildTests(this.tests.concurrent, contextRef), this.bail); - const allTests = new Sequence([serialTests, concurrentTests]); - - let finalTests; - // Only run before and after hooks when there are unskipped tests - if (this._hasUnskippedTests()) { - const beforeHooks = new Sequence(this._buildHooks(this.hooks.before, null, contextRef)); - const afterHooks = new Sequence(this._buildHooks(this.hooks.after, null, contextRef)); - finalTests = new Sequence([beforeHooks, allTests, afterHooks], true); - } else { - finalTests = new Sequence([allTests], true); - } - - if (this.hooks.afterAlways.length > 0) { - const afterAlwaysHooks = new Sequence(this._buildHooks(this.hooks.afterAlways, null, contextRef)); - finalTests = new Sequence([finalTests, afterAlwaysHooks], false); - } - - return finalTests; - } - - attributeLeakedError(err) { - for (const test of this.pendingTestInstances) { - if (test.attributeLeakedError(err)) { - return true; - } - } - return false; - } -} - -module.exports = TestCollection; diff --git a/lib/test-worker.js b/lib/test-worker.js index 972e88689..ff93e5ad0 100644 --- a/lib/test-worker.js +++ b/lib/test-worker.js @@ -20,35 +20,40 @@ const currentlyUnhandled = require('currently-unhandled')(); const isObj = require('is-obj'); const adapter = require('./process-adapter'); -const globals = require('./globals'); - -const opts = adapter.opts; -globals.options = opts; - const serializeError = require('./serialize-error'); +const opts = require('./worker-options').get(); -// Install before processing opts.require, so if helpers are added to the -// require configuration the *compiled* helper will be loaded. -adapter.installSourceMapSupport(); -adapter.installPrecompilerHook(); - -(opts.require || []).forEach(x => { - if (/[/\\]@std[/\\]esm[/\\]index\.js$/.test(x)) { - require = require(x)(module); // eslint-disable-line no-global-assign - } else { - require(x); - } -}); - -const testPath = opts.file; - +// Store details about the test run, to be sent to the parent process later. const dependencies = new Set(); -adapter.installDependencyTracking(dependencies, testPath); - const touchedFiles = new Set(); // Set when main.js is required (since test files should have `require('ava')`). let runner = null; + +// Track when exiting begins, to avoid repeatedly sending stats, or sending +// individual test results once stats have been sent. This is necessary since +// exit() can be invoked from the worker process and over IPC. +let exiting = false; +function exit() { + if (exiting) { + return; + } + exiting = true; + + // Reference the IPC channel so the exit sequence can be completed. + adapter.forceRefChannel(); + + const stats = { + failCount: runner.stats.failCount + runner.stats.failedHookCount, + knownFailureCount: runner.stats.knownFailureCount, + passCount: runner.stats.passCount, + skipCount: runner.stats.skipCount, + testCount: runner.stats.testCount, + todoCount: runner.stats.todoCount + }; + adapter.send('results', {stats}); +} + exports.setRunner = newRunner => { runner = newRunner; runner.on('dependency', file => { @@ -59,14 +64,69 @@ exports.setRunner = newRunner => { touchedFiles.add(file); } }); -}; + runner.on('start', started => { + adapter.send('stats', { + testCount: started.stats.testCount, + hasExclusive: started.stats.hasExclusive + }); -require(testPath); + for (const partial of started.skippedTests) { + adapter.send('test', { + duration: null, + error: null, + failing: partial.failing, + logs: [], + skip: true, + title: partial.title, + todo: false, + type: 'test' + }); + } + for (const title of started.todoTitles) { + adapter.send('test', { + duration: null, + error: null, + failing: false, + logs: [], + skip: true, + title, + todo: true, + type: 'test' + }); + } -// If AVA was not required, show an error -if (!runner) { - adapter.send('no-tests', {avaRequired: false}); -} + started.ended.then(() => { + runner.saveSnapshotState(); + return exit(); + }).catch(err => { + handleUncaughtException(err); + }); + }); + runner.on('hook-failed', result => { + adapter.send('test', { + duration: result.duration, + error: serializeError(result.error), + failing: result.metadata.failing, + logs: result.logs, + skip: result.metadata.skip, + title: result.title, + todo: result.metadata.todo, + type: result.metadata.type + }); + }); + runner.on('test', result => { + adapter.send('test', { + duration: result.duration, + error: result.passed ? null : serializeError(result.error), + failing: result.metadata.failing, + logs: result.logs, + skip: result.metadata.skip, + title: result.title, + todo: result.metadata.todo, + type: result.metadata.type + }); + }); +}; function attributeLeakedError(err) { if (!runner) { @@ -76,14 +136,7 @@ function attributeLeakedError(err) { return runner.attributeLeakedError(err); } -const attributedRejections = new Set(); -process.on('unhandledRejection', (reason, promise) => { - if (attributeLeakedError(reason)) { - attributedRejections.add(promise); - } -}); - -process.on('uncaughtException', exception => { +function handleUncaughtException(exception) { if (attributeLeakedError(exception)) { return; } @@ -102,13 +155,22 @@ process.on('uncaughtException', exception => { }; } - // Ensure the IPC channel is refereced. The uncaught exception will kick off + // Ensure the IPC channel is referenced. The uncaught exception will kick off // the teardown sequence, for which the messages must be received. - adapter.ipcChannel.ref(); + adapter.forceRefChannel(); adapter.send('uncaughtException', {exception: serialized}); +} + +const attributedRejections = new Set(); +process.on('unhandledRejection', (reason, promise) => { + if (attributeLeakedError(reason)) { + attributedRejections.add(promise); + } }); +process.on('uncaughtException', handleUncaughtException); + let tearingDown = false; process.on('ava-teardown', () => { // AVA-teardown can be sent more than once @@ -117,6 +179,9 @@ process.on('ava-teardown', () => { } tearingDown = true; + // Reference the IPC channel so the teardown sequence can be completed. + adapter.forceRefChannel(); + let rejections = currentlyUnhandled() .filter(rejection => !attributedRejections.has(rejection.promise)); @@ -147,3 +212,47 @@ process.on('ava-teardown', () => { process.on('ava-exit', () => { process.exit(0); // eslint-disable-line xo/no-process-exit }); + +process.on('ava-init-exit', () => { + exit(); +}); + +process.on('ava-peer-failed', () => { + if (runner) { + runner.interrupt(); + } +}); + +// Store value in case to prevent required modules from modifying it. +const testPath = opts.file; + +// Install before processing opts.require, so if helpers are added to the +// require configuration the *compiled* helper will be loaded. +adapter.installDependencyTracking(dependencies, testPath); +adapter.installSourceMapSupport(); +adapter.installPrecompilerHook(); + +try { + (opts.require || []).forEach(x => { + if (/[/\\]@std[/\\]esm[/\\]index\.js$/.test(x)) { + require = require(x)(module); // eslint-disable-line no-global-assign + } else { + require(x); + } + }); + + require(testPath); +} catch (err) { + handleUncaughtException(err); +} finally { + adapter.send('loaded-file', {avaRequired: Boolean(runner)}); + + if (runner) { + // Unreference the IPC channel if the test file required AVA. This stops it + // from keeping the event loop busy, which means the `beforeExit` event can be + // used to detect when tests stall. + // If AVA was not required then the parent process will initiated a teardown + // sequence, for which this process ought to stay active. + adapter.unrefChannel(); + } +} diff --git a/lib/test.js b/lib/test.js index b3bbe873a..115b02106 100644 --- a/lib/test.js +++ b/lib/test.js @@ -7,7 +7,7 @@ const isPromise = require('is-promise'); const isObservable = require('is-observable'); const plur = require('plur'); const assert = require('./assert'); -const globals = require('./globals'); +const nowAndTimers = require('./now-and-timers'); const concordanceOptions = require('./concordance-options').default; function formatErrorValue(label, error) { @@ -103,7 +103,6 @@ class Test { this.failWithoutAssertions = options.failWithoutAssertions; this.fn = isGeneratorFn(options.fn) ? co.wrap(options.fn) : options.fn; this.metadata = options.metadata; - this.onResult = options.onResult; this.title = options.title; this.logs = []; @@ -278,7 +277,7 @@ class Test { // Wait up to a second to see if an error can be attributed to the // pending assertion. - globals.setTimeout(() => this.finishDueToInactivity(), 1000).unref(); + nowAndTimers.setTimeout(() => this.finishDueToInactivity(), 1000).unref(); }); } @@ -306,7 +305,7 @@ class Test { } run() { - this.startedAt = globals.now(); + this.startedAt = nowAndTimers.now(); const result = this.callFn(); if (!result.ok) { @@ -317,7 +316,7 @@ class Test { values: [formatErrorValue('Error thrown in test:', result.error)] })); } - return this.finish(); + return this.finishPromised(); } const returnedObservable = isObservable(result.retval); @@ -335,11 +334,11 @@ class Test { if (returnedObservable || returnedPromise) { const asyncType = returnedObservable ? 'observables' : 'promises'; this.saveFirstError(new Error(`Do not return ${asyncType} from tests declared via \`test.cb(...)\`, if you want to return a promise simply declare the test via \`test(...)\``)); - return this.finish(); + return this.finishPromised(); } if (this.calledEnd) { - return this.finish(); + return this.finishPromised(); } return new Promise(resolve => { @@ -386,7 +385,7 @@ class Test { }); } - return this.finish(); + return this.finishPromised(); } finish() { @@ -399,28 +398,29 @@ class Test { this.verifyPlan(); this.verifyAssertions(); - this.duration = globals.now() - this.startedAt; + this.duration = nowAndTimers.now() - this.startedAt; - let reason = this.assertError; - let passed = !reason; + let error = this.assertError; + let passed = !error; if (this.metadata.failing) { passed = !passed; if (passed) { - reason = undefined; + error = null; } else { - reason = new Error('Test was expected to fail, but succeeded, you should stop marking the test as failing'); + error = new Error('Test was expected to fail, but succeeded, you should stop marking the test as failing'); } } - this.onResult({ + return { + duration: this.duration, + error, + logs: this.logs, + metadata: this.metadata, passed, - result: this, - reason - }); - - return passed; + title: this.title + }; } finishPromised() { diff --git a/lib/validate-test.js b/lib/validate-test.js deleted file mode 100644 index 8258a5990..000000000 --- a/lib/validate-test.js +++ /dev/null @@ -1,48 +0,0 @@ -'use strict'; - -function validate(title, fn, metadata) { - if (metadata.type !== 'test') { - if (metadata.exclusive) { - return '`only` is only for tests and cannot be used with hooks'; - } - - if (metadata.failing) { - return '`failing` is only for tests and cannot be used with hooks'; - } - - if (metadata.todo) { - return '`todo` is only for documentation of future tests and cannot be used with hooks'; - } - } - - if (metadata.todo) { - if (typeof fn === 'function') { - return '`todo` tests are not allowed to have an implementation. Use ' + - '`test.skip()` for tests with an implementation.'; - } - - if (typeof title !== 'string') { - return '`todo` tests require a title'; - } - - if (metadata.skipped || metadata.failing || metadata.exclusive) { - return '`todo` tests are just for documentation and cannot be used with `skip`, `only`, or `failing`'; - } - } else if (typeof fn !== 'function') { - return 'Expected an implementation. Use `test.todo()` for tests without an implementation.'; - } - - if (metadata.always) { - if (!(metadata.type === 'after' || metadata.type === 'afterEach')) { - return '`always` can only be used with `after` and `afterEach`'; - } - } - - if (metadata.skipped && metadata.exclusive) { - return '`only` tests cannot be skipped'; - } - - return null; -} - -module.exports = validate; diff --git a/lib/worker-options.js b/lib/worker-options.js new file mode 100644 index 000000000..65a7c1756 --- /dev/null +++ b/lib/worker-options.js @@ -0,0 +1,14 @@ +'use strict'; +let options = null; +exports.get = () => { + if (!options) { + throw new Error('Options have not yet been set'); + } + return options; +}; +exports.set = newOptions => { + if (options) { + throw new Error('Options have already been set'); + } + options = newOptions; +}; diff --git a/package-lock.json b/package-lock.json index a4f1cabff..442826346 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2002,7 +2002,8 @@ "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=" + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true }, "component-emitter": { "version": "1.2.1", @@ -3270,26 +3271,6 @@ "repeat-string": "1.6.1" } }, - "find-cache-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", - "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", - "requires": { - "commondir": "1.0.1", - "make-dir": "1.1.0", - "pkg-dir": "2.0.0" - }, - "dependencies": { - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "requires": { - "find-up": "2.1.0" - } - } - } - }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", diff --git a/package.json b/package.json index e7211fdd4..1a6e099df 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,6 @@ "empower-core": "^0.6.1", "equal-length": "^1.0.0", "figures": "^2.0.0", - "find-cache-dir": "^1.0.0", "get-port": "^3.2.0", "globby": "^7.1.1", "hullabaloo-config-manager": "^2.0.0-beta.2", diff --git a/profile.js b/profile.js index 26b0bfd05..35acdce80 100644 --- a/profile.js +++ b/profile.js @@ -9,13 +9,11 @@ const EventEmitter = require('events'); const meow = require('meow'); const Promise = require('bluebird'); const pkgConf = require('pkg-conf'); -const findCacheDir = require('find-cache-dir'); const uniqueTempDir = require('unique-temp-dir'); const arrify = require('arrify'); const resolveCwd = require('resolve-cwd'); const babelConfigHelper = require('./lib/babel-config'); const CachingPrecompiler = require('./lib/caching-precompiler'); -const globals = require('./lib/globals'); function resolveModules(modules) { return arrify(modules).map(name => { @@ -29,10 +27,6 @@ function resolveModules(modules) { }); } -// Chrome gets upset when the `this` value is non-null for these functions -globals.setTimeout = setTimeout.bind(null); -globals.clearTimeout = clearTimeout.bind(null); - Promise.longStackTraces(); const conf = pkgConf.sync('ava', { @@ -44,6 +38,9 @@ const conf = pkgConf.sync('ava', { } }); +const filepath = pkgConf.filepath(conf); +const projectDir = filepath === null ? process.cwd() : path.dirname(filepath); + // Define a minimal set of options from the main CLI const cli = meow(` Usage @@ -74,10 +71,7 @@ if (cli.input.length === 0) { } const file = path.resolve(cli.input[0]); -const cacheDir = findCacheDir({ - name: 'ava', - files: [file] -}) || uniqueTempDir(); +const cacheDir = conf.cacheEnabled === false ? uniqueTempDir() : path.join(projectDir, 'node_modules', '.cache', 'ava'); babelConfigHelper.build(process.cwd(), cacheDir, babelConfigHelper.validate(conf.babel), conf.compileEnhancements === true) .then(result => { @@ -103,6 +97,9 @@ babelConfigHelper.build(process.cwd(), cacheDir, babelConfigHelper.validate(conf }; const events = new EventEmitter(); + events.on('loaded-file', () => {}); + + let failCount = 0; let uncaughtExceptionCount = 0; // Mock the behavior of a parent process @@ -134,8 +131,13 @@ babelConfigHelper.build(process.cwd(), cacheDir, babelConfigHelper.validate(conf console.log('RESULTS:', data.stats); + failCount = data.stats.failCount; + setImmediate(() => process.emit('ava-teardown')); + }); + + events.on('teardown', () => { if (process.exit) { - process.exit(data.stats.failCount + uncaughtExceptionCount); // eslint-disable-line unicorn/no-process-exit + process.exit(failCount + uncaughtExceptionCount); // eslint-disable-line unicorn/no-process-exit } }); diff --git a/readme.md b/readme.md index b039e14fa..5004a2b21 100644 --- a/readme.md +++ b/readme.md @@ -524,17 +524,15 @@ test.failing('demonstrate some bug', t => { AVA lets you register hooks that are run before and after your tests. This allows you to run setup and/or teardown code. -`test.before()` registers a hook to be run before the first test in your test file. Similarly `test.after()` registers a hook to be run after the last test. Use `test.after.always()` to register a hook that will **always** run once your tests and other hooks complete. `.always()` hooks run regardless of whether there were earlier failures or if all tests were skipped, so they are ideal for cleanup tasks. There are two exceptions to this however. If you use `--fail-fast` AVA will stop testing as soon as a failure occurs, and it won't run any hooks including the `.always()` hooks. Uncaught exceptions will crash your tests, possibly preventing `.always()` hooks from running. +`test.before()` registers a hook to be run before the first test in your test file. Similarly `test.after()` registers a hook to be run after the last test. Use `test.after.always()` to register a hook that will **always** run once your tests and other hooks complete. `.always()` hooks run regardless of whether there were earlier failures, so they are ideal for cleanup tasks. Note however that uncaught exceptions, unhandled rejections or timeouts will crash your tests, possibly preventing `.always()` hooks from running. -`test.beforeEach()` registers a hook to be run before each test in your test file. Similarly `test.afterEach()` a hook to be run after each test. Use `test.afterEach.always()` to register an after hook that is called even if other test hooks, or the test itself, fail. `.always()` hooks are ideal for cleanup tasks. +`test.beforeEach()` registers a hook to be run before each test in your test file. Similarly `test.afterEach()` a hook to be run after each test. Use `test.afterEach.always()` to register an after hook that is called even if other test hooks, or the test itself, fail. -If a test is skipped with the `.skip` modifier, the respective `.beforeEach()` and `.afterEach()` hooks are not run. Likewise, if all tests in a test file are skipped `.before()` and `.after()` hooks for the file are not run. Hooks modified with `.always()` will always run, even if all tests are skipped. +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. -**Note**: If the `--fail-fast` flag is specified, AVA will stop after the first test failure and the `.always` hook will **not** run. +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](#t). You can use assertions in your hooks. You can also pass a [macro function](#test-macros) and additional arguments. -Like `test()` these methods take an optional title and a callback function. The title is shown if your hook fails to execute. The callback is called with an [execution object](#t). - -`before` hooks execute before `beforeEach` hooks. `afterEach` hooks execute before `after` hooks. Within their category the hooks execute in the order they were defined. +`.before()` hooks execute before `.beforeEach()` hooks. `.afterEach()` hooks execute before `.after()` hooks. Within their category the hooks execute in the order they were defined. By default hooks execute concurrently, but you can use `test.serial` to ensure only that single hook is run at a time. Unlike with tests, serial hooks are *not* run before other hooks: ```js test.before(t => { @@ -542,7 +540,15 @@ test.before(t => { }); test.before(t => { - // This runs after the above, but before tests + // This runs concurrently with the above +}); + +test.serial.before(t => { + // This runs after the above +}); + +test.serial.before(t => { + // This too runs after the above, and before tests }); test.after('cleanup', t => { @@ -590,13 +596,13 @@ test.afterEach.cb(t => { }); ``` -Keep in mind that the `beforeEach` and `afterEach` hooks run just before and after a test is run, and that by default tests run concurrently. If you need to set up global state for each test (like spying on `console.log` [for example](https://github.com/avajs/ava/issues/560)), you'll need to make sure the tests are [run serially](#running-tests-serially). +Keep in mind that the `.beforeEach()` and `.afterEach()` hooks run just before and after a test is run, and that by default tests run concurrently. This means each multiple `.beforeEach()` hooks may run concurrently. Using `test.serial.beforeEach()` does not change this. If you need to set up global state for each test (like spying on `console.log` [for example](https://github.com/avajs/ava/issues/560)), you'll need to make sure the tests themselves are [run serially](#running-tests-serially). -Remember that AVA runs each test file in its own process. You may not have to clean up global state in a `after`-hook since that's only called right before the process exits. +Remember that AVA runs each test file in its own process. You may not have to clean up global state in a `.after()`-hook since that's only called right before the process exits. #### Test context -The `beforeEach` & `afterEach` hooks can share context with the test: +The `.beforeEach()` & `.afterEach()` hooks can share context with the test: ```js test.beforeEach(t => { @@ -620,7 +626,7 @@ test('context is unicorn', t => { }); ``` -Context sharing is *not* available to `before` and `after` hooks. +Context sharing is *not* available to `.before()` and `.after()` hooks. ### Test macros @@ -781,9 +787,7 @@ test.cb('data.txt can be read', t => { ### Global timeout -A global timeout can be set via the `--timeout` option. -Timeout in AVA behaves 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. +A global timeout can be set via the `--timeout` option. Timeout in AVA behaves 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. You can set timeouts in a human-readable way: @@ -823,7 +827,7 @@ Should contain the actual test. Type: `object` -The execution object of a particular test. Each test implementation receives a different object. Contains the [assertions](#assertions) as well as `.plan(count)` and `.end()` methods. `t.context` can contain shared state from `beforeEach` hooks. `t.title` returns the test's title. +The execution object of a particular test. Each test implementation receives a different object. Contains the [assertions](#assertions) as well as `.plan(count)` and `.end()` methods. `t.context` can contain shared state from `.beforeEach()` hooks. `t.title` returns the test's title. ###### `t.plan(count)` diff --git a/test/api.js b/test/api.js index 559f6716e..8ee4e32ed 100644 --- a/test/api.js +++ b/test/api.js @@ -13,6 +13,7 @@ const ROOT_DIR = path.join(__dirname, '..'); function apiCreator(options) { options = options || {}; options.babelConfig = options.babelConfig || {testOptions: {}}; + options.concurrency = 2; options.projectDir = options.projectDir || ROOT_DIR; options.resolveTestsFrom = options.resolveTestsFrom || options.projectDir; const instance = new Api(options); @@ -22,1023 +23,1166 @@ function apiCreator(options) { return instance; } -generateTests('With Pool:', options => { - options = options || {}; - options.concurrency = 2; - return apiCreator(options); -}); +test('ES2015 support', t => { + const api = apiCreator(); -function generateTests(prefix, apiCreator) { - test(`${prefix} ES2015 support`, t => { - const api = apiCreator(); + return api.run([path.join(__dirname, 'fixture/es2015.js')]) + .then(result => { + t.is(result.passCount, 1); + }); +}); - return api.run([path.join(__dirname, 'fixture/es2015.js')]) - .then(result => { - t.is(result.passCount, 1); - }); +test('precompile helpers', t => { + const api = apiCreator({ + precompileHelpers: true, + resolveTestsFrom: path.join(__dirname, 'fixture/precompile-helpers') }); - test(`${prefix} precompile helpers`, t => { - const api = apiCreator({ - precompileHelpers: true, - resolveTestsFrom: path.join(__dirname, 'fixture/precompile-helpers') + return api.run() + .then(result => { + t.is(result.passCount, 1); }); +}); - return api.run() - .then(result => { - t.is(result.passCount, 1); - }); - }); +test('generators support', t => { + const api = apiCreator(); - test(`${prefix} generators support`, t => { - const api = apiCreator(); + return api.run([path.join(__dirname, 'fixture/generators.js')]) + .then(result => { + t.is(result.passCount, 1); + }); +}); - return api.run([path.join(__dirname, 'fixture/generators.js')]) - .then(result => { - t.is(result.passCount, 1); - }); - }); +test('async/await support', t => { + const api = apiCreator(); - test(`${prefix} async/await support`, t => { - const api = apiCreator(); + return api.run([path.join(__dirname, 'fixture/async-await.js')]) + .then(result => { + t.is(result.passCount, 2); + }); +}); - return api.run([path.join(__dirname, 'fixture/async-await.js')]) - .then(result => { - t.is(result.passCount, 2); - }); +test('test title prefixes — multiple files', t => { + t.plan(5); + + const separator = ` ${figures.pointerSmall} `; + const files = [ + path.join(__dirname, 'fixture/async-await.js'), + path.join(__dirname, 'fixture/generators.js'), + path.join(__dirname, 'fixture/subdir/in-a-subdir.js') + ]; + const expected = [ + ['async-await', 'async function'].join(separator), + ['async-await', 'arrow async function'].join(separator), + ['generators', 'generator function'].join(separator), + ['subdir', 'in-a-subdir', 'subdir'].join(separator) + ]; + let index; + + const api = apiCreator(); + + api.run(files) + .then(() => { + // If all lines were removed from expected output + // actual output matches expected output + t.is(expected.length, 0); + }); + + api.on('test-run', runStatus => { + runStatus.on('test', a => { + index = expected.indexOf(a.title); + + t.true(index >= 0); + + // Remove line from expected output + expected.splice(index, 1); + }); }); +}); - test(`${prefix} test title prefixes — multiple files`, t => { - t.plan(5); - - const separator = ` ${figures.pointerSmall} `; - const files = [ - path.join(__dirname, 'fixture/async-await.js'), - path.join(__dirname, 'fixture/generators.js'), - path.join(__dirname, 'fixture/subdir/in-a-subdir.js') - ]; - const expected = [ - ['async-await', 'async function'].join(separator), - ['async-await', 'arrow async function'].join(separator), - ['generators', 'generator function'].join(separator), - ['subdir', 'in-a-subdir', 'subdir'].join(separator) - ]; - let index; - - const api = apiCreator(); - - api.run(files) - .then(() => { - // If all lines were removed from expected output - // actual output matches expected output - t.is(expected.length, 0); - }); +test('test title prefixes — single file', t => { + t.plan(2); + + const separator = ` ${figures.pointerSmall} `; + const files = [ + path.join(__dirname, 'fixture/generators.js') + ]; + const expected = [ + ['generator function'].join(separator) + ]; + let index; + + const api = apiCreator(); + + api.run(files) + .then(() => { + // If all lines were removed from expected output + // actual output matches expected output + t.is(expected.length, 0); + }); - api.on('test-run', runStatus => { - runStatus.on('test', a => { - index = expected.indexOf(a.title); + api.on('test-run', runStatus => { + runStatus.on('test', a => { + index = expected.indexOf(a.title); - t.true(index >= 0); + t.true(index >= 0); - // Remove line from expected output - expected.splice(index, 1); - }); + // Remove line from expected output + expected.splice(index, 1); }); }); +}); - test(`${prefix} test title prefixes — single file`, t => { - t.plan(2); +test('test title prefixes — single file (explicit)', t => { + t.plan(2); - const separator = ` ${figures.pointerSmall} `; - const files = [ - path.join(__dirname, 'fixture/generators.js') - ]; - const expected = [ - ['generator function'].join(separator) - ]; - let index; + const separator = ` ${figures.pointerSmall} `; + const files = [ + path.join(__dirname, 'fixture/generators.js') + ]; + const expected = [ + ['generators', 'generator function'].join(separator) + ]; + let index; - const api = apiCreator(); + const api = apiCreator({ + explicitTitles: true + }); - api.run(files) - .then(() => { - // If all lines were removed from expected output - // actual output matches expected output - t.is(expected.length, 0); - }); + api.run(files) + .then(() => { + // If all lines were removed from expected output + // actual output matches expected output + t.is(expected.length, 0); + }); - api.on('test-run', runStatus => { - runStatus.on('test', a => { - index = expected.indexOf(a.title); + api.on('test-run', runStatus => { + runStatus.on('test', a => { + index = expected.indexOf(a.title); - t.true(index >= 0); + t.true(index >= 0); - // Remove line from expected output - expected.splice(index, 1); - }); + // Remove line from expected output + expected.splice(index, 1); }); }); +}); - test(`${prefix} test title prefixes — single file (explicit)`, t => { - t.plan(2); +test('display filename prefixes for failed test stack traces', t => { + const files = [ + path.join(__dirname, 'fixture/es2015.js'), + path.join(__dirname, 'fixture/one-pass-one-fail.js') + ]; - const separator = ` ${figures.pointerSmall} `; - const files = [ - path.join(__dirname, 'fixture/generators.js') - ]; - const expected = [ - ['generators', 'generator function'].join(separator) - ]; - let index; + const api = apiCreator(); - const api = apiCreator({ - explicitTitles: true + return api.run(files) + .then(result => { + t.is(result.passCount, 2); + t.is(result.failCount, 1); + t.match(result.errors[0].title, /one-pass-one-fail \S this is a failing test/); }); +}); - api.run(files) - .then(() => { - // If all lines were removed from expected output - // actual output matches expected output - t.is(expected.length, 0); - }); - - api.on('test-run', runStatus => { - runStatus.on('test', a => { - index = expected.indexOf(a.title); +// This is a seperate test because we can't ensure the order of the errors (to match them), and this is easier than +// sorting. +test('display filename prefixes for failed test stack traces in subdirs', t => { + const files = [ + path.join(__dirname, 'fixture/es2015.js'), + path.join(__dirname, 'fixture/subdir/failing-subdir.js') + ]; - t.true(index >= 0); + const api = apiCreator(); - // Remove line from expected output - expected.splice(index, 1); - }); + return api.run(files) + .then(result => { + t.is(result.passCount, 1); + t.is(result.failCount, 1); + t.match(result.errors[0].title, /subdir \S failing-subdir \S subdir fail/); }); - }); +}); - test(`${prefix} display filename prefixes for failed test stack traces`, t => { - const files = [ - path.join(__dirname, 'fixture/es2015.js'), - path.join(__dirname, 'fixture/one-pass-one-fail.js') - ]; +test('fail-fast mode - single file & serial', t => { + const api = apiCreator({ + failFast: true + }); - const api = apiCreator(); + const tests = []; - return api.run(files) - .then(result => { - t.is(result.passCount, 2); - t.is(result.failCount, 1); - t.match(result.errors[0].title, /one-pass-one-fail \S this is a failing test/); + api.on('test-run', runStatus => { + runStatus.on('test', test => { + tests.push({ + ok: !test.error, + title: test.title }); + }); }); - // This is a seperate test because we can't ensure the order of the errors (to match them), and this is easier than - // sorting. - test(`${prefix} display filename prefixes for failed test stack traces in subdirs`, t => { - const files = [ - path.join(__dirname, 'fixture/es2015.js'), - path.join(__dirname, 'fixture/subdir/failing-subdir.js') - ]; + return api.run([path.join(__dirname, 'fixture/fail-fast/single-file/test.js')]) + .then(result => { + t.ok(api.options.failFast); + t.strictDeepEqual(tests, [{ + ok: true, + title: 'first pass' + }, { + ok: false, + title: 'second fail' + }, { + ok: true, + title: 'third pass' + }]); + t.is(result.passCount, 2); + t.is(result.failCount, 1); + }); +}); + +test('fail-fast mode - multiple files & serial', t => { + const api = apiCreator({ + failFast: true, + serial: true + }); - const api = apiCreator(); + const tests = []; - return api.run(files) - .then(result => { - t.is(result.passCount, 1); - t.is(result.failCount, 1); - t.match(result.errors[0].title, /subdir \S failing-subdir \S subdir fail/); + api.on('test-run', runStatus => { + runStatus.on('test', test => { + tests.push({ + ok: !test.error, + title: test.title }); + }); }); - test(`${prefix} fail-fast mode`, t => { - const api = apiCreator({ - failFast: true + return api.run([ + path.join(__dirname, 'fixture/fail-fast/multiple-files/fails.js'), + path.join(__dirname, 'fixture/fail-fast/multiple-files/passes.js') + ]) + .then(result => { + t.ok(api.options.failFast); + t.strictDeepEqual(tests, [{ + ok: true, + title: `fails ${figures.pointerSmall} first pass` + }, { + ok: false, + title: `fails ${figures.pointerSmall} second fail` + }]); + t.is(result.passCount, 1); + t.is(result.failCount, 1); }); +}); - const tests = []; +test('fail-fast mode - multiple files & interrupt', t => { + const api = apiCreator({ + failFast: true, + concurrency: 2 + }); - api.on('test-run', runStatus => { - runStatus.on('test', test => { - tests.push({ - ok: !test.error, - title: test.title - }); - }); - }); + const tests = []; - return api.run([path.join(__dirname, 'fixture/fail-fast.js')]) - .then(result => { - t.ok(api.options.failFast); - t.strictDeepEqual(tests, [{ - ok: true, - title: 'first pass' - }, { - ok: false, - title: 'second fail' - }]); - t.is(result.passCount, 1); - t.is(result.failCount, 1); + api.on('test-run', runStatus => { + runStatus.on('test', test => { + tests.push({ + ok: !test.error, + title: test.title }); + }); }); - test(`${prefix} serial execution mode`, t => { - const api = apiCreator({ - serial: true + return api.run([ + path.join(__dirname, 'fixture/fail-fast/multiple-files/fails.js'), + path.join(__dirname, 'fixture/fail-fast/multiple-files/passes-slow.js') + ]) + .then(result => { + t.ok(api.options.failFast); + t.strictDeepEqual(tests, [{ + ok: true, + title: `fails ${figures.pointerSmall} first pass` + }, { + ok: false, + title: `fails ${figures.pointerSmall} second fail` + }, { + ok: true, + title: `fails ${figures.pointerSmall} third pass` + }, { + ok: true, + title: `passes-slow ${figures.pointerSmall} first pass` + }]); + t.is(result.passCount, 3); + t.is(result.failCount, 1); }); +}); - return api.run([path.join(__dirname, 'fixture/serial.js')]) - .then(result => { - t.ok(api.options.serial); - t.is(result.passCount, 3); - t.is(result.failCount, 0); - }); +test('fail-fast mode - crash & serial', t => { + const api = apiCreator({ + failFast: true, + serial: true }); - test(`${prefix} circular references on assertions do not break process.send`, t => { - const api = apiCreator(); + const tests = []; + const errors = []; - return api.run([path.join(__dirname, 'fixture/circular-reference-on-assertion.js')]) - .then(result => { - t.is(result.failCount, 1); + api.on('test-run', runStatus => { + runStatus.on('test', test => { + tests.push({ + ok: !test.error, + title: test.title }); + }); + runStatus.on('error', err => { + errors.push(err); + }); }); - test(`${prefix} run from package.json folder by default`, t => { - const api = apiCreator(); + return api.run([ + path.join(__dirname, 'fixture/fail-fast/crash/crashes.js'), + path.join(__dirname, 'fixture/fail-fast/crash/passes.js') + ]) + .then(result => { + t.ok(api.options.failFast); + t.strictDeepEqual(tests, []); + t.is(errors.length, 1); + t.is(errors[0].name, 'AvaError'); + t.is(errors[0].message, `${path.join('test', 'fixture', 'fail-fast', 'crash', 'crashes.js')} exited with a non-zero exit code: 1`); + t.is(result.passCount, 0); + t.is(result.failCount, 0); + }); +}); - return api.run([path.join(__dirname, 'fixture/process-cwd-default.js')]) - .then(result => { - t.is(result.passCount, 1); - }); +test('fail-fast mode - timeout & serial', t => { + const api = apiCreator({ + failFast: true, + serial: true, + timeout: '100ms' }); - test(`${prefix} control worker's process.cwd() with projectDir option`, t => { - const fullPath = path.join(__dirname, 'fixture/process-cwd-pkgdir.js'); - const api = apiCreator({projectDir: path.dirname(fullPath)}); + const tests = []; + const errors = []; - return api.run([fullPath]) - .then(result => { - t.is(result.passCount, 1); + api.on('test-run', runStatus => { + runStatus.on('test', test => { + tests.push({ + ok: !test.error, + title: test.title }); + }); + runStatus.on('error', err => { + errors.push(err); + }); }); - test(`${prefix} unhandled promises will throw an error`, t => { - t.plan(3); + return api.run([ + path.join(__dirname, 'fixture/fail-fast/timeout/fails.js'), + path.join(__dirname, 'fixture/fail-fast/timeout/passes.js') + ]) + .then(result => { + t.ok(api.options.failFast); + t.strictDeepEqual(tests, []); + t.is(errors.length, 1); + t.is(errors[0].name, 'AvaError'); + t.is(errors[0].message, 'Exited because no new tests completed within the last 100ms of inactivity'); + t.is(result.passCount, 0); + t.is(result.failCount, 0); + }); +}); - const api = apiCreator(); +test('serial execution mode', t => { + const api = apiCreator({ + serial: true + }); - api.on('test-run', runStatus => { - runStatus.on('error', data => { - t.is(data.name, 'Error'); - t.match(data.message, /You can't handle this!/); - }); + return api.run([path.join(__dirname, 'fixture/serial.js')]) + .then(result => { + t.ok(api.options.serial); + t.is(result.passCount, 3); + t.is(result.failCount, 0); }); +}); - return api.run([path.join(__dirname, 'fixture/loud-rejection.js')]) - .then(result => { - t.is(result.passCount, 1); - }); - }); +test('circular references on assertions do not break process.send', t => { + const api = apiCreator(); - test(`${prefix} uncaught exception will throw an error`, t => { - t.plan(3); + return api.run([path.join(__dirname, 'fixture/circular-reference-on-assertion.js')]) + .then(result => { + t.is(result.failCount, 1); + }); +}); - const api = apiCreator(); +test('run from package.json folder by default', t => { + const api = apiCreator(); - api.on('test-run', runStatus => { - runStatus.on('error', data => { - t.is(data.name, 'Error'); - t.match(data.message, /Can't catch me!/); - }); + return api.run([path.join(__dirname, 'fixture/process-cwd-default.js')]) + .then(result => { + t.is(result.passCount, 1); }); +}); - return api.run([path.join(__dirname, 'fixture/uncaught-exception.js')]) - .then(result => { - t.is(result.passCount, 1); - }); - }); +test('control worker\'s process.cwd() with projectDir option', t => { + const fullPath = path.join(__dirname, 'fixture/process-cwd-pkgdir.js'); + const api = apiCreator({projectDir: path.dirname(fullPath)}); - test(`${prefix} errors can occur without messages`, t => { - const api = apiCreator(); + return api.run([fullPath]) + .then(result => { + t.is(result.passCount, 1); + }); +}); - return api.run([path.join(__dirname, 'fixture/error-without-message.js')]) - .then(result => { - t.is(result.failCount, 1); - t.is(result.errors.length, 1); - }); - }); +test('unhandled promises will throw an error', t => { + t.plan(3); - test(`${prefix} stack traces for exceptions are corrected using a source map file`, t => { - t.plan(4); + const api = apiCreator(); - const api = apiCreator({ - cacheEnabled: true + api.on('test-run', runStatus => { + runStatus.on('error', data => { + t.is(data.name, 'Error'); + t.match(data.message, /You can't handle this!/); }); + }); - api.on('test-run', runStatus => { - runStatus.on('error', data => { - t.match(data.message, /Thrown by source-map-fixtures/); - t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m); - t.match(data.stack, /^.*?Immediate\b.*source-map-file.js:4.*$/m); - }); + return api.run([path.join(__dirname, 'fixture/loud-rejection.js')]) + .then(result => { + t.is(result.passCount, 1); }); +}); - return api.run([path.join(__dirname, 'fixture/source-map-file.js')]) - .then(result => { - t.is(result.passCount, 1); - }); - }); +test('uncaught exception will throw an error', t => { + t.plan(3); - test(`${prefix} stack traces for exceptions are corrected using a source map file in what looks like a browser env`, t => { - t.plan(4); + const api = apiCreator(); - const api = apiCreator({ - cacheEnabled: true + api.on('test-run', runStatus => { + runStatus.on('error', data => { + t.is(data.name, 'Error'); + t.match(data.message, /Can't catch me!/); }); + }); - api.on('test-run', runStatus => { - runStatus.on('error', data => { - t.match(data.message, /Thrown by source-map-fixtures/); - t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m); - t.match(data.stack, /^.*?Immediate\b.*source-map-file-browser-env.js:7.*$/m); - }); + return api.run([path.join(__dirname, 'fixture/uncaught-exception.js')]) + .then(result => { + t.is(result.passCount, 1); }); +}); - return api.run([path.join(__dirname, 'fixture/source-map-file-browser-env.js')]) - .then(result => { - t.is(result.passCount, 1); - }); - }); +test('errors can occur without messages', t => { + const api = apiCreator(); - test(`${prefix} enhanced assertion formatting necessary whitespace and empty strings`, t => { - const expected = [ - [ - /foo === "" && "" === foo/, - /foo === ""/, - /foo/ - ], - [ - /new Object\(foo\) instanceof Object/, - /Object/, - /new Object\(foo\)/, - /foo/ - ], - [ - /\[foo].filter\(item => {\n\s+return item === "bar";\n}\).length > 0/, - /\[foo].filter\(item => {\n\s+return item === "bar";\n}\).length/, - /\[foo].filter\(item => {\n\s+return item === "bar";\n}\)/, - /\[foo]/, - /foo/ - ] - ]; - - t.plan(14); - const api = apiCreator(); - return api.run([path.join(__dirname, 'fixture/enhanced-assertion-formatting.js')]) - .then(result => { - t.is(result.errors.length, 3); - t.is(result.passCount, 0); + return api.run([path.join(__dirname, 'fixture/error-without-message.js')]) + .then(result => { + t.is(result.failCount, 1); + t.is(result.errors.length, 1); + }); +}); - result.errors.forEach((error, errorIndex) => { - error.error.statements.forEach((statement, statementIndex) => { - t.match(statement[0], expected[errorIndex][statementIndex]); - }); - }); - }); - }); +test('stack traces for exceptions are corrected using a source map file', t => { + t.plan(4); - test(`${prefix} stack traces for exceptions are corrected using a source map file (cache off)`, t => { - t.plan(4); + const api = apiCreator({ + cacheEnabled: true + }); - const api = apiCreator({ - cacheEnabled: false + api.on('test-run', runStatus => { + runStatus.on('error', data => { + t.match(data.message, /Thrown by source-map-fixtures/); + t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m); + t.match(data.stack, /^.*?Immediate\b.*source-map-file.js:4.*$/m); }); + }); - api.on('test-run', runStatus => { - runStatus.on('error', data => { - t.match(data.message, /Thrown by source-map-fixtures/); - t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m); - t.match(data.stack, /^.*?Immediate\b.*source-map-file.js:4.*$/m); - }); + return api.run([path.join(__dirname, 'fixture/source-map-file.js')]) + .then(result => { + t.is(result.passCount, 1); }); +}); - return api.run([path.join(__dirname, 'fixture/source-map-file.js')]) - .then(result => { - t.is(result.passCount, 1); - }); +test('stack traces for exceptions are corrected using a source map file in what looks like a browser env', t => { + t.plan(4); + + const api = apiCreator({ + cacheEnabled: true }); - test(`${prefix} stack traces for exceptions are corrected using a source map, taking an initial source map for the test file into account (cache on)`, t => { - t.plan(4); + api.on('test-run', runStatus => { + runStatus.on('error', data => { + t.match(data.message, /Thrown by source-map-fixtures/); + t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m); + t.match(data.stack, /^.*?Immediate\b.*source-map-file-browser-env.js:7.*$/m); + }); + }); - const api = apiCreator({ - cacheEnabled: true + return api.run([path.join(__dirname, 'fixture/source-map-file-browser-env.js')]) + .then(result => { + t.is(result.passCount, 1); }); +}); - api.on('test-run', runStatus => { - runStatus.on('error', data => { - t.match(data.message, /Thrown by source-map-fixtures/); - t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m); - t.match(data.stack, /^.*?Immediate\b.*source-map-initial-input.js:14.*$/m); +test('enhanced assertion formatting necessary whitespace and empty strings', t => { + const expected = [ + [ + /foo === "" && "" === foo/, + /foo === ""/, + /foo/ + ], + [ + /new Object\(foo\) instanceof Object/, + /Object/, + /new Object\(foo\)/, + /foo/ + ], + [ + /\[foo].filter\(item => {\n\s+return item === "bar";\n}\).length > 0/, + /\[foo].filter\(item => {\n\s+return item === "bar";\n}\).length/, + /\[foo].filter\(item => {\n\s+return item === "bar";\n}\)/, + /\[foo]/, + /foo/ + ] + ]; + + t.plan(14); + const api = apiCreator(); + return api.run([path.join(__dirname, 'fixture/enhanced-assertion-formatting.js')]) + .then(result => { + t.is(result.errors.length, 3); + t.is(result.passCount, 0); + + result.errors.forEach((error, errorIndex) => { + error.error.statements.forEach((statement, statementIndex) => { + t.match(statement[0], expected[errorIndex][statementIndex]); + }); }); }); +}); - return api.run([path.join(__dirname, 'fixture/source-map-initial.js')]) - .then(result => { - t.is(result.passCount, 1); - }); - }); +test('stack traces for exceptions are corrected using a source map file (cache off)', t => { + t.plan(4); - test(`${prefix} stack traces for exceptions are corrected using a source map, taking an initial source map for the test file into account (cache off)`, t => { - t.plan(4); + const api = apiCreator({ + cacheEnabled: false + }); - const api = apiCreator({ - cacheEnabled: false + api.on('test-run', runStatus => { + runStatus.on('error', data => { + t.match(data.message, /Thrown by source-map-fixtures/); + t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m); + t.match(data.stack, /^.*?Immediate\b.*source-map-file.js:4.*$/m); }); + }); - api.on('test-run', runStatus => { - runStatus.on('error', data => { - t.match(data.message, /Thrown by source-map-fixtures/); - t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m); - t.match(data.stack, /^.*?Immediate\b.*source-map-initial-input.js:14.*$/m); - }); + return api.run([path.join(__dirname, 'fixture/source-map-file.js')]) + .then(result => { + t.is(result.passCount, 1); }); +}); - return api.run([path.join(__dirname, 'fixture/source-map-initial.js')]) - .then(result => { - t.is(result.passCount, 1); - }); - }); - - test(`${prefix} absolute paths`, t => { - const api = apiCreator(); +test('stack traces for exceptions are corrected using a source map, taking an initial source map for the test file into account (cache on)', t => { + t.plan(4); - return api.run([path.resolve('test/fixture/es2015.js')]) - .then(result => { - t.is(result.passCount, 1); - }); + const api = apiCreator({ + cacheEnabled: true }); - test(`${prefix} symlink to directory containing test files`, t => { - const api = apiCreator(); - - return api.run([path.join(__dirname, 'fixture/symlink')]) - .then(result => { - t.is(result.passCount, 1); - }); + api.on('test-run', runStatus => { + runStatus.on('error', data => { + t.match(data.message, /Thrown by source-map-fixtures/); + t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m); + t.match(data.stack, /^.*?Immediate\b.*source-map-initial-input.js:14.*$/m); + }); }); - test(`${prefix} symlink to test file directly`, t => { - const api = apiCreator(); - - return api.run([path.join(__dirname, 'fixture/symlinkfile.js')]) - .then(result => { - t.is(result.passCount, 1); - }); - }); + return api.run([path.join(__dirname, 'fixture/source-map-initial.js')]) + .then(result => { + t.is(result.passCount, 1); + }); +}); - test(`${prefix} search directories recursively for files`, t => { - const api = apiCreator(); +test('stack traces for exceptions are corrected using a source map, taking an initial source map for the test file into account (cache off)', t => { + t.plan(4); - return api.run([path.join(__dirname, 'fixture/subdir')]) - .then(result => { - t.is(result.passCount, 2); - t.is(result.failCount, 1); - }); + const api = apiCreator({ + cacheEnabled: false }); - test(`${prefix} titles of both passing and failing tests and AssertionErrors are returned`, t => { - const api = apiCreator(); - - return api.run([path.join(__dirname, 'fixture/one-pass-one-fail.js')]) - .then(result => { - t.match(result.errors[0].title, /this is a failing test/); - t.match(result.tests[0].title, /this is a passing test/); - t.match(result.errors[0].error.name, /AssertionError/); - }); + api.on('test-run', runStatus => { + runStatus.on('error', data => { + t.match(data.message, /Thrown by source-map-fixtures/); + t.match(data.stack, /^.*?Object\.t.*?as run\b.*source-map-fixtures.src.throws.js:1.*$/m); + t.match(data.stack, /^.*?Immediate\b.*source-map-initial-input.js:14.*$/m); + }); }); - test(`${prefix} empty test files cause an AvaError to be emitted`, t => { - t.plan(2); + return api.run([path.join(__dirname, 'fixture/source-map-initial.js')]) + .then(result => { + t.is(result.passCount, 1); + }); +}); - const api = apiCreator(); +test('absolute paths', t => { + const api = apiCreator(); - api.on('test-run', runStatus => { - runStatus.on('error', err => { - t.is(err.name, 'AvaError'); - t.match(err.message, /No tests found.*?import "ava"/); - }); + return api.run([path.resolve('test/fixture/es2015.js')]) + .then(result => { + t.is(result.passCount, 1); }); +}); - return api.run([path.join(__dirname, 'fixture/empty.js')]); - }); +test('symlink to directory containing test files', t => { + const api = apiCreator(); - test(`${prefix} test file with no tests causes an AvaError to be emitted`, t => { - t.plan(2); + return api.run([path.join(__dirname, 'fixture/symlink')]) + .then(result => { + t.is(result.passCount, 1); + }); +}); - const api = apiCreator(); +test('symlink to test file directly', t => { + const api = apiCreator(); - api.on('test-run', runStatus => { - runStatus.on('error', err => { - t.is(err.name, 'AvaError'); - t.match(err.message, /No tests/); - }); + return api.run([path.join(__dirname, 'fixture/symlinkfile.js')]) + .then(result => { + t.is(result.passCount, 1); }); +}); - return api.run([path.join(__dirname, 'fixture/no-tests.js')]); - }); +test('search directories recursively for files', t => { + const api = apiCreator(); - test(`${prefix} test file that immediately exits with 0 exit code`, t => { - t.plan(2); + return api.run([path.join(__dirname, 'fixture/subdir')]) + .then(result => { + t.is(result.passCount, 2); + t.is(result.failCount, 1); + }); +}); - const api = apiCreator(); +test('titles of both passing and failing tests and AssertionErrors are returned', t => { + const api = apiCreator(); - api.on('test-run', runStatus => { - runStatus.on('error', err => { - t.is(err.name, 'AvaError'); - t.match(err.message, /Test results were not received from/); - }); + return api.run([path.join(__dirname, 'fixture/one-pass-one-fail.js')]) + .then(result => { + t.match(result.errors[0].title, /this is a failing test/); + t.match(result.tests[0].title, /this is a passing test/); + t.match(result.errors[0].error.name, /AssertionError/); }); +}); - return api.run([path.join(__dirname, 'fixture/immediate-0-exit.js')]); - }); - - test(`${prefix} test file that immediately exits with 3 exit code`, t => { - t.plan(3); +test('empty test files cause an AvaError to be emitted', t => { + t.plan(2); - const api = apiCreator(); + const api = apiCreator(); - api.on('test-run', runStatus => { - runStatus.on('error', err => { - t.is(err.name, 'AvaError'); - t.is(err.file, path.join('test', 'fixture', 'immediate-3-exit.js')); - t.match(err.message, /exited with a non-zero exit code: 3/); - }); + api.on('test-run', runStatus => { + runStatus.on('error', err => { + t.is(err.name, 'AvaError'); + t.match(err.message, /No tests found.*?import "ava"/); }); - - return api.run([path.join(__dirname, 'fixture/immediate-3-exit.js')]); }); - test(`${prefix} testing nonexistent files causes an AvaError to be emitted`, t => { - t.plan(2); + return api.run([path.join(__dirname, 'fixture/empty.js')]); +}); - const api = apiCreator(); +test('test file with no tests causes an AvaError to be emitted', t => { + t.plan(2); - api.on('test-run', runStatus => { - runStatus.on('error', err => { - t.is(err.name, 'AvaError'); - t.match(err.message, /Couldn't find any files to test/); - }); - }); + const api = apiCreator(); - return api.run([path.join(__dirname, 'fixture/broken.js')]); + api.on('test-run', runStatus => { + runStatus.on('error', err => { + t.is(err.name, 'AvaError'); + t.match(err.message, /No tests/); + }); }); - test(`${prefix} test file in node_modules is ignored`, t => { - t.plan(2); + return api.run([path.join(__dirname, 'fixture/no-tests.js')]); +}); - const api = apiCreator(); +test('test file that immediately exits with 0 exit code', t => { + t.plan(2); - api.on('test-run', runStatus => { - runStatus.on('error', err => { - t.is(err.name, 'AvaError'); - t.match(err.message, /Couldn't find any files to test/); - }); - }); + const api = apiCreator(); - return api.run([path.join(__dirname, 'fixture/ignored-dirs/node_modules/test.js')]); + api.on('test-run', runStatus => { + runStatus.on('error', err => { + t.is(err.name, 'AvaError'); + t.match(err.message, /Test results were not received from/); + }); }); - test(`${prefix} test file in fixtures is ignored`, t => { - t.plan(2); + return api.run([path.join(__dirname, 'fixture/immediate-0-exit.js')]); +}); - const api = apiCreator(); +test('test file that immediately exits with 3 exit code', t => { + t.plan(3); - api.on('test-run', runStatus => { - runStatus.on('error', err => { - t.is(err.name, 'AvaError'); - t.match(err.message, /Couldn't find any files to test/); - }); - }); + const api = apiCreator(); - return api.run([path.join(__dirname, 'fixture/ignored-dirs/fixtures/test.js')]); + api.on('test-run', runStatus => { + runStatus.on('error', err => { + t.is(err.name, 'AvaError'); + t.is(err.file, path.join('test', 'fixture', 'immediate-3-exit.js')); + t.match(err.message, /exited with a non-zero exit code: 3/); + }); }); - test(`${prefix} test file in helpers is ignored`, t => { - t.plan(2); + return api.run([path.join(__dirname, 'fixture/immediate-3-exit.js')]); +}); - const api = apiCreator(); +test('testing nonexistent files causes an AvaError to be emitted', t => { + t.plan(2); - api.on('test-run', runStatus => { - runStatus.on('error', err => { - t.is(err.name, 'AvaError'); - t.match(err.message, /Couldn't find any files to test/); - }); - }); + const api = apiCreator(); - return api.run([path.join(__dirname, 'fixture/ignored-dirs/helpers/test.js')]); + api.on('test-run', runStatus => { + runStatus.on('error', err => { + t.is(err.name, 'AvaError'); + t.match(err.message, /Couldn't find any files to test/); + }); }); - test(`${prefix} Node.js-style --require CLI argument`, t => { - const requirePath = './' + path.relative('.', path.join(__dirname, 'fixture/install-global.js')).replace(/\\/g, '/'); + return api.run([path.join(__dirname, 'fixture/broken.js')]); +}); - const api = apiCreator({ - require: [requirePath] - }); +test('test file in node_modules is ignored', t => { + t.plan(2); - return api.run([path.join(__dirname, 'fixture/validate-installed-global.js')]) - .then(result => { - t.is(result.passCount, 1); - }); - }); + const api = apiCreator(); - test(`${prefix} Node.js-style --require CLI argument module not found`, t => { - t.throws(() => { - /* eslint no-new: 0 */ - apiCreator({require: ['foo-bar']}); - }, /^Could not resolve required module 'foo-bar'$/); - t.end(); + api.on('test-run', runStatus => { + runStatus.on('error', err => { + t.is(err.name, 'AvaError'); + t.match(err.message, /Couldn't find any files to test/); + }); }); - test(`${prefix} caching is enabled by default`, t => { - del.sync(path.join(__dirname, 'fixture/caching/node_modules')); + return api.run([path.join(__dirname, 'fixture/ignored-dirs/node_modules/test.js')]); +}); + +test('test file in fixtures is ignored', t => { + t.plan(2); + + const api = apiCreator(); - const api = apiCreator({ - resolveTestsFrom: path.join(__dirname, 'fixture/caching') + api.on('test-run', runStatus => { + runStatus.on('error', err => { + t.is(err.name, 'AvaError'); + t.match(err.message, /Couldn't find any files to test/); }); + }); - return api.run([path.join(__dirname, 'fixture/caching/test.js')]) - .then(() => { - const files = fs.readdirSync(path.join(__dirname, 'fixture/caching/node_modules/.cache/ava')); - t.ok(files.length, 4); - t.is(files.filter(x => endsWithBin(x)).length, 1); - t.is(files.filter(x => endsWithJs(x)).length, 2); - t.is(files.filter(x => endsWithMap(x)).length, 1); - }); + return api.run([path.join(__dirname, 'fixture/ignored-dirs/fixtures/test.js')]); +}); - function endsWithBin(filename) { - return /\.bin$/.test(filename); - } +test('test file in helpers is ignored', t => { + t.plan(2); - function endsWithJs(filename) { - return /\.js$/.test(filename); - } + const api = apiCreator(); - function endsWithMap(filename) { - return /\.map$/.test(filename); - } + api.on('test-run', runStatus => { + runStatus.on('error', err => { + t.is(err.name, 'AvaError'); + t.match(err.message, /Couldn't find any files to test/); + }); }); - test(`${prefix} caching can be disabled`, t => { - del.sync(path.join(__dirname, 'fixture/caching/node_modules')); + return api.run([path.join(__dirname, 'fixture/ignored-dirs/helpers/test.js')]); +}); - const api = apiCreator({ - resolveTestsFrom: path.join(__dirname, 'fixture/caching'), - cacheEnabled: false - }); +test('Node.js-style --require CLI argument', t => { + const requirePath = './' + path.relative('.', path.join(__dirname, 'fixture/install-global.js')).replace(/\\/g, '/'); - return api.run([path.join(__dirname, 'fixture/caching/test.js')]) - .then(() => { - t.false(fs.existsSync(path.join(__dirname, 'fixture/caching/node_modules/.cache/ava'))); - }); + const api = apiCreator({ + require: [requirePath] }); - test(`${prefix} test file with only skipped tests does not create a failure`, t => { - const api = apiCreator(); + return api.run([path.join(__dirname, 'fixture/validate-installed-global.js')]) + .then(result => { + t.is(result.passCount, 1); + }); +}); - return api.run([path.join(__dirname, 'fixture/skip-only.js')]) - .then(result => { - t.is(result.tests.length, 1); - t.true(result.tests[0].skip); - }); - }); +test('Node.js-style --require CLI argument module not found', t => { + t.throws(() => { + /* eslint no-new: 0 */ + apiCreator({require: ['foo-bar']}); + }, /^Could not resolve required module 'foo-bar'$/); + t.end(); +}); - test(`${prefix} test file with only skipped tests does not run hooks`, t => { - const api = apiCreator(); +test('caching is enabled by default', t => { + del.sync(path.join(__dirname, 'fixture/caching/node_modules')); - return api.run([path.join(__dirname, 'fixture/hooks-skipped.js')]) - .then(result => { - t.is(result.tests.length, 1); - t.is(result.skipCount, 1); - t.is(result.passCount, 0); - t.is(result.failCount, 0); - }); + const api = apiCreator({ + projectDir: path.join(__dirname, 'fixture/caching') }); - test(`${prefix} resets state before running`, t => { - const api = apiCreator(); - - return api.run([path.resolve('test/fixture/es2015.js')]).then(result => { - t.is(result.passCount, 1); - return api.run([path.resolve('test/fixture/es2015.js')]); - }).then(result => { - t.is(result.passCount, 1); + return api.run([path.join(__dirname, 'fixture/caching/test.js')]) + .then(() => { + const files = fs.readdirSync(path.join(__dirname, 'fixture/caching/node_modules/.cache/ava')); + t.ok(files.length, 4); + t.is(files.filter(x => endsWithBin(x)).length, 1); + t.is(files.filter(x => endsWithJs(x)).length, 2); + t.is(files.filter(x => endsWithMap(x)).length, 1); }); - }); - test(`${prefix} emits dependencies for test files`, t => { - t.plan(8); + function endsWithBin(filename) { + return /\.bin$/.test(filename); + } - const api = apiCreator({ - require: [path.resolve('test/fixture/with-dependencies/require-custom.js')] - }); + function endsWithJs(filename) { + return /\.js$/.test(filename); + } - const testFiles = [ - path.normalize('test/fixture/with-dependencies/no-tests.js'), - path.normalize('test/fixture/with-dependencies/test.js'), - path.normalize('test/fixture/with-dependencies/test-failure.js'), - path.normalize('test/fixture/with-dependencies/test-uncaught-exception.js') - ]; + function endsWithMap(filename) { + return /\.map$/.test(filename); + } +}); - const sourceFiles = [ - path.resolve('test/fixture/with-dependencies/dep-1.js'), - path.resolve('test/fixture/with-dependencies/dep-2.js'), - path.resolve('test/fixture/with-dependencies/dep-3.custom') - ]; +test('caching can be disabled', t => { + del.sync(path.join(__dirname, 'fixture/caching/node_modules')); - api.on('test-run', runStatus => { - runStatus.on('dependencies', (file, dependencies) => { - t.notEqual(testFiles.indexOf(file), -1); - t.strictDeepEqual(dependencies.slice(-3), sourceFiles); - }); + const api = apiCreator({ + resolveTestsFrom: path.join(__dirname, 'fixture/caching'), + cacheEnabled: false + }); - // The test files are designed to cause errors so ignore them here. - runStatus.on('error', () => {}); + return api.run([path.join(__dirname, 'fixture/caching/test.js')]) + .then(() => { + t.false(fs.existsSync(path.join(__dirname, 'fixture/caching/node_modules/.cache/ava'))); }); +}); - const result = api.run(['test/fixture/with-dependencies/*test*.js']); +test('test file with only skipped tests does not create a failure', t => { + const api = apiCreator(); - return result.catch(() => {}); - }); + return api.run([path.join(__dirname, 'fixture/skip-only.js')]) + .then(result => { + t.is(result.tests.length, 1); + t.true(result.tests[0].skip); + }); +}); - test(`${prefix} emits stats for test files`, t => { - t.plan(2); +test('test file with only skipped tests does not run hooks', t => { + const api = apiCreator(); - const api = apiCreator(); - api.on('test-run', runStatus => { - runStatus.on('stats', stats => { - if (stats.file === path.normalize('test/fixture/exclusive.js')) { - t.is(stats.hasExclusive, true); - } else if (stats.file === path.normalize('test/fixture/generators.js')) { - t.is(stats.hasExclusive, false); - } else { - t.ok(false); - } - }); + return api.run([path.join(__dirname, 'fixture/hooks-skipped.js')]) + .then(result => { + t.is(result.tests.length, 1); + t.is(result.skipCount, 1); + t.is(result.passCount, 0); + t.is(result.failCount, 0); }); +}); - return api.run([ - 'test/fixture/exclusive.js', - 'test/fixture/generators.js' - ]); +test('resets state before running', t => { + const api = apiCreator(); + + return api.run([path.resolve('test/fixture/es2015.js')]).then(result => { + t.is(result.passCount, 1); + return api.run([path.resolve('test/fixture/es2015.js')]); + }).then(result => { + t.is(result.passCount, 1); }); +}); - test(`${prefix} verify test count`, t => { - t.plan(8); +test('emits dependencies for test files', t => { + t.plan(8); - const api = apiCreator(); + const api = apiCreator({ + require: [path.resolve('test/fixture/with-dependencies/require-custom.js')] + }); - api.on('test-run', runStatus => { - t.is(runStatus.passCount, 0); - t.is(runStatus.failCount, 0); - t.is(runStatus.skipCount, 0); - t.is(runStatus.todoCount, 0); + const testFiles = [ + path.normalize('test/fixture/with-dependencies/no-tests.js'), + path.normalize('test/fixture/with-dependencies/test.js'), + path.normalize('test/fixture/with-dependencies/test-failure.js'), + path.normalize('test/fixture/with-dependencies/test-uncaught-exception.js') + ]; + + const sourceFiles = [ + path.resolve('test/fixture/with-dependencies/dep-1.js'), + path.resolve('test/fixture/with-dependencies/dep-2.js'), + path.resolve('test/fixture/with-dependencies/dep-3.custom') + ]; + + api.on('test-run', runStatus => { + runStatus.on('dependencies', (file, dependencies) => { + t.notEqual(testFiles.indexOf(file), -1); + t.strictDeepEqual(dependencies.slice(-3), sourceFiles); }); - return api.run([ - path.join(__dirname, 'fixture/test-count.js'), - path.join(__dirname, 'fixture/test-count-2.js'), - path.join(__dirname, 'fixture/test-count-3.js') - ]).then(result => { - t.is(result.passCount, 4, 'pass count'); - t.is(result.failCount, 3, 'fail count'); - t.is(result.skipCount, 3, 'skip count'); - t.is(result.todoCount, 3, 'todo count'); - }); + // The test files are designed to cause errors so ignore them here. + runStatus.on('error', () => {}); }); - test(`${prefix} babel.testOptions with a custom plugin`, t => { - t.plan(2); + const result = api.run(['test/fixture/with-dependencies/*test*.js']); - const api = apiCreator({ - babelConfig: { - testOptions: { - plugins: [testCapitalizerPlugin] - } - }, - cacheEnabled: false, - projectDir: __dirname - }); + return result.catch(() => {}); +}); - api.on('test-run', runStatus => { - runStatus.on('test', data => { - t.is(data.title, 'FOO'); - }); +test('emits stats for test files', t => { + t.plan(2); + + const api = apiCreator(); + api.on('test-run', runStatus => { + runStatus.on('stats', stats => { + if (stats.file === path.normalize('test/fixture/exclusive.js')) { + t.is(stats.hasExclusive, true); + } else if (stats.file === path.normalize('test/fixture/generators.js')) { + t.is(stats.hasExclusive, false); + } else { + t.ok(false); + } }); - - return api.run([path.join(__dirname, 'fixture/babelrc/test.js')]) - .then(result => { - t.is(result.passCount, 1); - }, t.threw); }); - test(`${prefix} babel.testOptions.babelrc effectively defaults to true`, t => { - t.plan(3); + return api.run([ + 'test/fixture/exclusive.js', + 'test/fixture/generators.js' + ]); +}); - const api = apiCreator({ - projectDir: path.join(__dirname, 'fixture/babelrc') - }); +test('verify test count', t => { + t.plan(8); - api.on('test-run', runStatus => { - runStatus.on('test', data => { - t.ok((data.title === 'foo') || (data.title === 'repeated test: foo')); - }); - }); + const api = apiCreator(); - return api.run() - .then(result => { - t.is(result.passCount, 2); - }); + api.on('test-run', runStatus => { + t.is(runStatus.passCount, 0); + t.is(runStatus.failCount, 0); + t.is(runStatus.skipCount, 0); + t.is(runStatus.todoCount, 0); }); - test(`${prefix} babel.testOptions.babelrc can explicitly be true`, t => { - t.plan(3); + return api.run([ + path.join(__dirname, 'fixture/test-count.js'), + path.join(__dirname, 'fixture/test-count-2.js'), + path.join(__dirname, 'fixture/test-count-3.js') + ]).then(result => { + t.is(result.passCount, 4, 'pass count'); + t.is(result.failCount, 3, 'fail count'); + t.is(result.skipCount, 3, 'skip count'); + t.is(result.todoCount, 3, 'todo count'); + }); +}); - const api = apiCreator({ - babelConfig: { - testOptions: {babelrc: true} - }, - cacheEnabled: false, - projectDir: path.join(__dirname, 'fixture/babelrc') - }); +test('babel.testOptions with a custom plugin', t => { + t.plan(2); + + const api = apiCreator({ + babelConfig: { + testOptions: { + plugins: [testCapitalizerPlugin] + } + }, + cacheEnabled: false, + projectDir: __dirname + }); - api.on('test-run', runStatus => { - runStatus.on('test', data => { - t.ok(data.title === 'foo' || data.title === 'repeated test: foo'); - }); + api.on('test-run', runStatus => { + runStatus.on('test', data => { + t.is(data.title, 'FOO'); }); - - return api.run() - .then(result => { - t.is(result.passCount, 2); - }); }); - test(`${prefix} babel.testOptions.babelrc can explicitly be false`, t => { - t.plan(2); + return api.run([path.join(__dirname, 'fixture/babelrc/test.js')]) + .then(result => { + t.is(result.passCount, 1); + }, t.threw); +}); + +test('babel.testOptions.babelrc effectively defaults to true', t => { + t.plan(3); + + const api = apiCreator({ + projectDir: path.join(__dirname, 'fixture/babelrc') + }); - const api = apiCreator({ - babelConfig: { - testOptions: {babelrc: false} - }, - cacheEnabled: false, - projectDir: path.join(__dirname, 'fixture/babelrc') + api.on('test-run', runStatus => { + runStatus.on('test', data => { + t.ok((data.title === 'foo') || (data.title === 'repeated test: foo')); }); + }); - api.on('test-run', runStatus => { - runStatus.on('test', data => { - t.is(data.title, 'foo'); - }); + return api.run() + .then(result => { + t.is(result.passCount, 2); }); +}); - return api.run() - .then(result => { - t.is(result.passCount, 1); - }); - }); +test('babel.testOptions.babelrc can explicitly be true', t => { + t.plan(3); - test(`${prefix} babelConfig.testOptions merges plugins with .babelrc`, t => { - t.plan(3); + const api = apiCreator({ + babelConfig: { + testOptions: {babelrc: true} + }, + cacheEnabled: false, + projectDir: path.join(__dirname, 'fixture/babelrc') + }); - const api = apiCreator({ - babelConfig: { - testOptions: { - babelrc: true, - plugins: [testCapitalizerPlugin] - } - }, - cacheEnabled: false, - projectDir: path.join(__dirname, 'fixture/babelrc') + api.on('test-run', runStatus => { + runStatus.on('test', data => { + t.ok(data.title === 'foo' || data.title === 'repeated test: foo'); }); + }); - api.on('test-run', runStatus => { - runStatus.on('test', data => { - t.ok(data.title === 'FOO' || data.title === 'repeated test: foo'); - }); + return api.run() + .then(result => { + t.is(result.passCount, 2); }); +}); - return api.run() - .then(result => { - t.is(result.passCount, 2); - }); - }); +test('babel.testOptions.babelrc can explicitly be false', t => { + t.plan(2); - test(`${prefix} babelConfig.testOptions with extends still merges plugins with .babelrc`, t => { - t.plan(3); + const api = apiCreator({ + babelConfig: { + testOptions: {babelrc: false} + }, + cacheEnabled: false, + projectDir: path.join(__dirname, 'fixture/babelrc') + }); - const api = apiCreator({ - babelConfig: { - testOptions: { - plugins: [testCapitalizerPlugin], - extends: path.join(__dirname, 'fixture/babelrc/.alt-babelrc') - } - }, - cacheEnabled: false, - projectDir: path.join(__dirname, 'fixture/babelrc') + api.on('test-run', runStatus => { + runStatus.on('test', data => { + t.is(data.title, 'foo'); }); + }); - api.on('test-run', runStatus => { - runStatus.on('test', data => { - t.ok(data.title === 'BAR' || data.title === 'repeated test: bar'); - }); + return api.run() + .then(result => { + t.is(result.passCount, 1); }); +}); - return api.run() - .then(result => { - t.is(result.passCount, 2); - }); +test('babelConfig.testOptions merges plugins with .babelrc', t => { + t.plan(3); + + const api = apiCreator({ + babelConfig: { + testOptions: { + babelrc: true, + plugins: [testCapitalizerPlugin] + } + }, + cacheEnabled: false, + projectDir: path.join(__dirname, 'fixture/babelrc') }); - test(`${prefix} using --match with no matching tests causes an AvaError to be emitted`, t => { - t.plan(2); - - const api = apiCreator({ - match: ['can\'t match this'] + api.on('test-run', runStatus => { + runStatus.on('test', data => { + t.ok(data.title === 'FOO' || data.title === 'repeated test: foo'); }); + }); - api.on('test-run', runStatus => { - runStatus.on('test', data => { - t.fail(`Unexpected test run: ${data.title}`); - }); - runStatus.on('error', err => { - t.is(err.name, 'AvaError'); - t.match(err.message, /Couldn't find any matching tests/); - }); + return api.run() + .then(result => { + t.is(result.passCount, 2); }); +}); - return api.run([ - path.join(__dirname, 'fixture/match-no-match.js'), - path.join(__dirname, 'fixture/match-no-match-2.js'), - path.join(__dirname, 'fixture/test-count.js') - ]); +test('babelConfig.testOptions with extends still merges plugins with .babelrc', t => { + t.plan(3); + + const api = apiCreator({ + babelConfig: { + testOptions: { + plugins: [testCapitalizerPlugin], + extends: path.join(__dirname, 'fixture/babelrc/.alt-babelrc') + } + }, + cacheEnabled: false, + projectDir: path.join(__dirname, 'fixture/babelrc') }); - test(`${prefix} using --match with matching tests will only report those passing tests`, t => { - t.plan(2); - - const api = apiCreator({ - match: ['this test will match'] + api.on('test-run', runStatus => { + runStatus.on('test', data => { + t.ok(data.title === 'BAR' || data.title === 'repeated test: bar'); }); + }); - api.on('test-run', runStatus => { - runStatus.on('test', data => { - t.match(data.title, /^match-no-match-2 .+ this test will match$/); - }); - runStatus.on('error', err => { - t.fail(`Unexpected failure: ${err}`); - }); + return api.run() + .then(result => { + t.is(result.passCount, 2); }); +}); - return api.run([ - path.join(__dirname, 'fixture/match-no-match.js'), - path.join(__dirname, 'fixture/match-no-match-2.js'), - path.join(__dirname, 'fixture/test-count.js') - ]).then(result => { - t.is(result.passCount, 1); - }).catch(() => { - t.fail(); +test('using --match with no matching tests causes an AvaError to be emitted', t => { + t.plan(2); + + const api = apiCreator({ + match: ['can\'t match this'] + }); + + api.on('test-run', runStatus => { + runStatus.on('test', data => { + t.fail(`Unexpected test run: ${data.title}`); + }); + runStatus.on('error', err => { + t.is(err.name, 'AvaError'); + t.match(err.message, /Couldn't find any matching tests/); }); }); - test(`${prefix} errors thrown when running files are emitted`, t => { - t.plan(3); + return api.run([ + path.join(__dirname, 'fixture/match-no-match.js'), + path.join(__dirname, 'fixture/match-no-match-2.js'), + path.join(__dirname, 'fixture/test-count.js') + ]); +}); - const api = apiCreator(); +test('using --match with matching tests will only report those passing tests', t => { + t.plan(2); - api.on('test-run', runStatus => { - runStatus.on('error', err => { - t.is(err.name, 'SyntaxError'); - t.is(err.file, path.join('test', 'fixture', 'syntax-error.js')); - t.match(err.message, /Unexpected token/); - }); + const api = apiCreator({ + match: ['this test will match'] + }); + + api.on('test-run', runStatus => { + runStatus.on('test', data => { + t.match(data.title, /^match-no-match-2 .+ this test will match$/); + }); + runStatus.on('error', err => { + t.fail(`Unexpected failure: ${err}`); }); + }); - return api.run([ - path.join(__dirname, 'fixture/es2015.js'), - path.join(__dirname, 'fixture/syntax-error.js') - ]); + return api.run([ + path.join(__dirname, 'fixture/match-no-match.js'), + path.join(__dirname, 'fixture/match-no-match-2.js'), + path.join(__dirname, 'fixture/test-count.js') + ]).then(result => { + t.is(result.passCount, 1); + }).catch(() => { + t.fail(); }); -} +}); + +test('errors thrown when running files are emitted', t => { + t.plan(3); + + const api = apiCreator(); + + api.on('test-run', runStatus => { + runStatus.on('error', err => { + t.is(err.name, 'SyntaxError'); + t.is(err.file, path.join('test', 'fixture', 'syntax-error.js')); + t.match(err.message, /Unexpected token/); + }); + }); + + return api.run([ + path.join(__dirname, 'fixture/es2015.js'), + path.join(__dirname, 'fixture/syntax-error.js') + ]); +}); function generatePassDebugTests(execArgv, expectedInspectIndex) { test(`pass ${execArgv.join(' ')} to fork`, t => { const api = apiCreator({testOnlyExecArgv: execArgv}); - return api._computeForkExecArgs(['foo.js']) + return api._computeForkExecArgv() .then(result => { - t.true(result.length === 1); + t.true(result.length === execArgv.length); if (expectedInspectIndex === -1) { - t.true(result[0].length === 1); - t.true(/--debug=\d+/.test(result[0][0])); + t.true(/--debug=\d+/.test(result[0])); } else { - t.true(/--inspect=\d+/.test(result[0][expectedInspectIndex])); + t.true(/--inspect=\d+/.test(result[expectedInspectIndex])); } }); }); diff --git a/test/assert.js b/test/assert.js index 4edfdcfe0..4336ff67b 100644 --- a/test/assert.js +++ b/test/assert.js @@ -1,5 +1,5 @@ 'use strict'; -require('../lib/globals').options.color = false; +require('../lib/worker-options').set({color: false}); const path = require('path'); const stripAnsi = require('strip-ansi'); diff --git a/test/beautify-stack.js b/test/beautify-stack.js index b88e0ba5d..d23ac0186 100644 --- a/test/beautify-stack.js +++ b/test/beautify-stack.js @@ -1,7 +1,9 @@ 'use strict'; +require('../lib/worker-options').set({}); + const proxyquire = require('proxyquire').noPreserveCache(); const test = require('tap').test; -const Sequence = require('../lib/sequence'); +const Runner = require('../lib/runner'); const beautifyStack = proxyquire('../lib/beautify-stack', { debug() { @@ -56,20 +58,20 @@ test('returns empty string without any arguments', t => { test('beautify stack - removes uninteresting lines', t => { try { - const seq = new Sequence([{ + const runner = new Runner(); + runner.runSingle({ run() { fooFunc(); } - }]); - seq.run(); + }); } catch (err) { const stack = beautifyStack(err.stack); t.match(stack, /fooFunc/); t.match(stack, /barFunc/); - // The runNext line is introduced by Sequence. It's internal so it should + // The runSingle line is introduced by Runner. It's internal so it should // be stripped. - t.match(err.stack, /runNext/); - t.notMatch(stack, /runNext/); + t.match(err.stack, /runSingle/); + t.notMatch(stack, /runSingle/); t.end(); } }); diff --git a/test/cli.js b/test/cli.js index 3edbfb299..07652fc39 100644 --- a/test/cli.js +++ b/test/cli.js @@ -199,7 +199,7 @@ test('improper use of t.throws, even if caught and then rethrown too slowly, wil }); }); -test('babel require hook only does not apply to source files', t => { +test('precompiler require hook does not apply to source files', t => { t.plan(3); execCli('fixture/babel-hook.js', (err, stdout, stderr) => { diff --git a/test/concurrent.js b/test/concurrent.js deleted file mode 100644 index 78edd3178..000000000 --- a/test/concurrent.js +++ /dev/null @@ -1,790 +0,0 @@ -'use strict'; -const tap = require('tap'); -const isPromise = require('is-promise'); -const Concurrent = require('../lib/concurrent'); - -let results = []; -const test = (name, fn) => { - tap.test(name, t => { - results = []; - return fn(t); - }); -}; -function collect(result) { - if (isPromise(result)) { - return result.then(collect); - } - - results.push(result); - return result.passed; -} - -function pass(val) { - return { - run() { - return collect({ - passed: true, - result: val - }); - } - }; -} - -function fail(val) { - return { - run() { - return collect({ - passed: false, - reason: val - }); - } - }; -} - -function failWithTypeError() { - return { - run() { - throw new TypeError('Unexpected Error'); - } - }; -} - -function passAsync(val) { - return { - run() { - return collect(Promise.resolve({ - passed: true, - result: val - })); - } - }; -} - -function failAsync(err) { - return { - run() { - return collect(Promise.resolve({ - passed: false, - reason: err - })); - } - }; -} - -function reject(err) { - return { - run() { - return Promise.reject(err); - } - }; -} - -test('all sync - all pass - no bail', t => { - const passed = new Concurrent( - [ - pass('a'), - pass('b'), - pass('c') - ], - false - ).run(); - - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); -}); - -test('all sync - no failure - bail', t => { - const passed = new Concurrent( - [ - pass('a'), - pass('b'), - pass('c') - ], - true - ).run(); - - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); -}); - -test('all sync - begin failure - no bail', t => { - const passed = new Concurrent( - [ - fail('a'), - pass('b'), - pass('c') - ], - false - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: false, - reason: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); -}); - -test('all sync - mid failure - no bail', t => { - const passed = new Concurrent( - [ - pass('a'), - fail('b'), - pass('c') - ], - false - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: false, - reason: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); -}); - -test('all sync - end failure - no bail', t => { - const passed = new Concurrent( - [ - pass('a'), - pass('b'), - fail('c') - ], - false - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: false, - reason: 'c' - } - ]); - t.end(); -}); - -test('all sync - multiple failure - no bail', t => { - const passed = new Concurrent( - [ - fail('a'), - pass('b'), - fail('c') - ], - false - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: false, - reason: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: false, - reason: 'c' - } - ]); - t.end(); -}); - -test('all sync - begin failure - bail', t => { - const passed = new Concurrent( - [ - fail('a'), - pass('b'), - pass('c') - ], - true - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: false, - reason: 'a' - } - ]); - t.end(); -}); - -test('all sync - mid failure - bail', t => { - const passed = new Concurrent( - [ - pass('a'), - fail('b'), - pass('c') - ], - true - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: false, - reason: 'b' - } - ]); - t.end(); -}); - -test('all sync - end failure - bail', t => { - const passed = new Concurrent( - [ - pass('a'), - pass('b'), - fail('c') - ], - true - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: false, - reason: 'c' - } - ]); - t.end(); -}); - -test('all async - no failure - no bail', t => { - return new Concurrent( - [ - passAsync('a'), - passAsync('b'), - passAsync('c') - ], - false - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - }); -}); - -test('all async - no failure - bail', t => { - return new Concurrent( - [ - passAsync('a'), - passAsync('b'), - passAsync('c') - ], - true - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - }); -}); - -test('last async - no failure - no bail', t => { - return new Concurrent( - [ - pass('a'), - pass('b'), - passAsync('c') - ], - false - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - }); -}); - -test('mid async - no failure - no bail', t => { - return new Concurrent( - [ - pass('a'), - passAsync('b'), - pass('c') - ], - false - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'c' - }, - { - passed: true, - result: 'b' - } - ]); - }); -}); - -test('first async - no failure - no bail', t => { - return new Concurrent( - [ - passAsync('a'), - pass('b'), - pass('c') - ], - false - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - }, - { - passed: true, - result: 'a' - } - ]); - }); -}); - -test('last async - no failure - bail', t => { - return new Concurrent( - [ - pass('a'), - pass('b'), - passAsync('c') - ], - true - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - }); -}); - -test('mid async - no failure - bail', t => { - return new Concurrent( - [ - pass('a'), - passAsync('b'), - pass('c') - ], - true - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'c' - }, - { - passed: true, - result: 'b' - } - ]); - }); -}); - -test('first async - no failure - bail', t => { - return new Concurrent( - [ - passAsync('a'), - pass('b'), - pass('c') - ], - true - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - }, - { - passed: true, - result: 'a' - } - ]); - }); -}); - -test('all async - begin failure - bail', t => { - return new Concurrent( - [ - failAsync('a'), - passAsync('b'), - passAsync('c') - ], - true - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: false, - reason: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - }); -}); - -test('all async - mid failure - bail', t => { - return new Concurrent( - [ - passAsync('a'), - failAsync('b'), - passAsync('c') - ], - true - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: false, - reason: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - }); -}); - -test('all async - end failure - bail', t => { - return new Concurrent( - [ - passAsync('a'), - passAsync('b'), - failAsync('c') - ], - true - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: false, - reason: 'c' - } - ]); - }); -}); - -test('all async - begin failure - no bail', t => { - return new Concurrent( - [ - failAsync('a'), - passAsync('b'), - passAsync('c') - ], - false - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: false, - reason: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - }); -}); - -test('all async - mid failure - no bail', t => { - return new Concurrent( - [ - passAsync('a'), - failAsync('b'), - passAsync('c') - ], - false - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: false, - reason: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - }); -}); - -test('all async - end failure - no bail', t => { - return new Concurrent( - [ - passAsync('a'), - passAsync('b'), - failAsync('c') - ], - false - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: false, - reason: 'c' - } - ]); - }); -}); - -test('all async - multiple failure - no bail', t => { - return new Concurrent( - [ - failAsync('a'), - passAsync('b'), - failAsync('c') - ], - false - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: false, - reason: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: false, - reason: 'c' - } - ]); - }); -}); - -test('rejections are just passed through - no bail', t => { - return new Concurrent( - [ - pass('a'), - pass('b'), - reject('foo') - ], - false - ).run().catch(err => { - t.is(err, 'foo'); - }); -}); - -test('rejections are just passed through - bail', t => { - return new Concurrent( - [ - pass('a'), - pass('b'), - reject('foo') - ], - true - ).run().catch(err => { - t.is(err, 'foo'); - }); -}); - -test('sequences of sequences', t => { - const passed = new Concurrent([ - new Concurrent([pass('a'), pass('b')]), - new Concurrent([pass('c')]) - ]).run(); - - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - - t.end(); -}); - -test('must be called with array of runnables', t => { - t.throws(() => { - new Concurrent(pass('a')).run(); - }, {message: 'Expected an array of runnables'}); - t.end(); -}); - -test('should throw an error then test.run() fails with not AvaError', t => { - t.throws(() => { - new Concurrent([failWithTypeError()]).run(); - }, {message: 'Unexpected Error'}); - t.end(); -}); diff --git a/test/fixture/fail-fast/crash/crashes.js b/test/fixture/fail-fast/crash/crashes.js new file mode 100644 index 000000000..fe046b460 --- /dev/null +++ b/test/fixture/fail-fast/crash/crashes.js @@ -0,0 +1,3 @@ +import '../../../../'; // eslint-disable-line import/no-unassigned-import + +process.exit(1); // eslint-disable-line unicorn/no-process-exit diff --git a/test/fixture/fail-fast/crash/passes.js b/test/fixture/fail-fast/crash/passes.js new file mode 100644 index 000000000..fe55dc3d0 --- /dev/null +++ b/test/fixture/fail-fast/crash/passes.js @@ -0,0 +1,5 @@ +import test from '../../../../'; + +test('first pass', t => { + t.pass(); +}); diff --git a/test/fixture/fail-fast.js b/test/fixture/fail-fast/multiple-files/fails.js similarity index 79% rename from test/fixture/fail-fast.js rename to test/fixture/fail-fast/multiple-files/fails.js index add61f7fb..83c8bc219 100644 --- a/test/fixture/fail-fast.js +++ b/test/fixture/fail-fast/multiple-files/fails.js @@ -1,4 +1,4 @@ -import test from '../../'; +import test from '../../../../'; test('first pass', t => { t.pass(); diff --git a/test/fixture/fail-fast/multiple-files/passes-slow.js b/test/fixture/fail-fast/multiple-files/passes-slow.js new file mode 100644 index 000000000..3464a4216 --- /dev/null +++ b/test/fixture/fail-fast/multiple-files/passes-slow.js @@ -0,0 +1,14 @@ +import test from '../../../../'; + +test.serial('first pass', async t => { + t.pass(); + return new Promise(resolve => setTimeout(resolve, 3000)); +}); + +test.serial('second pass', t => { + t.pass(); +}); + +test('third pass', t => { + t.pass(); +}); diff --git a/test/fixture/fail-fast/multiple-files/passes.js b/test/fixture/fail-fast/multiple-files/passes.js new file mode 100644 index 000000000..fe55dc3d0 --- /dev/null +++ b/test/fixture/fail-fast/multiple-files/passes.js @@ -0,0 +1,5 @@ +import test from '../../../../'; + +test('first pass', t => { + t.pass(); +}); diff --git a/test/fixture/fail-fast/single-file/test.js b/test/fixture/fail-fast/single-file/test.js new file mode 100644 index 000000000..83c8bc219 --- /dev/null +++ b/test/fixture/fail-fast/single-file/test.js @@ -0,0 +1,13 @@ +import test from '../../../../'; + +test('first pass', t => { + t.pass(); +}); + +test('second fail', t => { + t.fail(); +}); + +test('third pass', t => { + t.pass(); +}); diff --git a/test/fixture/fail-fast/timeout/fails.js b/test/fixture/fail-fast/timeout/fails.js new file mode 100644 index 000000000..cd9545d57 --- /dev/null +++ b/test/fixture/fail-fast/timeout/fails.js @@ -0,0 +1,5 @@ +import test from '../../../../'; + +test.cb('slow pass', t => { + setTimeout(t.end, 1000); +}); diff --git a/test/fixture/fail-fast/timeout/passes.js b/test/fixture/fail-fast/timeout/passes.js new file mode 100644 index 000000000..fe55dc3d0 --- /dev/null +++ b/test/fixture/fail-fast/timeout/passes.js @@ -0,0 +1,5 @@ +import test from '../../../../'; + +test('first pass', t => { + t.pass(); +}); diff --git a/test/fork.js b/test/fork.js index 86cf966b7..c0e7e339f 100644 --- a/test/fork.js +++ b/test/fork.js @@ -35,7 +35,6 @@ test('emits test event', t => { t.plan(1); fork(fixture('generators.js')) - .run({}) .on('test', tt => { t.is(tt.title, 'generator function'); t.end(); @@ -48,7 +47,6 @@ test('resolves promise with tests info', t => { const file = fixture('generators.js'); return fork(file) - .run({}) .then(info => { t.is(info.stats.passCount, 1); t.is(info.tests.length, 1); @@ -64,7 +62,6 @@ test('exit after tests are finished', t => { let cleanupCompleted = false; fork(fixture('slow-exit.js')) - .run({}) .on('exit', () => { t.true(Date.now() - start < 10000, 'test waited for a pending setTimeout'); t.true(cleanupCompleted, 'cleanup did not complete'); @@ -104,7 +101,6 @@ test('rejects promise if the process is killed', t => { test('fake timers do not break duration', t => { return fork(fixture('fake-timers.js')) - .run({}) .then(info => { const duration = info.tests[0].duration; t.true(duration < 1000, `${duration} < 1000`); @@ -114,21 +110,8 @@ test('fake timers do not break duration', t => { }); }); -/* ignore -test('destructuring of `t` is allowed', t => { - fork(fixture('destructuring-public-api.js')) - .run({}) - .then(info => { - t.is(info.stats.failCount, 0); - t.is(info.stats.passCount, 3); - t.end(); - }); -}); -*/ - test('babelrc is ignored', t => { return fork(fixture('babelrc/test.js')) - .run({}) .then(info => { t.is(info.stats.passCount, 1); t.end(); @@ -139,7 +122,6 @@ test('@std/esm support', t => { return fork(fixture('std-esm/test.js'), { require: [require.resolve('@std/esm')] }) - .run({}) .then(info => { t.is(info.stats.passCount, 1); t.end(); @@ -151,9 +133,9 @@ test('color support is initialized correctly', t => { t.plan(1); return Promise.all([ - fork(fixture('chalk-enabled.js'), {color: true}).run({}), - fork(fixture('chalk-disabled.js'), {color: false}).run({}), - fork(fixture('chalk-disabled.js'), {}).run({}) + fork(fixture('chalk-enabled.js'), {color: true}), + fork(fixture('chalk-disabled.js'), {color: false}), + fork(fixture('chalk-disabled.js'), {}) ]).then(infos => { for (const info of infos) { if (info.stats.failCount > 0) { diff --git a/test/hooks.js b/test/hooks.js index ee731b4c1..78229f77c 100644 --- a/test/hooks.js +++ b/test/hooks.js @@ -1,4 +1,6 @@ 'use strict'; +require('../lib/worker-options').set({}); + const path = require('path'); const test = require('tap').test; const Runner = require('../lib/runner'); @@ -28,22 +30,27 @@ function fork(testPath) { }); } +const promiseEnd = (runner, next) => { + return new Promise(resolve => { + runner.on('start', data => resolve(data.ended)); + next(runner); + }).then(() => runner); +}; + test('before', t => { t.plan(1); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.before(() => { + arr.push('a'); + }); - runner.chain.before(() => { - arr.push('a'); - }); - - runner.chain('test', a => { - a.pass(); - arr.push('b'); - }); - - return runner.run({}).then(() => { + runner.chain('test', a => { + a.pass(); + arr.push('b'); + }); + }).then(() => { t.strictDeepEqual(arr, ['a', 'b']); }); }); @@ -51,466 +58,412 @@ test('before', t => { test('after', t => { t.plan(3); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.after(() => { + arr.push('b'); + }); - runner.chain.after(() => { - arr.push('b'); - }); - - runner.chain('test', a => { - a.pass(); - arr.push('a'); - }); - - return runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.passCount, 1); - t.is(stats.failCount, 0); + runner.chain('test', a => { + a.pass(); + arr.push('a'); + }); + }).then(runner => { + t.is(runner.stats.passCount, 1); + t.is(runner.stats.failCount, 0); t.strictDeepEqual(arr, ['a', 'b']); - t.end(); }); }); test('after not run if test failed', t => { t.plan(3); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.after(() => { + arr.push('a'); + }); - runner.chain.after(() => { - arr.push('a'); - }); - - runner.chain('test', () => { - throw new Error('something went wrong'); - }); - return runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.passCount, 0); - t.is(stats.failCount, 1); + runner.chain('test', () => { + throw new Error('something went wrong'); + }); + }).then(runner => { + t.is(runner.stats.passCount, 0); + t.is(runner.stats.failCount, 1); t.strictDeepEqual(arr, []); - t.end(); }); }); test('after.always run even if test failed', t => { t.plan(3); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.after.always(() => { + arr.push('a'); + }); - runner.chain.after.always(() => { - arr.push('a'); - }); - - runner.chain('test', () => { - throw new Error('something went wrong'); - }); - return runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.passCount, 0); - t.is(stats.failCount, 1); + runner.chain('test', () => { + throw new Error('something went wrong'); + }); + }).then(runner => { + t.is(runner.stats.passCount, 0); + t.is(runner.stats.failCount, 1); t.strictDeepEqual(arr, ['a']); - t.end(); }); }); test('after.always run even if before failed', t => { t.plan(1); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.before(() => { + throw new Error('something went wrong'); + }); - runner.chain.before(() => { - throw new Error('something went wrong'); - }); - - runner.chain.after.always(() => { - arr.push('a'); - }); + runner.chain('test', a => a.pass()); - return runner.run({}).then(() => { + runner.chain.after.always(() => { + arr.push('a'); + }); + }).then(() => { t.strictDeepEqual(arr, ['a']); - t.end(); }); }); test('stop if before hooks failed', t => { t.plan(1); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.before(() => { + arr.push('a'); + }); - runner.chain.before(() => { - arr.push('a'); - }); - - runner.chain.before(() => { - throw new Error('something went wrong'); - }); - - runner.chain('test', a => { - a.pass(); - arr.push('b'); - a.end(); - }); + runner.chain.before(() => { + throw new Error('something went wrong'); + }); - return runner.run({}).then(() => { + runner.chain('test', a => { + a.pass(); + arr.push('b'); + a.end(); + }); + }).then(() => { t.strictDeepEqual(arr, ['a']); - t.end(); }); }); test('before each with concurrent tests', t => { t.plan(1); - const runner = new Runner(); const arr = [[], []]; - let i = 0; - let k = 0; - - runner.chain.beforeEach(() => { - arr[i++].push('a'); - }); + return promiseEnd(new Runner(), runner => { + let i = 0; + let k = 0; - runner.chain.beforeEach(() => { - arr[k++].push('b'); - }); + runner.chain.beforeEach(() => { + arr[i++].push('a'); + }); - runner.chain('c', a => { - a.pass(); - arr[0].push('c'); - }); + runner.chain.beforeEach(() => { + arr[k++].push('b'); + }); - runner.chain('d', a => { - a.pass(); - arr[1].push('d'); - }); + runner.chain('c', a => { + a.pass(); + arr[0].push('c'); + }); - return runner.run({}).then(() => { + runner.chain('d', a => { + a.pass(); + arr[1].push('d'); + }); + }).then(() => { t.strictDeepEqual(arr, [['a', 'b', 'c'], ['a', 'b', 'd']]); - t.end(); }); }); test('before each with serial tests', t => { t.plan(1); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.beforeEach(() => { + arr.push('a'); + }); - runner.chain.beforeEach(() => { - arr.push('a'); - }); - - runner.chain.beforeEach(() => { - arr.push('b'); - }); - - runner.chain.serial('c', a => { - a.pass(); - arr.push('c'); - }); + runner.chain.beforeEach(() => { + arr.push('b'); + }); - runner.chain.serial('d', a => { - a.pass(); - arr.push('d'); - }); + runner.chain.serial('c', a => { + a.pass(); + arr.push('c'); + }); - return runner.run({}).then(() => { + runner.chain.serial('d', a => { + a.pass(); + arr.push('d'); + }); + }).then(() => { t.strictDeepEqual(arr, ['a', 'b', 'c', 'a', 'b', 'd']); - t.end(); }); }); test('fail if beforeEach hook fails', t => { t.plan(2); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.beforeEach(a => { + arr.push('a'); + a.fail(); + }); - runner.chain.beforeEach(a => { - arr.push('a'); - a.fail(); - }); - - runner.chain('test', a => { - arr.push('b'); - a.pass(); - }); - - return runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.failCount, 1); + runner.chain('test', a => { + arr.push('b'); + a.pass(); + }); + }).then(runner => { + t.is(runner.stats.failedHookCount, 1); t.strictDeepEqual(arr, ['a']); - t.end(); }); }); test('after each with concurrent tests', t => { t.plan(1); - const runner = new Runner(); const arr = [[], []]; - let i = 0; - let k = 0; - - runner.chain.afterEach(() => { - arr[i++].push('a'); - }); + return promiseEnd(new Runner(), runner => { + let i = 0; + let k = 0; - runner.chain.afterEach(() => { - arr[k++].push('b'); - }); + runner.chain.afterEach(() => { + arr[i++].push('a'); + }); - runner.chain('c', a => { - a.pass(); - arr[0].push('c'); - }); + runner.chain.afterEach(() => { + arr[k++].push('b'); + }); - runner.chain('d', a => { - a.pass(); - arr[1].push('d'); - }); + runner.chain('c', a => { + a.pass(); + arr[0].push('c'); + }); - return runner.run({}).then(() => { + runner.chain('d', a => { + a.pass(); + arr[1].push('d'); + }); + }).then(() => { t.strictDeepEqual(arr, [['c', 'a', 'b'], ['d', 'a', 'b']]); - t.end(); }); }); test('after each with serial tests', t => { t.plan(1); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.afterEach(() => { + arr.push('a'); + }); - runner.chain.afterEach(() => { - arr.push('a'); - }); - - runner.chain.afterEach(() => { - arr.push('b'); - }); - - runner.chain.serial('c', a => { - a.pass(); - arr.push('c'); - }); + runner.chain.afterEach(() => { + arr.push('b'); + }); - runner.chain.serial('d', a => { - a.pass(); - arr.push('d'); - }); + runner.chain.serial('c', a => { + a.pass(); + arr.push('c'); + }); - return runner.run({}).then(() => { + runner.chain.serial('d', a => { + a.pass(); + arr.push('d'); + }); + }).then(() => { t.strictDeepEqual(arr, ['c', 'a', 'b', 'd', 'a', 'b']); - t.end(); }); }); test('afterEach not run if concurrent tests failed', t => { t.plan(1); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.afterEach(() => { + arr.push('a'); + }); - runner.chain.afterEach(() => { - arr.push('a'); - }); - - runner.chain('test', () => { - throw new Error('something went wrong'); - }); - - return runner.run({}).then(() => { + runner.chain('test', () => { + throw new Error('something went wrong'); + }); + }).then(() => { t.strictDeepEqual(arr, []); - t.end(); }); }); test('afterEach not run if serial tests failed', t => { t.plan(1); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.afterEach(() => { + arr.push('a'); + }); - runner.chain.afterEach(() => { - arr.push('a'); - }); - - runner.chain.serial('test', () => { - throw new Error('something went wrong'); - }); - - return runner.run({}).then(() => { + runner.chain.serial('test', () => { + throw new Error('something went wrong'); + }); + }).then(() => { t.strictDeepEqual(arr, []); - t.end(); }); }); test('afterEach.always run even if concurrent tests failed', t => { t.plan(1); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.afterEach.always(() => { + arr.push('a'); + }); - runner.chain.afterEach.always(() => { - arr.push('a'); - }); - - runner.chain('test', () => { - throw new Error('something went wrong'); - }); - - return runner.run({}).then(() => { + runner.chain('test', () => { + throw new Error('something went wrong'); + }); + }).then(() => { t.strictDeepEqual(arr, ['a']); - t.end(); }); }); test('afterEach.always run even if serial tests failed', t => { t.plan(1); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.afterEach.always(() => { + arr.push('a'); + }); - runner.chain.afterEach.always(() => { - arr.push('a'); - }); - - runner.chain.serial('test', () => { - throw new Error('something went wrong'); - }); - - return runner.run({}).then(() => { + runner.chain.serial('test', () => { + throw new Error('something went wrong'); + }); + }).then(() => { t.strictDeepEqual(arr, ['a']); - t.end(); }); }); test('afterEach.always run even if beforeEach failed', t => { t.plan(1); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.beforeEach(() => { + throw new Error('something went wrong'); + }); - runner.chain.beforeEach(() => { - throw new Error('something went wrong'); - }); - - runner.chain('test', a => { - a.pass(); - arr.push('a'); - }); - - runner.chain.afterEach.always(() => { - arr.push('b'); - }); + runner.chain('test', a => { + a.pass(); + arr.push('a'); + }); - return runner.run({}).then(() => { + runner.chain.afterEach.always(() => { + arr.push('b'); + }); + }).then(() => { t.strictDeepEqual(arr, ['b']); - t.end(); }); }); test('ensure hooks run only around tests', t => { t.plan(1); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain.beforeEach(() => { + arr.push('beforeEach'); + }); - runner.chain.beforeEach(() => { - arr.push('beforeEach'); - }); - - runner.chain.before(() => { - arr.push('before'); - }); - - runner.chain.afterEach(() => { - arr.push('afterEach'); - }); + runner.chain.before(() => { + arr.push('before'); + }); - runner.chain.after(() => { - arr.push('after'); - }); + runner.chain.afterEach(() => { + arr.push('afterEach'); + }); - runner.chain('test', a => { - a.pass(); - arr.push('test'); - }); + runner.chain.after(() => { + arr.push('after'); + }); - return runner.run({}).then(() => { + runner.chain('test', a => { + a.pass(); + arr.push('test'); + }); + }).then(() => { t.strictDeepEqual(arr, ['before', 'beforeEach', 'test', 'afterEach', 'after']); - t.end(); }); }); test('shared context', t => { t.plan(1); - const runner = new Runner(); - - runner.chain.before(a => { - a.deepEqual(a.context, {}); - a.context.arr = ['a']; - a.context.prop = 'before'; - }); - - runner.chain.after(a => { - a.deepEqual(a.context.arr, ['a', 'b', 'c', 'd']); - a.is(a.context.prop, 'before'); - }); + return promiseEnd(new Runner(), runner => { + runner.chain.before(a => { + a.deepEqual(a.context, {}); + a.context.arr = ['a']; + a.context.prop = 'before'; + }); - runner.chain.beforeEach(a => { - a.deepEqual(a.context.arr, ['a']); - a.context.arr.push('b'); - a.is(a.context.prop, 'before'); - a.context.prop = 'beforeEach'; - }); + runner.chain.after(a => { + a.deepEqual(a.context.arr, ['a', 'b', 'c', 'd']); + a.is(a.context.prop, 'before'); + }); - runner.chain('test', a => { - a.pass(); - a.deepEqual(a.context.arr, ['a', 'b']); - a.context.arr.push('c'); - a.is(a.context.prop, 'beforeEach'); - a.context.prop = 'test'; - }); + runner.chain.beforeEach(a => { + a.deepEqual(a.context.arr, ['a']); + a.context.arr.push('b'); + a.is(a.context.prop, 'before'); + a.context.prop = 'beforeEach'; + }); - runner.chain.afterEach(a => { - a.deepEqual(a.context.arr, ['a', 'b', 'c']); - a.context.arr.push('d'); - a.is(a.context.prop, 'test'); - a.context.prop = 'afterEach'; - }); + runner.chain('test', a => { + a.pass(); + a.deepEqual(a.context.arr, ['a', 'b']); + a.context.arr.push('c'); + a.is(a.context.prop, 'beforeEach'); + a.context.prop = 'test'; + }); - return runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.failCount, 0); - t.end(); + runner.chain.afterEach(a => { + a.deepEqual(a.context.arr, ['a', 'b', 'c']); + a.context.arr.push('d'); + a.is(a.context.prop, 'test'); + a.context.prop = 'afterEach'; + }); + }).then(runner => { + t.is(runner.stats.failCount, 0); }); }); test('shared context of any type', t => { t.plan(1); - const runner = new Runner(); - - runner.chain.beforeEach(a => { - a.context = 'foo'; - }); - - runner.chain('test', a => { - a.pass(); - a.is(a.context, 'foo'); - }); + return promiseEnd(new Runner(), runner => { + runner.chain.beforeEach(a => { + a.context = 'foo'; + }); - return runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.failCount, 0); - t.end(); + runner.chain('test', a => { + a.pass(); + a.is(a.context, 'foo'); + }); + }).then(runner => { + t.is(runner.stats.failCount, 0); }); }); @@ -518,13 +471,9 @@ test('don\'t display hook title if it did not fail', t => { t.plan(2); return fork(path.join(__dirname, 'fixture/hooks-passing.js')) - .run({}) .on('test', test => { t.strictDeepEqual(test.error, null); t.is(test.title, 'pass'); - }) - .then(() => { - t.end(); }); }); @@ -532,12 +481,8 @@ test('display hook title if it failed', t => { t.plan(2); return fork(path.join(__dirname, 'fixture/hooks-failing.js')) - .run({}) .on('test', test => { t.is(test.error.name, 'AssertionError'); t.is(test.title, 'beforeEach hook for pass'); - }) - .then(() => { - t.end(); }); }); diff --git a/test/observable.js b/test/observable.js index 5027cddd3..819fd4f59 100644 --- a/test/observable.js +++ b/test/observable.js @@ -1,33 +1,32 @@ 'use strict'; +require('../lib/worker-options').set({}); + const test = require('tap').test; const Test = require('../lib/test'); const Observable = require('zen-observable'); // eslint-disable-line import/order -function ava(fn, onResult) { +function ava(fn) { return new Test({ contextRef: null, failWithoutAssertions: true, fn, metadata: {type: 'test', callback: false}, - onResult, title: '[anonymous]' }); } -ava.cb = function (fn, onResult) { +ava.cb = function (fn) { return new Test({ contextRef: null, failWithoutAssertions: true, fn, metadata: {type: 'test', callback: true}, - onResult, title: '[anonymous]' }); }; test('returning an observable from a legacy async fn is an error', t => { - let result; - const passed = ava.cb(a => { + return ava.cb(a => { a.plan(2); const observable = Observable.of(); @@ -39,18 +38,14 @@ test('returning an observable from a legacy async fn is an error', t => { }); return observable; - }, r => { - result = r; - }).run(); - - t.is(passed, false); - t.match(result.reason.message, /Do not return observables/); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.match(result.error.message, /Do not return observables/); + }); }); test('handle throws with erroring observable', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(1); const observable = new Observable(observer => { @@ -58,18 +53,15 @@ test('handle throws with erroring observable', t => { }); return a.throws(observable); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('handle throws with erroring observable returned by function', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(1); const observable = new Observable(observer => { @@ -77,18 +69,15 @@ test('handle throws with erroring observable returned by function', t => { }); return a.throws(() => observable); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('handle throws with long running erroring observable', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(1); const observable = new Observable(observer => { @@ -98,50 +87,39 @@ test('handle throws with long running erroring observable', t => { }); return a.throws(observable, /abc/); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('handle throws with completed observable', t => { - let result; - ava(a => { + return ava(a => { a.plan(1); const observable = Observable.of(); return a.throws(observable); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); test('handle throws with completed observable returned by function', t => { - let result; - ava(a => { + return ava(a => { a.plan(1); const observable = Observable.of(); return a.throws(() => observable); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); test('handle throws with regex', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(1); const observable = new Observable(observer => { @@ -149,18 +127,15 @@ test('handle throws with regex', t => { }); return a.throws(observable, /abc/); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('handle throws with string', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(1); const observable = new Observable(observer => { @@ -168,18 +143,15 @@ test('handle throws with string', t => { }); return a.throws(observable, 'abc'); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('handle throws with false-positive observable', t => { - let result; - ava(a => { + return ava(a => { a.plan(1); const observable = new Observable(observer => { @@ -188,34 +160,27 @@ test('handle throws with false-positive observable', t => { }); return a.throws(observable); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); test('handle notThrows with completed observable', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(1); const observable = Observable.of(); return a.notThrows(observable); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('handle notThrows with thrown observable', t => { - let result; - ava(a => { + return ava(a => { a.plan(1); const observable = new Observable(observer => { @@ -223,18 +188,14 @@ test('handle notThrows with thrown observable', t => { }); return a.notThrows(observable); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); test('handle notThrows with erroring observable returned by function', t => { - let result; - ava(a => { + return ava(a => { a.plan(1); const observable = new Observable(observer => { @@ -242,11 +203,8 @@ test('handle notThrows with erroring observable returned by function', t => { }); return a.notThrows(() => observable); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); diff --git a/test/promise.js b/test/promise.js index 318132161..7ad06bc59 100644 --- a/test/promise.js +++ b/test/promise.js @@ -1,28 +1,26 @@ 'use strict'; -require('../lib/globals').options.color = false; +require('../lib/worker-options').set({color: false}); const Promise = require('bluebird'); const test = require('tap').test; const Test = require('../lib/test'); -function ava(fn, onResult) { +function ava(fn) { return new Test({ contextRef: null, failWithoutAssertions: true, fn, metadata: {type: 'test', callback: false}, - onResult, title: '[anonymous]' }); } -ava.cb = function (fn, onResult) { +ava.cb = function (fn) { return new Test({ contextRef: null, failWithoutAssertions: true, fn, metadata: {type: 'test', callback: true}, - onResult, title: '[anonymous]' }); }; @@ -42,27 +40,22 @@ function fail() { } test('returning a promise from a legacy async fn is an error', t => { - let result; - const passed = ava.cb(a => { + return ava.cb(a => { a.plan(1); return Promise.resolve(true).then(() => { a.pass(); a.end(); }); - }, r => { - result = r; - }).run(); - - t.is(passed, false); - t.match(result.reason.message, /Do not return promises/); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.match(result.error.message, /Do not return promises/); + }); }); test('assertion plan is tested after returned promise resolves', t => { - let result; const start = Date.now(); - ava(a => { + const instance = ava(a => { a.plan(2); const defer = Promise.defer(); @@ -75,20 +68,17 @@ test('assertion plan is tested after returned promise resolves', t => { a.pass(); return defer.promise; - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.planCount, 2); - t.is(result.result.assertCount, 2); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.planCount, 2); + t.is(instance.assertCount, 2); t.true(Date.now() - start >= 500); - t.end(); }); }); test('missing assertion will fail the test', t => { - let result; - ava(a => { + return ava(a => { a.plan(2); const defer = Promise.defer(); @@ -99,18 +89,14 @@ test('missing assertion will fail the test', t => { }, 200); return defer.promise; - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.assertion, 'plan'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.assertion, 'plan'); }); }); test('extra assertion will fail the test', t => { - let result; - ava(a => { + return ava(a => { a.plan(2); const defer = Promise.defer(); @@ -126,51 +112,41 @@ test('extra assertion will fail the test', t => { }, 500); return defer.promise; - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.assertion, 'plan'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.assertion, 'plan'); }); }); test('handle throws with rejected promise', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(1); const promise = Promise.reject(new Error()); return a.throws(promise); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('handle throws with rejected promise returned by function', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(1); const promise = Promise.reject(new Error()); return a.throws(() => promise); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); // TODO(team): This is a very slow test, and I can't figure out why we need it - James test('handle throws with long running rejected promise', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(1); const promise = new Promise((resolve, reject) => { @@ -180,270 +156,208 @@ test('handle throws with long running rejected promise', t => { }); return a.throws(promise, /abc/); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('handle throws with resolved promise', t => { - let result; - ava(a => { + return ava(a => { a.plan(1); const promise = Promise.resolve(); return a.throws(promise); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); test('handle throws with resolved promise returned by function', t => { - let result; - ava(a => { + return ava(a => { a.plan(1); const promise = Promise.resolve(); return a.throws(() => promise); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); test('handle throws with regex', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(1); const promise = Promise.reject(new Error('abc')); return a.throws(promise, /abc/); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('throws with regex will fail if error message does not match', t => { - let result; - ava(a => { + return ava(a => { a.plan(1); const promise = Promise.reject(new Error('abc')); return a.throws(promise, /def/); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); test('handle throws with string', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(1); const promise = Promise.reject(new Error('abc')); return a.throws(promise, 'abc'); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('throws with string argument will reject if message does not match', t => { - let result; - ava(a => { + return ava(a => { a.plan(1); const promise = Promise.reject(new Error('abc')); return a.throws(promise, 'def'); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); test('does not handle throws with string reject', t => { - let result; - ava(a => { + return ava(a => { a.plan(1); const promise = Promise.reject('abc'); // eslint-disable-line prefer-promise-reject-errors return a.throws(promise, 'abc'); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); test('handle throws with false-positive promise', t => { - let result; - ava(a => { + return ava(a => { a.plan(1); const promise = Promise.resolve(new Error()); return a.throws(promise); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); test('handle notThrows with resolved promise', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(1); const promise = Promise.resolve(); return a.notThrows(promise); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('handle notThrows with rejected promise', t => { - let result; - ava(a => { + return ava(a => { a.plan(1); const promise = Promise.reject(new Error()); return a.notThrows(promise); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); test('handle notThrows with resolved promise returned by function', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(1); const promise = Promise.resolve(); return a.notThrows(() => promise); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('handle notThrows with rejected promise returned by function', t => { - let result; - ava(a => { + return ava(a => { a.plan(1); const promise = Promise.reject(new Error()); return a.notThrows(() => promise); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); test('assert pass', t => { - let result; - ava(a => { + const instance = ava(a => { return pass().then(() => { a.pass(); }); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('assert fail', t => { - let result; - ava(a => { + return ava(a => { return pass().then(() => { a.fail(); }); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); }); }); test('reject', t => { - let result; - ava(a => { + return ava(a => { return fail().then(() => { a.pass(); }); - }, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.is(result.reason.message, 'Rejected promise returned by test'); - t.is(result.reason.values.length, 1); - t.is(result.reason.values[0].label, 'Rejected promise returned by test. Reason:'); - t.match(result.reason.values[0].formatted, /.*Error.*\n.*message: 'unicorn'/); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); + t.is(result.error.message, 'Rejected promise returned by test'); + t.is(result.error.values.length, 1); + t.is(result.error.values[0].label, 'Rejected promise returned by test. Reason:'); + t.match(result.error.values[0].formatted, /.*Error.*\n.*message: 'unicorn'/); }); }); test('reject with non-Error', t => { - let result; - ava( - () => Promise.reject('failure'), // eslint-disable-line prefer-promise-reject-errors - r => { - result = r; - } - ).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.is(result.reason.message, 'Rejected promise returned by test'); - t.is(result.reason.values.length, 1); - t.is(result.reason.values[0].label, 'Rejected promise returned by test. Reason:'); - t.match(result.reason.values[0].formatted, /failure/); - t.end(); + return ava(() => { + return Promise.reject('failure'); // eslint-disable-line prefer-promise-reject-errors + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); + t.is(result.error.message, 'Rejected promise returned by test'); + t.is(result.error.values.length, 1); + t.is(result.error.values[0].label, 'Rejected promise returned by test. Reason:'); + t.match(result.error.values[0].formatted, /failure/); }); }); diff --git a/test/reporters/mini.js b/test/reporters/mini.js index cf3a441bd..3d62a1d2e 100644 --- a/test/reporters/mini.js +++ b/test/reporters/mini.js @@ -1,4 +1,5 @@ 'use strict'; +require('../../lib/worker-options').set({}); // These tests are run as a sub-process of the `tap` module, so the standard // output stream will not be recognized as a text terminal. AVA internals are @@ -296,8 +297,12 @@ test('results with passing tests and rejections', t => { reporter.passCount = 1; reporter.rejectionCount = 1; - const err1 = errorFromWorker(new Error('failure one'), {type: 'rejection'}); + const err1 = errorFromWorker(new Error('failure one'), { + file: 'test.js', + type: 'rejection' + }); const err2 = errorFromWorker(new Error('failure two'), { + file: 'test.js', type: 'rejection', stack: 'Error: failure two\n at trailingWhitespace (test.js:1:1)\r\n' }); @@ -312,12 +317,12 @@ test('results with passing tests and rejections', t => { ' ' + colors.green('1 passed'), ' ' + colors.red('1 rejection'), '', - ' ' + colors.boldWhite('Unhandled Rejection'), + ' ' + colors.boldWhite('Unhandled rejection in test.js'), /Error: failure one/, /test\/reporters\/mini\.js/, compareLineOutput.SKIP_UNTIL_EMPTY_LINE, '', - ' ' + colors.boldWhite('Unhandled Rejection'), + ' ' + colors.boldWhite('Unhandled rejection in test.js'), /Error: failure two/, /trailingWhitespace/, '' @@ -330,9 +335,15 @@ test('results with passing tests and exceptions', t => { reporter.passCount = 1; reporter.exceptionCount = 2; - const err = errorFromWorker(new Error('failure'), {type: 'exception'}); + const err = errorFromWorker(new Error('failure'), { + file: 'test.js', + type: 'exception' + }); - const avaErr = errorFromWorker(new AvaError('A futuristic test runner'), {type: 'exception'}); + const avaErr = errorFromWorker(new AvaError('A futuristic test runner'), { + file: 'test.js', + type: 'exception' + }); const runStatus = { errors: [err, avaErr] @@ -344,7 +355,7 @@ test('results with passing tests and exceptions', t => { ' ' + colors.green('1 passed'), ' ' + colors.red('2 exceptions'), '', - ' ' + colors.boldWhite('Uncaught Exception'), + ' ' + colors.boldWhite('Uncaught exception in test.js'), /Error: failure/, /test\/reporters\/mini\.js/, compareLineOutput.SKIP_UNTIL_EMPTY_LINE, @@ -638,7 +649,9 @@ test('results when fail-fast is enabled', t => { const runStatus = { remainingCount: 1, failCount: 1, - failFastEnabled: true + failFastEnabled: true, + fileCount: 1, + observationCount: 1 }; const output = reporter.finish(runStatus); @@ -655,7 +668,9 @@ test('results when fail-fast is enabled with multiple skipped tests', t => { const runStatus = { remainingCount: 2, failCount: 1, - failFastEnabled: true + failFastEnabled: true, + fileCount: 1, + observationCount: 1 }; const output = reporter.finish(runStatus); @@ -667,12 +682,71 @@ test('results when fail-fast is enabled with multiple skipped tests', t => { t.end(); }); +test('results when fail-fast is enabled with skipped test file', t => { + const reporter = miniReporter(); + const runStatus = { + remainingCount: 0, + failCount: 1, + failFastEnabled: true, + fileCount: 2, + observationCount: 1 + }; + + const output = reporter.finish(runStatus); + compareLineOutput(t, output, [ + '', + ' ' + colors.magenta('`--fail-fast` is on. 1 test file was skipped.'), + '' + ]); + t.end(); +}); + +test('results when fail-fast is enabled with multiple skipped test files', t => { + const reporter = miniReporter(); + const runStatus = { + remainingCount: 0, + failCount: 1, + failFastEnabled: true, + fileCount: 3, + observationCount: 1 + }; + + const output = reporter.finish(runStatus); + compareLineOutput(t, output, [ + '', + ' ' + colors.magenta('`--fail-fast` is on. 2 test files were skipped.'), + '' + ]); + t.end(); +}); + +test('results when fail-fast is enabled with skipped tests and files', t => { + const reporter = miniReporter(); + const runStatus = { + remainingCount: 1, + failCount: 1, + failFastEnabled: true, + fileCount: 3, + observationCount: 1 + }; + + const output = reporter.finish(runStatus); + compareLineOutput(t, output, [ + '', + ' ' + colors.magenta('`--fail-fast` is on. At least 1 test was skipped, as well as 2 test files.'), + '' + ]); + t.end(); +}); + test('results without fail-fast if no failing tests', t => { const reporter = miniReporter(); const runStatus = { remainingCount: 1, failCount: 0, - failFastEnabled: true + failFastEnabled: true, + fileCount: 1, + observationCount: 1 }; const output = reporter.finish(runStatus); @@ -685,7 +759,9 @@ test('results without fail-fast if no skipped tests', t => { const runStatus = { remainingCount: 0, failCount: 1, - failFastEnabled: true + failFastEnabled: true, + fileCount: 1, + observationCount: 1 }; const output = reporter.finish(runStatus); @@ -820,7 +896,7 @@ test('returns description based on error itself if no stack available', t => { const reporter = miniReporter(); reporter.exceptionCount = 1; const thrownValue = {message: 'failure one'}; - const err1 = errorFromWorker(thrownValue); + const err1 = errorFromWorker(thrownValue, {file: 'test.js'}); const runStatus = { errors: [err1] }; @@ -829,7 +905,7 @@ test('returns description based on error itself if no stack available', t => { const expectedOutput = [ '\n ' + colors.red('1 exception'), '\n', - '\n ' + colors.boldWhite('Uncaught Exception'), + '\n ' + colors.boldWhite('Uncaught exception in test.js'), '\n Threw non-error: ' + JSON.stringify(thrownValue), '\n' ].join(''); @@ -840,7 +916,7 @@ test('returns description based on error itself if no stack available', t => { test('shows "non-error" hint for invalid throws', t => { const reporter = miniReporter(); reporter.exceptionCount = 1; - const err = errorFromWorker({type: 'exception', message: 'function fooFn() {}', stack: 'function fooFn() {}'}); + const err = errorFromWorker({type: 'exception', message: 'function fooFn() {}', stack: 'function fooFn() {}'}, {file: 'test.js'}); const runStatus = { errors: [err] }; @@ -848,7 +924,7 @@ test('shows "non-error" hint for invalid throws', t => { const expectedOutput = [ '\n ' + colors.red('1 exception'), '\n', - '\n ' + colors.boldWhite('Uncaught Exception'), + '\n ' + colors.boldWhite('Uncaught exception in test.js'), '\n Threw non-error: function fooFn() {}', '\n' ].join(''); diff --git a/test/reporters/verbose.js b/test/reporters/verbose.js index f0f014478..c12073060 100644 --- a/test/reporters/verbose.js +++ b/test/reporters/verbose.js @@ -1,4 +1,5 @@ 'use strict'; +require('../../lib/worker-options').set({}); // These tests are run as a sub-process of the `tap` module, so the standard // output stream will not be recognized as a text terminal. AVA internals are @@ -69,7 +70,7 @@ test('passing test and duration less than threshold', t => { const actualOutput = reporter.test({ title: 'passed', duration: 90 - }, createRunStatus()); + }); const expectedOutput = ' ' + colors.green(figures.tick) + ' passed'; @@ -83,7 +84,7 @@ test('passing test and duration greater than threshold', t => { const actualOutput = reporter.test({ title: 'passed', duration: 150 - }, createRunStatus()); + }); const expectedOutput = ' ' + colors.green(figures.tick) + ' passed' + colors.dimGray(' (150ms)'); @@ -91,24 +92,13 @@ test('passing test and duration greater than threshold', t => { t.end(); }); -test('don\'t display test title if there is only one anonymous test', t => { - const reporter = createReporter(); - - const output = reporter.test({ - title: '[anonymous]' - }, createRunStatus()); - - t.is(output, undefined); - t.end(); -}); - test('known failure test', t => { const reporter = createReporter(); const actualOutput = reporter.test({ title: 'known failure', failing: true - }, createRunStatus()); + }); const expectedOutput = ' ' + colors.red(figures.tick) + ' ' + colors.red('known failure'); @@ -124,7 +114,7 @@ test('failing test', t => { error: { message: 'assertion failed' } - }, createRunStatus()); + }); const expectedOutput = ' ' + colors.red(figures.cross) + ' failed ' + colors.red('assertion failed'); @@ -138,7 +128,7 @@ test('skipped test', t => { const actualOutput = reporter.test({ title: 'skipped', skip: true - }, createRunStatus()); + }); const expectedOutput = ' ' + colors.yellow('- skipped'); @@ -153,7 +143,7 @@ test('todo test', t => { title: 'todo', skip: true, todo: true - }, createRunStatus()); + }); const expectedOutput = ' ' + colors.blue('- todo'); @@ -171,7 +161,7 @@ test('uncaught exception', t => { const output = reporter.unhandledError(error, createRunStatus()).split('\n'); - t.is(output[0], colors.red('Uncaught Exception: test.js')); + t.is(output[0].trim(), colors.boldWhite('Uncaught exception in test.js')); t.match(output[1], /Error: Unexpected token/); t.match(output[2], /test\/reporters\/verbose\.js/); t.end(); @@ -201,7 +191,7 @@ test('unhandled rejection', t => { const output = reporter.unhandledError(error, createRunStatus()).split('\n'); - t.is(output[0], colors.red('Unhandled Rejection: test.js')); + t.is(output[0].trim(), colors.boldWhite('Unhandled rejection in test.js')); t.match(output[1], /Error: Unexpected token/); t.match(output[2], /test\/reporters\/verbose\.js/); t.end(); @@ -217,8 +207,8 @@ test('unhandled error without stack', t => { const output = reporter.unhandledError(err, createRunStatus()).split('\n'); - t.is(output[0], colors.red('Uncaught Exception: test.js')); - t.is(output[1], ' ' + colors.red(JSON.stringify(err))); + t.is(output[0].trim(), colors.boldWhite('Uncaught exception in test.js')); + t.is(output[1], ' Threw non-error: ' + JSON.stringify({message: 'test'})); t.end(); }); @@ -604,6 +594,8 @@ test('results when fail-fast is enabled', t => { runStatus.remainingCount = 1; runStatus.failCount = 1; runStatus.failFastEnabled = true; + runStatus.fileCount = 1; + runStatus.observationCount = 1; runStatus.tests = [{ title: 'failed test' }]; @@ -626,6 +618,8 @@ test('results when fail-fast is enabled with multiple skipped tests', t => { runStatus.remainingCount = 2; runStatus.failCount = 1; runStatus.failFastEnabled = true; + runStatus.fileCount = 1; + runStatus.observationCount = 1; runStatus.tests = [{ title: 'failed test' }]; @@ -642,6 +636,78 @@ test('results when fail-fast is enabled with multiple skipped tests', t => { t.end(); }); +test('results when fail-fast is enabled with skipped test file', t => { + const reporter = createReporter(); + const runStatus = createRunStatus(); + runStatus.remainingCount = 0; + runStatus.failCount = 1; + runStatus.failFastEnabled = true; + runStatus.fileCount = 2; + runStatus.observationCount = 1; + runStatus.tests = [{ + title: 'failed test' + }]; + + const output = reporter.finish(runStatus); + const expectedOutput = [ + '\n ' + colors.red('1 test failed'), + '\n', + '\n ' + colors.magenta('`--fail-fast` is on. 1 test file was skipped.'), + '\n' + ].join(''); + + t.is(output, expectedOutput); + t.end(); +}); + +test('results when fail-fast is enabled with multiple skipped test files', t => { + const reporter = createReporter(); + const runStatus = createRunStatus(); + runStatus.remainingCount = 0; + runStatus.failCount = 1; + runStatus.failFastEnabled = true; + runStatus.fileCount = 3; + runStatus.observationCount = 1; + runStatus.tests = [{ + title: 'failed test' + }]; + + const output = reporter.finish(runStatus); + const expectedOutput = [ + '\n ' + colors.red('1 test failed'), + '\n', + '\n ' + colors.magenta('`--fail-fast` is on. 2 test files were skipped.'), + '\n' + ].join(''); + + t.is(output, expectedOutput); + t.end(); +}); + +test('results when fail-fast is enabled with skipped tests and files', t => { + const reporter = createReporter(); + const runStatus = createRunStatus(); + runStatus.remainingCount = 1; + runStatus.failCount = 1; + runStatus.failFastEnabled = true; + runStatus.fileCount = 3; + runStatus.observationCount = 1; + runStatus.tests = [{ + title: 'failed test' + }]; + + const output = reporter.finish(runStatus); + const expectedOutput = [ + '\n ' + colors.red('1 test failed'), + '\n', + '\n ' + colors.magenta('`--fail-fast` is on. At least 1 test was skipped, as well as 2 test files.'), + '\n' + ].join(''); + + t.is(output, expectedOutput); + t.end(); +}); + test('results without fail-fast if no failing tests', t => { const reporter = createReporter(); const runStatus = createRunStatus(); @@ -649,6 +715,8 @@ test('results without fail-fast if no failing tests', t => { runStatus.failCount = 0; runStatus.passCount = 1; runStatus.failFastEnabled = true; + runStatus.fileCount = 1; + runStatus.observationCount = 1; const output = reporter.finish(runStatus); const expectedOutput = [ @@ -667,6 +735,8 @@ test('results without fail-fast if no skipped tests', t => { runStatus.remainingCount = 0; runStatus.failCount = 1; runStatus.failFastEnabled = true; + runStatus.fileCount = 1; + runStatus.observationCount = 1; runStatus.tests = [{ title: 'failed test' }]; diff --git a/test/runner.js b/test/runner.js index f8d85acb9..4ed269dd0 100644 --- a/test/runner.js +++ b/test/runner.js @@ -1,53 +1,50 @@ 'use strict'; +require('../lib/worker-options').set({}); + const test = require('tap').test; const Runner = require('../lib/runner'); const slice = Array.prototype.slice; const noop = () => {}; +const promiseEnd = (runner, next) => { + return new Promise(resolve => { + runner.on('start', data => resolve(data.ended)); + next(runner); + }).then(() => runner); +}; + test('nested tests and hooks aren\'t allowed', t => { t.plan(1); - const runner = new Runner(); - - runner.chain('test', a => { - t.throws(() => { - runner.chain(noop); - }, {message: 'All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.'}); - a.pass(); - }); - - runner.run({}).then(() => { - t.end(); + return promiseEnd(new Runner(), runner => { + runner.chain('test', a => { + t.throws(() => { + runner.chain(noop); + }, {message: 'All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.'}); + a.pass(); + }); }); }); test('tests must be declared synchronously', t => { t.plan(1); - const runner = new Runner(); - - runner.chain('test', a => { - a.pass(); - return Promise.resolve(); + return promiseEnd(new Runner(), runner => { + runner.chain('test', a => { + a.pass(); + return Promise.resolve(); + }); + }).then(runner => { + t.throws(() => { + runner.chain(noop); + }, {message: 'All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.'}); }); - - runner.run({}); - - t.throws(() => { - runner.chain(noop); - }, {message: 'All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.'}); - - t.end(); }); test('runner emits a "test" event', t => { const runner = new Runner(); - runner.chain('foo', a => { - a.pass(); - }); - runner.on('test', props => { t.ifError(props.error); t.is(props.title, 'foo'); @@ -55,38 +52,35 @@ test('runner emits a "test" event', t => { t.end(); }); - runner.run({}); + runner.chain('foo', a => { + a.pass(); + }); }); test('run serial tests before concurrent ones', t => { - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain('test', a => { + arr.push('c'); + a.end(); + }); - runner.chain('test', a => { - arr.push('c'); - a.end(); - }); - - runner.chain.serial('serial', a => { - arr.push('a'); - a.end(); - }); - - runner.chain.serial('serial 2', a => { - arr.push('b'); - a.end(); - }); + runner.chain.serial('serial', a => { + arr.push('a'); + a.end(); + }); - runner.run({}).then(() => { + runner.chain.serial('serial 2', a => { + arr.push('b'); + a.end(); + }); + }).then(() => { t.strictDeepEqual(arr, ['a', 'b', 'c']); - t.end(); }); }); test('anything can be skipped', t => { - const runner = new Runner(); const arr = []; - function pusher(title) { return a => { arr.push(title); @@ -94,25 +88,25 @@ test('anything can be skipped', t => { }; } - runner.chain.after(pusher('after')); - runner.chain.after.skip(pusher('after.skip')); - - runner.chain.afterEach(pusher('afterEach')); - runner.chain.afterEach.skip(pusher('afterEach.skip')); + return promiseEnd(new Runner(), runner => { + runner.chain.after(pusher('after')); + runner.chain.after.skip(pusher('after.skip')); - runner.chain.before(pusher('before')); - runner.chain.before.skip(pusher('before.skip')); + runner.chain.afterEach(pusher('afterEach')); + runner.chain.afterEach.skip(pusher('afterEach.skip')); - runner.chain.beforeEach(pusher('beforeEach')); - runner.chain.beforeEach.skip(pusher('beforeEach.skip')); + runner.chain.before(pusher('before')); + runner.chain.before.skip(pusher('before.skip')); - runner.chain('concurrent', pusher('concurrent')); - runner.chain.skip('concurrent.skip', pusher('concurrent.skip')); + runner.chain.beforeEach(pusher('beforeEach')); + runner.chain.beforeEach.skip(pusher('beforeEach.skip')); - runner.chain.serial('serial', pusher('serial')); - runner.chain.serial.skip('serial.skip', pusher('serial.skip')); + runner.chain('concurrent', pusher('concurrent')); + runner.chain.skip('concurrent.skip', pusher('concurrent.skip')); - runner.run({}).then(() => { + runner.chain.serial('serial', pusher('serial')); + runner.chain.serial.skip('serial.skip', pusher('serial.skip')); + }).then(() => { // Note that afterEach and beforeEach run twice because there are two actual tests - "serial" and "concurrent" t.strictDeepEqual(arr, [ 'before', @@ -124,471 +118,469 @@ test('anything can be skipped', t => { 'afterEach', 'after' ]); - t.end(); }); }); -test('include skipped tests in results', t => { - const runner = new Runner(); - - runner.chain.before('before', noop); - runner.chain.before.skip('before.skip', noop); - - runner.chain.beforeEach('beforeEach', noop); - runner.chain.beforeEach.skip('beforeEach.skip', noop); +test('emit skipped tests at start', t => { + t.plan(1); - runner.chain.serial('test', a => a.pass()); - runner.chain.serial.skip('test.skip', noop); + const runner = new Runner(); + runner.on('start', data => { + t.strictDeepEqual(data.skippedTests, [ + {failing: false, title: 'test.serial.skip'}, + {failing: true, title: 'test.failing.skip'} + ]); + }); - runner.chain.after('after', noop); - runner.chain.after.skip('after.skip', noop); + return promiseEnd(runner, () => { + runner.chain.before('before', noop); + runner.chain.before.skip('before.skip', noop); - runner.chain.afterEach('afterEach', noop); - runner.chain.afterEach.skip('afterEach.skip', noop); + runner.chain.beforeEach('beforeEach', noop); + runner.chain.beforeEach.skip('beforeEach.skip', noop); - const titles = []; + runner.chain.serial('test.serial', a => a.pass()); + runner.chain.serial.skip('test.serial.skip', noop); - runner.on('test', test => { - titles.push(test.title); - }); + runner.chain.failing('test.failing', a => a.fail()); + runner.chain.failing.skip('test.failing.skip', noop); - runner.run({}).then(() => { - t.strictDeepEqual(titles, [ - 'before', - 'before.skip', - 'beforeEach for test', - 'beforeEach.skip for test', - 'test', - 'afterEach for test', - 'afterEach.skip for test', - 'test.skip', - 'after', - 'after.skip' - ]); + runner.chain.after('after', noop); + runner.chain.after.skip('after.skip', noop); - t.end(); + runner.chain.afterEach('afterEach', noop); + runner.chain.afterEach.skip('afterEach.skip', noop); }); }); test('test types and titles', t => { - t.plan(10); - - const fn = a => { - a.pass(); - }; - - function named(a) { - a.pass(); - } + t.plan(20); - const runner = new Runner(); - runner.chain.before(named); - runner.chain.beforeEach(fn); - runner.chain.after(fn); - runner.chain.afterEach(named); - runner.chain('test', fn); - - const tests = [ - { - type: 'before', - title: 'before hook' - }, - { - type: 'beforeEach', - title: 'beforeEach hook for test' - }, - { - type: 'test', - title: 'test' - }, - { - type: 'afterEach', - title: 'afterEach hook for test' - }, - { - type: 'after', - title: 'after hook' - } - ]; + const fail = a => a.fail(); + const pass = a => a.pass(); - runner.on('test', props => { - const test = tests.shift(); - t.is(props.title, test.title); - t.is(props.type, test.type); - }); + const check = (setup, expect) => { + const runner = new Runner(); + const assert = data => { + const expected = expect.shift(); + t.is(data.title, expected.title); + t.is(data.metadata.type, expected.type); + }; + runner.on('hook-failed', assert); + runner.on('test', assert); + return promiseEnd(runner, () => setup(runner.chain)); + }; - runner.run({}).then(t.end); + return Promise.all([ + check(chain => { + chain.before(fail); + chain('test', pass); + }, [ + {type: 'before', title: 'before hook'} + ]), + check(chain => { + chain('test', pass); + chain.after(fail); + }, [ + {type: 'test', title: 'test'}, + {type: 'after', title: 'after hook'} + ]), + check(chain => { + chain('test', pass); + chain.after.always(fail); + }, [ + {type: 'test', title: 'test'}, + {type: 'after', title: 'after.always hook'} + ]), + check(chain => { + chain.beforeEach(fail); + chain('test', fail); + }, [ + {type: 'beforeEach', title: 'beforeEach hook for test'} + ]), + check(chain => { + chain('test', pass); + chain.afterEach(fail); + }, [ + {type: 'test', title: 'test'}, + {type: 'afterEach', title: 'afterEach hook for test'} + ]), + check(chain => { + chain('test', pass); + chain.afterEach.always(fail); + }, [ + {type: 'test', title: 'test'}, + {type: 'afterEach', title: 'afterEach.always hook for test'} + ]) + ]); }); test('skip test', t => { - t.plan(5); + t.plan(4); - const runner = new Runner(); const arr = []; - - runner.chain('test', a => { - arr.push('a'); - a.pass(); + return promiseEnd(new Runner(), runner => { + runner.chain('test', a => { + arr.push('a'); + a.pass(); + }); + + runner.chain.skip('skip', () => { + arr.push('b'); + }); + }).then(runner => { + t.is(runner.stats.testCount, 2); + t.is(runner.stats.passCount, 1); + t.is(runner.stats.skipCount, 1); + t.strictDeepEqual(arr, ['a']); }); +}); + +test('tests must have a non-empty title)', t => { + t.plan(1); - runner.chain.skip('skip', () => { - arr.push('b'); + return promiseEnd(new Runner(), runner => { + t.throws(() => { + runner.chain('', t => t.pass()); + }, new TypeError('Tests must have a title')); }); +}); - t.throws(() => { - runner.chain.skip('should be a todo'); - }, new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.')); +test('test titles must be unique', t => { + t.plan(1); - runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.testCount, 2); - t.is(stats.passCount, 1); - t.is(stats.skipCount, 1); - t.strictDeepEqual(arr, ['a']); - t.end(); + return promiseEnd(new Runner(), runner => { + runner.chain('title', t => t.pass()); + + t.throws(() => { + runner.chain('title', t => t.pass()); + }, new Error('Duplicate test title: title')); }); }); -test('test throws when given no function', t => { +test('tests must have an implementation', t => { t.plan(1); const runner = new Runner(); t.throws(() => { - runner.chain(); + runner.chain('title'); }, new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.')); }); test('todo test', t => { - t.plan(6); + t.plan(4); - const runner = new Runner(); const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain('test', a => { + arr.push('a'); + a.pass(); + }); - runner.chain('test', a => { - arr.push('a'); - a.pass(); - }); - - runner.chain.todo('todo'); - - t.throws(() => { - runner.chain.todo('todo', () => {}); - }, new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.')); - - t.throws(() => { - runner.chain.todo(); - }, new TypeError('`todo` tests require a title')); - - runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.testCount, 2); - t.is(stats.passCount, 1); - t.is(stats.todoCount, 1); + runner.chain.todo('todo'); + }).then(runner => { + t.is(runner.stats.testCount, 2); + t.is(runner.stats.passCount, 1); + t.is(runner.stats.todoCount, 1); t.strictDeepEqual(arr, ['a']); - t.end(); }); }); -test('only test', t => { - t.plan(3); - - const runner = new Runner(); - const arr = []; - - runner.chain('test', a => { - arr.push('a'); - a.pass(); - }); - - runner.chain.only('only', a => { - arr.push('b'); - a.pass(); - }); +test('todo tests must not have an implementation', t => { + t.plan(1); - runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.testCount, 1); - t.is(stats.passCount, 1); - t.strictDeepEqual(arr, ['b']); - t.end(); + return promiseEnd(new Runner(), runner => { + t.throws(() => { + runner.chain.todo('todo', () => {}); + }, new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.')); }); }); -test('throws if you give a function to todo', t => { - const runner = new Runner(); - - t.throws(() => { - runner.chain.todo('todo with function', noop); - }, new TypeError('`todo` tests are not allowed to have an implementation. Use ' + - '`test.skip()` for tests with an implementation.')); +test('todo tests must have a title', t => { + t.plan(1); - t.end(); + return promiseEnd(new Runner(), runner => { + t.throws(() => { + runner.chain.todo(); + }, new TypeError('`todo` tests require a title')); + }); }); -test('throws if todo has no title', t => { - const runner = new Runner(); +test('todo test titles must be unique', t => { + t.plan(1); - t.throws(() => { - runner.chain.todo(); - }, new TypeError('`todo` tests require a title')); + return promiseEnd(new Runner(), runner => { + runner.chain('title', t => t.pass()); - t.end(); + t.throws(() => { + runner.chain.todo('title'); + }, new Error('Duplicate test title: title')); + }); }); -test('validate accepts skipping failing tests', t => { - t.plan(2); - - const runner = new Runner(); +test('only test', t => { + t.plan(3); - runner.chain.failing.skip('skip failing', noop); + const arr = []; + return promiseEnd(new Runner(), runner => { + runner.chain('test', a => { + arr.push('a'); + a.pass(); + }); - runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.testCount, 1); - t.is(stats.skipCount, 1); - t.end(); + runner.chain.only('only', a => { + arr.push('b'); + a.pass(); + }); + }).then(runner => { + t.is(runner.stats.testCount, 1); + t.is(runner.stats.passCount, 1); + t.strictDeepEqual(arr, ['b']); }); }); -test('runOnlyExclusive option test', t => { +test('options.runOnlyExclusive means only exclusive tests are run', t => { t.plan(1); - const runner = new Runner(); - const options = {runOnlyExclusive: true}; - const arr = []; - - runner.chain('test', () => { - arr.push('a'); - }); + return promiseEnd(new Runner({runOnlyExclusive: true}), runner => { + runner.chain('test', () => { + t.fail(); + }); - runner.run(options).then(stats => { - t.is(stats, null); - t.end(); + runner.chain.only('test 2', () => { + t.pass(); + }); }); }); test('options.serial forces all tests to be serial', t => { t.plan(1); - const runner = new Runner({serial: true}); const arr = []; + return promiseEnd(new Runner({serial: true}), runner => { + runner.chain.cb('cb', a => { + setTimeout(() => { + arr.push(1); + a.end(); + }, 200); + a.pass(); + }); - runner.chain.cb('cb', a => { - setTimeout(() => { - arr.push(1); - a.end(); - }, 200); - a.pass(); - }); - - runner.chain.cb('cb 2', a => { - setTimeout(() => { - arr.push(2); - a.end(); - }, 100); - a.pass(); - }); + runner.chain.cb('cb 2', a => { + setTimeout(() => { + arr.push(2); + a.end(); + }, 100); + a.pass(); + }); - runner.chain('test', a => { - a.pass(); - t.strictDeepEqual(arr, [1, 2]); - t.end(); + runner.chain('test', a => { + a.pass(); + t.strictDeepEqual(arr, [1, 2]); + }); }); - - runner.run({}); }); -test('options.bail will bail out', t => { - t.plan(1); - - const runner = new Runner({bail: true}); - - runner.chain('test', a => { - t.pass(); - a.fail(); - }); +test('options.failFast does not stop concurrent tests from running', t => { + const expected = ['first', 'second']; + t.plan(expected.length); + + promiseEnd(new Runner({failFast: true}), runner => { + let block; + let resume; + runner.chain.beforeEach(() => { + if (block) { + return block; + } + + block = new Promise(resolve => { + resume = resolve; + }); + }); + + runner.chain('first', a => { + resume(); + a.fail(); + }); - runner.chain('test 2', () => { - t.fail(); - }); + runner.chain('second', a => { + a.pass(); + }); - runner.run({}).then(() => { - t.end(); + runner.on('test', data => { + t.is(data.title, expected.shift()); + }); }); }); -test('options.bail will bail out (async)', t => { - t.plan(2); - - const runner = new Runner({bail: true}); - const tests = []; - - runner.chain.cb('cb', a => { - setTimeout(() => { - tests.push(1); +test('options.failFast && options.serial stops subsequent tests from running ', t => { + const expected = ['first']; + t.plan(expected.length); + + promiseEnd(new Runner({failFast: true, serial: true}), runner => { + let block; + let resume; + runner.chain.beforeEach(() => { + if (block) { + return block; + } + + block = new Promise(resolve => { + resume = resolve; + }); + }); + + runner.chain('first', a => { + resume(); a.fail(); - a.end(); - }, 100); - a.pass(); - }); + }); - runner.chain.cb('cb 2', a => { - setTimeout(() => { - tests.push(2); - a.end(); - }, 300); - a.pass(); - }); + runner.chain('second', a => { + a.pass(); + }); - runner.run({}).then(() => { - t.strictDeepEqual(tests, [1]); - // With concurrent tests there is no stopping the second `setTimeout` callback from happening. - // See the `bail + serial` test below for comparison - setTimeout(() => { - t.strictDeepEqual(tests, [1, 2]); - t.end(); - }, 250); + runner.on('test', data => { + t.is(data.title, expected.shift()); + }); }); }); -test('options.bail + serial - tests will never happen (async)', t => { - t.plan(2); - - const runner = new Runner({ - bail: true, - serial: true - }); - const tests = []; - - runner.chain.cb('cb', a => { - setTimeout(() => { - tests.push(1); +test('options.failFast & failing serial test stops subsequent tests from running ', t => { + const expected = ['first']; + t.plan(expected.length); + + promiseEnd(new Runner({failFast: true, serial: true}), runner => { + let block; + let resume; + runner.chain.beforeEach(() => { + if (block) { + return block; + } + + block = new Promise(resolve => { + resume = resolve; + }); + }); + + runner.chain.serial('first', a => { + resume(); a.fail(); - a.end(); - }, 100); - }); + }); - runner.chain.cb('cb 2', a => { - setTimeout(() => { - tests.push(2); - a.end(); - }, 300); - }); + runner.chain.serial('second', a => { + a.pass(); + }); + + runner.chain('third', a => { + a.pass(); + }); - runner.run({}).then(() => { - t.strictDeepEqual(tests, [1]); - setTimeout(() => { - t.strictDeepEqual(tests, [1]); - t.end(); - }, 250); + runner.on('test', data => { + t.is(data.title, expected.shift()); + }); }); }); test('options.match will not run tests with non-matching titles', t => { t.plan(5); - const runner = new Runner({ - match: ['*oo', '!foo'] - }); - - runner.chain('mhm. grass tasty. moo', a => { - t.pass(); - a.pass(); - }); - - runner.chain('juggaloo', a => { - t.pass(); - a.pass(); - }); + return promiseEnd(new Runner({match: ['*oo', '!foo']}), runner => { + runner.chain('mhm. grass tasty. moo', a => { + t.pass(); + a.pass(); + }); - runner.chain('foo', a => { - t.fail(); - a.pass(); - }); + runner.chain('juggaloo', a => { + t.pass(); + a.pass(); + }); - runner.chain('test', a => { - t.fail(); - a.pass(); - }); + runner.chain('foo', a => { + t.fail(); + a.pass(); + }); - runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.skipCount, 0); - t.is(stats.passCount, 2); - t.is(stats.testCount, 2); - t.end(); + runner.chain('test', a => { + t.fail(); + a.pass(); + }); + }).then(runner => { + t.is(runner.stats.skipCount, 0); + t.is(runner.stats.passCount, 2); + t.is(runner.stats.testCount, 2); }); }); test('options.match hold no effect on hooks with titles', t => { t.plan(4); - const runner = new Runner({ - match: ['!before*'] - }); - - let actual; + return promiseEnd(new Runner({match: ['!before*']}), runner => { + let actual; - runner.chain.before('before hook with title', () => { - actual = 'foo'; - }); - - runner.chain('after', a => { - t.is(actual, 'foo'); - a.pass(); - }); + runner.chain.before('before hook with title', () => { + actual = 'foo'; + }); - runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.skipCount, 0); - t.is(stats.passCount, 1); - t.is(stats.testCount, 1); - t.end(); + runner.chain('after', a => { + t.is(actual, 'foo'); + a.pass(); + }); + }).then(runner => { + t.is(runner.stats.skipCount, 0); + t.is(runner.stats.passCount, 1); + t.is(runner.stats.testCount, 1); }); }); test('options.match overrides .only', t => { t.plan(5); - const runner = new Runner({ - match: ['*oo'] - }); + return promiseEnd(new Runner({match: ['*oo']}), runner => { + runner.chain('moo', a => { + t.pass(); + a.pass(); + }); - runner.chain('moo', a => { - t.pass(); - a.pass(); + runner.chain.only('boo', a => { + t.pass(); + a.pass(); + }); + }).then(runner => { + t.is(runner.stats.skipCount, 0); + t.is(runner.stats.passCount, 2); + t.is(runner.stats.testCount, 2); }); +}); - runner.chain.only('boo', a => { - t.pass(); - a.pass(); - }); +test('options.match matches todo tests', t => { + t.plan(2); - runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.skipCount, 0); - t.is(stats.passCount, 2); - t.is(stats.testCount, 2); - t.end(); + return promiseEnd(new Runner({match: ['*oo']}), runner => { + runner.chain.todo('moo'); + runner.chain.todo('oom'); + }).then(runner => { + t.is(runner.stats.testCount, 1); + t.is(runner.stats.todoCount, 1); }); }); test('macros: Additional args will be spread as additional args on implementation function', t => { - t.plan(3); - - const runner = new Runner(); + t.plan(4); - runner.chain('test1', function (a) { - t.deepEqual(slice.call(arguments, 1), ['foo', 'bar']); - a.pass(); - }, 'foo', 'bar'); + return promiseEnd(new Runner(), runner => { + runner.chain.before(function (a) { + t.deepEqual(slice.call(arguments, 1), ['foo', 'bar']); + a.pass(); + }, 'foo', 'bar'); - runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.passCount, 1); - t.is(stats.testCount, 1); - t.end(); + runner.chain('test1', function (a) { + t.deepEqual(slice.call(arguments, 1), ['foo', 'bar']); + a.pass(); + }, 'foo', 'bar'); + }).then(runner => { + t.is(runner.stats.passCount, 1); + t.is(runner.stats.testCount, 1); }); }); @@ -614,21 +606,41 @@ test('macros: Customize test names attaching a `title` function', t => { macroFn.title = (title, firstArg) => (title || 'default') + firstArg; - const runner = new Runner(); + return promiseEnd(new Runner(), runner => { + runner.on('test', props => { + t.is(props.title, expectedTitles.shift()); + }); - runner.on('test', props => { - t.is(props.title, expectedTitles.shift()); + runner.chain(macroFn, 'A'); + runner.chain('supplied', macroFn, 'B'); + runner.chain(macroFn, 'C'); + }).then(runner => { + t.is(runner.stats.passCount, 3); + t.is(runner.stats.testCount, 3); }); +}); - runner.chain(macroFn, 'A'); - runner.chain('supplied', macroFn, 'B'); - runner.chain(macroFn, 'C'); +test('macros: test titles must be strings', t => { + t.plan(1); - runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.passCount, 3); - t.is(stats.testCount, 3); - t.end(); + return promiseEnd(new Runner(), runner => { + t.throws(() => { + const macro = t => t.pass(); + macro.title = () => []; + runner.chain(macro); + }, new TypeError('Test & hook titles must be strings')); + }); +}); + +test('macros: hook titles must be strings', t => { + t.plan(1); + + return promiseEnd(new Runner(), runner => { + t.throws(() => { + const macro = t => t.pass(); + macro.title = () => []; + runner.chain.before(macro); + }, new TypeError('Test & hook titles must be strings')); }); }); @@ -641,22 +653,16 @@ test('match applies to macros', t => { macroFn.title = (title, firstArg) => `${firstArg}bar`; - const runner = new Runner({ - match: ['foobar'] - }); - - runner.on('test', props => { - t.is(props.title, 'foobar'); - }); - - runner.chain(macroFn, 'foo'); - runner.chain(macroFn, 'bar'); + return promiseEnd(new Runner({match: ['foobar']}), runner => { + runner.on('test', props => { + t.is(props.title, 'foobar'); + }); - runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.passCount, 1); - t.is(stats.testCount, 1); - t.end(); + runner.chain(macroFn, 'foo'); + runner.chain(macroFn, 'bar'); + }).then(runner => { + t.is(runner.stats.passCount, 1); + t.is(runner.stats.testCount, 1); }); }); @@ -685,20 +691,16 @@ test('arrays of macros', t => { } macroFnB.title = prefix => `${prefix}.B`; - const runner = new Runner(); - - runner.chain('A', [macroFnA, macroFnB], 'A'); - runner.chain('B', [macroFnA, macroFnB], 'B'); - runner.chain('C', macroFnA, 'C'); - runner.chain('D', macroFnB, 'D'); - - runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.passCount, 6); - t.is(stats.testCount, 6); + return promiseEnd(new Runner(), runner => { + runner.chain('A', [macroFnA, macroFnB], 'A'); + runner.chain('B', [macroFnA, macroFnB], 'B'); + runner.chain('C', macroFnA, 'C'); + runner.chain('D', macroFnB, 'D'); + }).then(runner => { + t.is(runner.stats.passCount, 6); + t.is(runner.stats.testCount, 6); t.is(expectedArgsA.length, 0); t.is(expectedArgsB.length, 0); - t.end(); }); }); @@ -723,21 +725,106 @@ test('match applies to arrays of macros', t => { } bazMacro.title = (title, firstArg) => `${firstArg}baz`; - const runner = new Runner({ - match: ['foobar'] + return promiseEnd(new Runner({match: ['foobar']}), runner => { + runner.on('test', props => { + t.is(props.title, 'foobar'); + }); + + runner.chain([fooMacro, barMacro, bazMacro], 'foo'); + runner.chain([fooMacro, barMacro, bazMacro], 'bar'); + }).then(runner => { + t.is(runner.stats.passCount, 1); + t.is(runner.stats.testCount, 1); }); +}); - runner.on('test', props => { - t.is(props.title, 'foobar'); +test('silently skips other tests when .only is used', t => { + return promiseEnd(new Runner(), runner => { + runner.chain('skip me', a => a.pass()); + runner.chain.serial('skip me too', a => a.pass()); + runner.chain.only('only me', a => a.pass()); + }).then(runner => { + t.is(runner.stats.passCount, 1); + t.is(runner.stats.skipCount, 0); }); +}); - runner.chain([fooMacro, barMacro, bazMacro], 'foo'); - runner.chain([fooMacro, barMacro, bazMacro], 'bar'); +test('subsequent always hooks are run even if earlier always hooks fail', t => { + t.plan(3); + return promiseEnd(new Runner(), runner => { + runner.chain('test', a => a.pass()); + runner.chain.serial.after.always(a => { + t.pass(); + a.fail(); + }); + runner.chain.serial.after.always(a => { + t.pass(); + a.fail(); + }); + runner.chain.after.always(a => { + t.pass(); + a.fail(); + }); + }); +}); - runner.run({}).then(() => { - const stats = runner.buildStats(); - t.is(stats.passCount, 1); - t.is(stats.testCount, 1); - t.end(); +test('hooks run concurrently, but can be serialized', t => { + t.plan(7); + + let activeCount = 0; + return promiseEnd(new Runner(), runner => { + runner.chain('test', a => a.pass()); + + runner.chain.before(() => { + t.is(activeCount, 0); + activeCount++; + return new Promise(resolve => setTimeout(resolve, 20)).then(() => { + activeCount--; + }); + }); + + runner.chain.before(() => { + t.is(activeCount, 1); + activeCount++; + return new Promise(resolve => setTimeout(resolve, 10)).then(() => { + activeCount--; + }); + }); + + runner.chain.serial.before(() => { + t.is(activeCount, 0); + activeCount++; + return new Promise(resolve => setTimeout(resolve, 10)).then(() => { + activeCount--; + }); + }); + + runner.chain.before(() => { + t.is(activeCount, 0); + activeCount++; + return new Promise(resolve => setTimeout(resolve, 20)).then(() => { + activeCount--; + }); + }); + + runner.chain.before(() => { + t.is(activeCount, 1); + activeCount++; + return new Promise(resolve => setTimeout(resolve, 10)).then(() => { + activeCount--; + }); + }); + + runner.chain.serial.before(() => { + t.is(activeCount, 0); + activeCount++; + return new Promise(resolve => setTimeout(resolve, 10)).then(() => { + activeCount--; + }); + }); + + runner.chain.serial.before(() => { + t.is(activeCount, 0); + }); }); }); diff --git a/test/sequence.js b/test/sequence.js deleted file mode 100644 index 555fba97e..000000000 --- a/test/sequence.js +++ /dev/null @@ -1,780 +0,0 @@ -'use strict'; -const tap = require('tap'); -const Promise = require('bluebird'); -const isPromise = require('is-promise'); -const Sequence = require('../lib/sequence'); - -let results = []; -const test = (name, fn) => { - tap.test(name, t => { - results = []; - return fn(t); - }); -}; -function collect(result) { - if (isPromise(result)) { - return result.then(collect); - } - - results.push(result); - return result.passed; -} - -function pass(val) { - return { - run() { - return collect({ - passed: true, - result: val - }); - } - }; -} - -function fail(val) { - return { - run() { - return collect({ - passed: false, - reason: val - }); - } - }; -} - -function passAsync(val) { - return { - run() { - return collect(Promise.resolve({ - passed: true, - result: val - })); - } - }; -} - -function failAsync(err) { - return { - run() { - return collect(Promise.resolve({ - passed: false, - reason: err - })); - } - }; -} - -function reject(err) { - return { - run() { - return Promise.reject(err); - } - }; -} - -test('all sync - no failure - no bail', t => { - const passed = new Sequence( - [ - pass('a'), - pass('b'), - pass('c') - ], - false - ).run(); - - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); -}); - -test('all sync - no failure - bail', t => { - const passed = new Sequence( - [ - pass('a'), - pass('b'), - pass('c') - ], - true - ).run(); - - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); -}); - -test('all sync - begin failure - no bail', t => { - const passed = new Sequence( - [ - fail('a'), - pass('b'), - pass('c') - ], - false - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: false, - reason: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); -}); - -test('all sync - mid failure - no bail', t => { - const passed = new Sequence( - [ - pass('a'), - fail('b'), - pass('c') - ], - false - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a'}, - { - passed: false, - reason: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); -}); - -test('all sync - end failure - no bail', t => { - const passed = new Sequence( - [ - pass('a'), - pass('b'), - fail('c') - ], - false - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: false, - reason: 'c' - } - ]); - t.end(); -}); - -test('all sync - multiple failure - no bail', t => { - const passed = new Sequence( - [ - fail('a'), - pass('b'), - fail('c') - ], - false - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: false, - reason: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: false, - reason: 'c' - } - ]); - t.end(); -}); - -test('all sync - begin failure - bail', t => { - const passed = new Sequence( - [ - fail('a'), - pass('b'), - pass('c') - ], - true - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: false, - reason: 'a' - } - ]); - t.end(); -}); - -test('all sync - mid failure - bail', t => { - const passed = new Sequence( - [ - pass('a'), - fail('b'), - pass('c') - ], - true - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: false, - reason: 'b' - } - ]); - t.end(); -}); - -test('all sync - end failure - bail', t => { - const passed = new Sequence( - [ - pass('a'), - pass('b'), - fail('c') - ], - true - ).run(); - - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: false, - reason: 'c' - } - ]); - t.end(); -}); - -test('all async - no failure - no bail', t => { - new Sequence( - [ - passAsync('a'), - passAsync('b'), - passAsync('c') - ], - false - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); - }); -}); - -test('all async - no failure - bail', t => { - new Sequence( - [ - passAsync('a'), - passAsync('b'), - passAsync('c') - ], - true - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); - }); -}); - -test('last async - no failure - no bail', t => { - new Sequence( - [ - pass('a'), - pass('b'), - passAsync('c') - ], - false - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); - }); -}); - -test('mid async - no failure - no bail', t => { - new Sequence( - [ - pass('a'), - passAsync('b'), - pass('c') - ], - false - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); - }); -}); - -test('first async - no failure - no bail', t => { - new Sequence( - [ - passAsync('a'), - pass('b'), - pass('c') - ], - false - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); - }); -}); - -test('last async - no failure - bail', t => { - new Sequence( - [ - pass('a'), - pass('b'), - passAsync('c') - ], - true - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); - }); -}); - -test('mid async - no failure - bail', t => { - new Sequence( - [ - pass('a'), - passAsync('b'), - pass('c') - ], - true - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); - }); -}); - -test('first async - no failure - bail', t => { - new Sequence( - [ - passAsync('a'), - pass('b'), - pass('c') - ], - true - ).run().then(passed => { - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); - }); -}); - -test('all async - begin failure - bail', t => { - new Sequence( - [ - failAsync('a'), - passAsync('b'), - passAsync('c') - ], - true - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: false, - reason: 'a' - } - ]); - t.end(); - }); -}); - -test('all async - mid failure - bail', t => { - new Sequence( - [ - passAsync('a'), - failAsync('b'), - passAsync('c') - ], - true - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: false, - reason: 'b' - } - ]); - t.end(); - }); -}); - -test('all async - end failure - bail', t => { - new Sequence( - [ - passAsync('a'), - passAsync('b'), - failAsync('c') - ], - true - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: false, - reason: 'c' - } - ]); - t.end(); - }); -}); - -test('all async - begin failure - no bail', t => { - new Sequence( - [ - failAsync('a'), - passAsync('b'), - passAsync('c') - ], - false - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: false, - reason: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); - }); -}); - -test('all async - mid failure - no bail', t => { - new Sequence( - [ - passAsync('a'), - failAsync('b'), - passAsync('c') - ], - false - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: false, - reason: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - t.end(); - }); -}); - -test('all async - end failure - no bail', t => { - new Sequence( - [ - passAsync('a'), - passAsync('b'), - failAsync('c') - ], - false - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: false, - reason: 'c' - } - ]); - t.end(); - }); -}); - -test('all async - multiple failure - no bail', t => { - new Sequence( - [ - failAsync('a'), - passAsync('b'), - failAsync('c') - ], - false - ).run().then(passed => { - t.equal(passed, false); - t.strictDeepEqual(results, [ - { - passed: false, - reason: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: false, - reason: 'c' - } - ]); - t.end(); - }); -}); - -test('rejections are just passed through - no bail', t => { - new Sequence( - [ - pass('a'), - pass('b'), - reject('foo') - ], - false - ).run().catch(err => { - t.is(err, 'foo'); - t.end(); - }); -}); - -test('rejections are just passed through - bail', t => { - new Sequence( - [ - pass('a'), - pass('b'), - reject('foo') - ], - true - ).run().catch(err => { - t.is(err, 'foo'); - t.end(); - }); -}); - -test('needs at least one sequence runnable', t => { - t.throws(() => { - new Sequence().run(); - }, {message: 'Expected an array of runnables'}); - t.end(); -}); - -test('sequences of sequences', t => { - const passed = new Sequence([ - new Sequence([pass('a'), pass('b')]), - new Sequence([pass('c')]) - ]).run(); - - t.equal(passed, true); - t.strictDeepEqual(results, [ - { - passed: true, - result: 'a' - }, - { - passed: true, - result: 'b' - }, - { - passed: true, - result: 'c' - } - ]); - - t.end(); -}); diff --git a/test/serialize-error.js b/test/serialize-error.js index f3c8a8865..6fc4efcb0 100644 --- a/test/serialize-error.js +++ b/test/serialize-error.js @@ -1,4 +1,5 @@ 'use strict'; +require('../lib/worker-options').set({}); const fs = require('fs'); const path = require('path'); @@ -160,3 +161,14 @@ test('remove non-string error properties', t => { t.is(serializedErr.stack, undefined); t.end(); }); + +test('creates multiline summaries for syntax errors', t => { + const err = { + name: 'SyntaxError', + stack: 'Hello\nThere\nSyntaxError here\nIgnore me' + }; + const serializedErr = serialize(err); + t.is(serializedErr.name, 'SyntaxError'); + t.is(serializedErr.summary, 'Hello\nThere\nSyntaxError here'); + t.end(); +}); diff --git a/test/test-collection.js b/test/test-collection.js deleted file mode 100644 index 995e19290..000000000 --- a/test/test-collection.js +++ /dev/null @@ -1,379 +0,0 @@ -'use strict'; -const test = require('tap').test; -const TestCollection = require('../lib/test-collection'); - -function defaults() { - return { - type: 'test', - serial: false, - exclusive: false, - skipped: false, - callback: false, - always: false - }; -} - -function metadata(opts) { - return Object.assign(defaults(), opts); -} - -function mockTest(opts, title) { - return { - title, - metadata: metadata(opts) - }; -} - -function titles(tests) { - if (!tests) { - tests = []; - } - - return tests.map(test => test.title); -} - -function removeEmptyProps(obj) { - if (Array.isArray(obj) && obj.length === 0) { - return null; - } - - if (obj.constructor !== Object) { - return obj; - } - - let cleanObj = null; - - Object.keys(obj).forEach(key => { - const value = removeEmptyProps(obj[key]); - - if (value) { - if (!cleanObj) { - cleanObj = {}; - } - - cleanObj[key] = value; - } - }); - - return cleanObj; -} - -function serialize(collection) { - const serialized = { - tests: { - concurrent: titles(collection.tests.concurrent), - serial: titles(collection.tests.serial) - }, - hooks: { - before: titles(collection.hooks.before), - beforeEach: titles(collection.hooks.beforeEach), - after: titles(collection.hooks.after), - afterAlways: titles(collection.hooks.afterAlways), - afterEach: titles(collection.hooks.afterEach), - afterEachAlways: titles(collection.hooks.afterEachAlways) - } - }; - - return removeEmptyProps(serialized); -} - -test('hasExclusive is set when an exclusive test is added', t => { - const collection = new TestCollection({}); - t.false(collection.hasExclusive); - collection.add(mockTest({exclusive: true}, 'foo')); - t.true(collection.hasExclusive); - t.end(); -}); - -test('adding a concurrent test', t => { - const collection = new TestCollection({}); - collection.add(mockTest({}, 'foo')); - t.strictDeepEqual(serialize(collection), { - tests: { - concurrent: ['foo'] - } - }); - t.end(); -}); - -test('adding a serial test', t => { - const collection = new TestCollection({}); - collection.add(mockTest({serial: true}, 'bar')); - t.strictDeepEqual(serialize(collection), { - tests: { - serial: ['bar'] - } - }); - t.end(); -}); - -test('adding a before test', t => { - const collection = new TestCollection({}); - collection.add(mockTest({type: 'before'}, 'baz')); - t.strictDeepEqual(serialize(collection), { - hooks: { - before: ['baz'] - } - }); - t.end(); -}); - -test('adding a beforeEach test', t => { - const collection = new TestCollection({}); - collection.add(mockTest({type: 'beforeEach'}, 'foo')); - t.strictDeepEqual(serialize(collection), { - hooks: { - beforeEach: ['foo'] - } - }); - t.end(); -}); - -test('adding a after test', t => { - const collection = new TestCollection({}); - collection.add(mockTest({type: 'after'}, 'bar')); - t.strictDeepEqual(serialize(collection), { - hooks: { - after: ['bar'] - } - }); - t.end(); -}); - -test('adding a after.always test', t => { - const collection = new TestCollection({}); - collection.add(mockTest({ - type: 'after', - always: true - }, 'bar')); - t.strictDeepEqual(serialize(collection), { - hooks: { - afterAlways: ['bar'] - } - }); - t.end(); -}); - -test('adding a afterEach test', t => { - const collection = new TestCollection({}); - collection.add(mockTest({type: 'afterEach'}, 'baz')); - t.strictDeepEqual(serialize(collection), { - hooks: { - afterEach: ['baz'] - } - }); - t.end(); -}); - -test('adding a afterEach.always test', t => { - const collection = new TestCollection({}); - collection.add(mockTest({ - type: 'afterEach', - always: true - }, 'baz')); - t.strictDeepEqual(serialize(collection), { - hooks: { - afterEachAlways: ['baz'] - } - }); - t.end(); -}); - -test('adding a bunch of different types', t => { - const collection = new TestCollection({}); - collection.add(mockTest({}, 'a')); - collection.add(mockTest({}, 'b')); - collection.add(mockTest({serial: true}, 'c')); - collection.add(mockTest({serial: true}, 'd')); - collection.add(mockTest({type: 'before'}, 'e')); - t.strictDeepEqual(serialize(collection), { - tests: { - concurrent: ['a', 'b'], - serial: ['c', 'd'] - }, - hooks: { - before: ['e'] - } - }); - t.end(); -}); - -test('skips before and after hooks when all tests are skipped', t => { - t.plan(5); - - const collection = new TestCollection({}); - collection.add({ - metadata: metadata({type: 'before'}), - fn: a => a.fail() - }); - collection.add({ - metadata: metadata({type: 'after'}), - fn: a => a.fail() - }); - collection.add({ - title: 'some serial test', - metadata: metadata({skipped: true, serial: true}), - fn: a => a.fail() - }); - collection.add({ - title: 'some concurrent test', - metadata: metadata({skipped: true}), - fn: a => a.fail() - }); - - const log = []; - collection.on('test', result => { - t.is(result.result.metadata.skipped, true); - t.is(result.result.metadata.type, 'test'); - log.push(result.result.title); - }); - - collection.build().run(); - - t.strictDeepEqual(log, [ - 'some serial test', - 'some concurrent test' - ]); - - t.end(); -}); - -test('runs after.always hook, even if all tests are skipped', t => { - t.plan(6); - - const collection = new TestCollection({}); - collection.add({ - title: 'some serial test', - metadata: metadata({skipped: true, serial: true}), - fn: a => a.fail() - }); - collection.add({ - title: 'some concurrent test', - metadata: metadata({skipped: true}), - fn: a => a.fail() - }); - collection.add({ - title: 'after always', - metadata: metadata({type: 'after', always: true}), - fn: a => a.pass() - }); - - const log = []; - collection.on('test', result => { - if (result.result.metadata.type === 'after') { - t.is(result.result.metadata.skipped, false); - } else { - t.is(result.result.metadata.skipped, true); - t.is(result.result.metadata.type, 'test'); - } - log.push(result.result.title); - }); - - collection.build().run(); - - t.strictDeepEqual(log, [ - 'some serial test', - 'some concurrent test', - 'after always' - ]); - - t.end(); -}); - -test('skips beforeEach and afterEach hooks when test is skipped', t => { - t.plan(3); - - const collection = new TestCollection({}); - collection.add({ - metadata: metadata({type: 'beforeEach'}), - fn: a => a.fail() - }); - collection.add({ - metadata: metadata({type: 'afterEach'}), - fn: a => a.fail() - }); - collection.add({ - title: 'some test', - metadata: metadata({skipped: true}), - fn: a => a.fail() - }); - - const log = []; - collection.on('test', result => { - t.is(result.result.metadata.skipped, true); - t.is(result.result.metadata.type, 'test'); - log.push(result.result.title); - }); - - collection.build().run(); - - t.strictDeepEqual(log, [ - 'some test' - ]); - - t.end(); -}); - -test('foo', t => { - const collection = new TestCollection({}); - const log = []; - - function logger(result) { - t.is(result.passed, true); - log.push(result.result.title); - } - - function add(title, opts) { - collection.add({ - title, - metadata: metadata(opts), - fn: a => a.pass() - }); - } - - add('after1', {type: 'after'}); - add('after.always', { - type: 'after', - always: true - }); - add('beforeEach1', {type: 'beforeEach'}); - add('before1', {type: 'before'}); - add('beforeEach2', {type: 'beforeEach'}); - add('afterEach1', {type: 'afterEach'}); - add('afterEach.always', { - type: 'afterEach', - always: true - }); - add('test1', {}); - add('afterEach2', {type: 'afterEach'}); - add('test2', {}); - add('after2', {type: 'after'}); - add('before2', {type: 'before'}); - - collection.on('test', logger); - - const passed = collection.build().run(); - t.is(passed, true); - - t.strictDeepEqual(log, [ - 'before1', - 'before2', - 'beforeEach1 for test1', - 'beforeEach2 for test1', - 'test1', - 'afterEach1 for test1', - 'afterEach2 for test1', - 'afterEach.always for test1', - 'beforeEach1 for test2', - 'beforeEach2 for test2', - 'test2', - 'afterEach1 for test2', - 'afterEach2 for test2', - 'afterEach.always for test2', - 'after1', - 'after2', - 'after.always' - ]); - - t.end(); -}); diff --git a/test/test.js b/test/test.js index 72b9500b9..0bd63d4d7 100644 --- a/test/test.js +++ b/test/test.js @@ -1,12 +1,11 @@ 'use strict'; -require('../lib/globals').options.color = false; +require('../lib/worker-options').set({color: false}); const test = require('tap').test; const delay = require('delay'); const Test = require('../lib/test'); const failingTestHint = 'Test was expected to fail, but succeeded, you should stop marking the test as failing'; -const noop = () => {}; class ContextRef { constructor() { @@ -20,255 +19,212 @@ class ContextRef { } } -function ava(fn, contextRef, onResult) { +function ava(fn, contextRef) { return new Test({ contextRef: contextRef || new ContextRef(), failWithoutAssertions: true, fn, metadata: {type: 'test', callback: false}, - onResult: onResult || noop, - title: '[anonymous]' + title: 'test' }); } -ava.failing = (fn, contextRef, onResult) => { +ava.failing = (fn, contextRef) => { return new Test({ contextRef: contextRef || new ContextRef(), failWithoutAssertions: true, fn, metadata: {type: 'test', callback: false, failing: true}, - onResult: onResult || noop, - title: '[anonymous]' + title: 'test.failing' }); }; -ava.cb = (fn, contextRef, onResult) => { +ava.cb = (fn, contextRef) => { return new Test({ contextRef: contextRef || new ContextRef(), failWithoutAssertions: true, fn, metadata: {type: 'test', callback: true}, - onResult: onResult || noop, - title: '[anonymous]' + title: 'test.cb' }); }; -ava.cb.failing = (fn, contextRef, onResult) => { +ava.cb.failing = (fn, contextRef) => { return new Test({ contextRef: contextRef || new ContextRef(), failWithoutAssertions: true, fn, metadata: {type: 'test', callback: true, failing: true}, - onResult: onResult || noop, - title: '[anonymous]' + title: 'test.cb.failing' }); }; test('run test', t => { - const passed = ava(a => { + return ava(a => { a.fail(); - }).run(); - - t.is(passed, false); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + }); }); test('multiple asserts', t => { - let result; - const passed = ava(a => { + const instance = ava(a => { a.pass(); a.pass(); a.pass(); - }, null, r => { - result = r; - }).run(); - - t.is(passed, true); - t.is(result.result.assertCount, 3); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 3); + }); }); test('plan assertions', t => { - let result; - const passed = ava(a => { + const instance = ava(a => { a.plan(2); a.pass(); a.pass(); - }, null, r => { - result = r; - }).run(); - t.is(passed, true); - t.is(result.result.planCount, 2); - t.is(result.result.assertCount, 2); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.planCount, 2); + t.is(instance.assertCount, 2); + }); }); test('run more assertions than planned', t => { - let result; - const passed = ava(a => { + return ava(a => { a.plan(2); a.pass(); a.pass(); a.pass(); - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.ok(result.reason); - t.match(result.reason.message, /Planned for 2 assertions, but got 3\./); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.ok(result.error); + t.match(result.error.message, /Planned for 2 assertions, but got 3\./); + t.is(result.error.name, 'AssertionError'); + }); }); test('fails if no assertions are run', t => { - let result; - const passed = ava(() => {}, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.ok(result.reason); - t.is(result.reason.name, 'Error'); - t.match(result.reason.message, /Test finished without running any assertions/); - t.end(); + return ava(() => {}).run().then(result => { + t.is(result.passed, false); + t.ok(result.error); + t.is(result.error.name, 'Error'); + t.match(result.error.message, /Test finished without running any assertions/); + }); }); test('fails if no assertions are run, unless so planned', t => { - const passed = ava(a => a.plan(0)).run(); - t.is(passed, true); - t.end(); + return ava(a => a.plan(0)).run().then(result => { + t.is(result.passed, true); + }); }); test('fails if no assertions are run, unless an ended callback test', t => { - const passed = ava.cb(a => a.end()).run(); - t.is(passed, true); - t.end(); + return ava.cb(a => a.end()).run().then(result => { + t.is(result.passed, true); + }); }); test('wrap non-assertion errors', t => { const err = new Error(); - let result; - const passed = ava(() => { + return ava(() => { throw err; - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.is(result.reason.message, 'Error thrown in test'); - t.is(result.reason.name, 'AssertionError'); - t.is(result.reason.values.length, 1); - t.is(result.reason.values[0].label, 'Error thrown in test:'); - t.match(result.reason.values[0].formatted, /Error/); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.message, 'Error thrown in test'); + t.is(result.error.name, 'AssertionError'); + t.is(result.error.values.length, 1); + t.is(result.error.values[0].label, 'Error thrown in test:'); + t.match(result.error.values[0].formatted, /Error/); + }); }); test('end can be used as callback without maintaining thisArg', t => { - ava.cb(a => { + return ava.cb(a => { a.pass(); setTimeout(a.end); - }).run().then(passed => { - t.is(passed, true); - t.end(); + }).run().then(result => { + t.is(result.passed, true); }); }); test('end can be used as callback with error', t => { const err = new Error('failed'); - let result; - const passed = ava.cb(a => { + return ava.cb(a => { a.end(err); - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.is(result.reason.message, 'Callback called with an error'); - t.is(result.reason.name, 'AssertionError'); - t.is(result.reason.values.length, 1); - t.is(result.reason.values[0].label, 'Callback called with an error:'); - t.match(result.reason.values[0].formatted, /.*Error.*\n.*message: 'failed'/); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.message, 'Callback called with an error'); + t.is(result.error.name, 'AssertionError'); + t.is(result.error.values.length, 1); + t.is(result.error.values[0].label, 'Callback called with an error:'); + t.match(result.error.values[0].formatted, /.*Error.*\n.*message: 'failed'/); + }); }); test('end can be used as callback with a non-error as its error argument', t => { const nonError = {foo: 'bar'}; - let result; - const passed = ava.cb(a => { + return ava.cb(a => { a.end(nonError); - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.ok(result.reason); - t.is(result.reason.message, 'Callback called with an error'); - t.is(result.reason.name, 'AssertionError'); - t.is(result.reason.values.length, 1); - t.is(result.reason.values[0].label, 'Callback called with an error:'); - t.match(result.reason.values[0].formatted, /.*\{.*\n.*foo: 'bar'/); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.ok(result.error); + t.is(result.error.message, 'Callback called with an error'); + t.is(result.error.name, 'AssertionError'); + t.is(result.error.values.length, 1); + t.is(result.error.values[0].label, 'Callback called with an error:'); + t.match(result.error.values[0].formatted, /.*\{.*\n.*foo: 'bar'/); + }); }); test('title returns the test title', t => { t.plan(1); - new Test({ + return new Test({ fn(a) { t.is(a.title, 'foo'); a.pass(); }, metadata: {type: 'test', callback: false}, - onResult: noop, title: 'foo' }).run(); }); test('handle non-assertion errors even when planned', t => { const err = new Error('bar'); - let result; - const passed = ava(a => { + return ava(a => { a.plan(1); throw err; - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.is(result.reason.message, 'Error thrown in test'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); + t.is(result.error.message, 'Error thrown in test'); + }); }); test('handle testing of arrays', t => { - let result; - const passed = ava(a => { + const instance = ava(a => { a.deepEqual(['foo', 'bar'], ['foo', 'bar']); - }, null, r => { - result = r; - }).run(); - - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); + }); }); test('handle falsy testing of arrays', t => { - let result; - const passed = ava(a => { + const instance = ava(a => { a.notDeepEqual(['foo', 'bar'], ['foo', 'bar', 'cat']); - }, null, r => { - result = r; - }).run(); - - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); + }); }); test('handle testing of objects', t => { - let result; - const passed = ava(a => { + const instance = ava(a => { a.deepEqual({ foo: 'foo', bar: 'bar' @@ -276,18 +232,15 @@ test('handle testing of objects', t => { foo: 'foo', bar: 'bar' }); - }, null, r => { - result = r; - }).run(); - - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); + }); }); test('handle falsy testing of objects', t => { - let result; - const passed = ava(a => { + const instance = ava(a => { a.notDeepEqual({ foo: 'foo', bar: 'bar' @@ -296,194 +249,150 @@ test('handle falsy testing of objects', t => { bar: 'bar', cat: 'cake' }); - }, null, r => { - result = r; - }).run(); - - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); + }); }); test('planned async assertion', t => { - let result; - ava.cb(a => { + const instance = ava.cb(a => { a.plan(1); setTimeout(() => { a.pass(); a.end(); }, 100); - }, null, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('async assertion with `.end()`', t => { - let result; - ava.cb(a => { + const instance = ava.cb(a => { setTimeout(() => { a.pass(); a.end(); }, 100); - }, null, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.assertCount, 1); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.assertCount, 1); }); }); test('more assertions than planned should emit an assertion error', t => { - let result; - const passed = ava(a => { + return ava(a => { a.plan(1); a.pass(); a.pass(); - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); + }); }); test('record test duration', t => { - let result; - ava.cb(a => { + return ava.cb(a => { a.plan(1); setTimeout(() => { a.true(true); a.end(); }, 1234); - }, null, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.true(result.result.duration >= 1000); - t.end(); + }).run().then(result => { + t.is(result.passed, true); + t.true(result.duration >= 1000); }); }); test('wait for test to end', t => { - let avaTest; - - let result; - ava.cb(a => { + const instance = ava.cb(a => { a.plan(1); - - avaTest = a; - }, null, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.planCount, 1); - t.is(result.result.assertCount, 1); - t.true(result.result.duration >= 1000); - t.end(); + setTimeout(() => { + a.pass(); + a.end(); + }, 1234); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.planCount, 1); + t.is(instance.assertCount, 1); + t.true(result.duration >= 1000); }); - - setTimeout(() => { - avaTest.pass(); - avaTest.end(); - }, 1234); }); test('fails with the first assertError', t => { - let result; - const passed = ava(a => { + return ava(a => { a.plan(2); a.is(1, 2); a.is(3, 4); - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.is(result.reason.name, 'AssertionError'); - t.is(result.reason.values.length, 1); - t.is(result.reason.values[0].label, 'Difference:'); - t.match(result.reason.values[0].formatted, /- 1\n\+ 2/); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'AssertionError'); + t.is(result.error.values.length, 1); + t.is(result.error.values[0].label, 'Difference:'); + t.match(result.error.values[0].formatted, /- 1\n\+ 2/); + }); }); test('failing pending assertion causes test to fail, not promise rejection', t => { - let result; return ava(a => { - return a.throws(Promise.resolve()) - .then(() => { - throw new Error('Should be ignored'); - }); - }, null, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.notMatch(result.reason.message, /Rejected promise returned by test/); + return a.throws(Promise.resolve()).then(() => { + throw new Error('Should be ignored'); + }); + }).run().then(result => { + t.is(result.passed, false); + t.notMatch(result.error.message, /Rejected promise returned by test/); }); }); test('fails with thrown falsy value', t => { - let result; - const passed = ava(() => { + return ava(() => { throw 0; // eslint-disable-line no-throw-literal - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.is(result.reason.message, 'Error thrown in test'); - t.is(result.reason.name, 'AssertionError'); - t.is(result.reason.values.length, 1); - t.is(result.reason.values[0].label, 'Error thrown in test:'); - t.match(result.reason.values[0].formatted, /0/); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.message, 'Error thrown in test'); + t.is(result.error.name, 'AssertionError'); + t.is(result.error.values.length, 1); + t.is(result.error.values[0].label, 'Error thrown in test:'); + t.match(result.error.values[0].formatted, /0/); + }); }); test('fails with thrown non-error object', t => { const obj = {foo: 'bar'}; - let result; - const passed = ava(() => { + return ava(() => { throw obj; - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.is(result.reason.message, 'Error thrown in test'); - t.is(result.reason.name, 'AssertionError'); - t.is(result.reason.values.length, 1); - t.is(result.reason.values[0].label, 'Error thrown in test:'); - t.match(result.reason.values[0].formatted, /.*\{.*\n.*foo: 'bar'/); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.message, 'Error thrown in test'); + t.is(result.error.name, 'AssertionError'); + t.is(result.error.values.length, 1); + t.is(result.error.values[0].label, 'Error thrown in test:'); + t.match(result.error.values[0].formatted, /.*\{.*\n.*foo: 'bar'/); + }); }); test('skipped assertions count towards the plan', t => { - let result; - const passed = ava(a => { + const instance = ava(a => { a.plan(2); a.pass(); a.skip.fail(); - }, null, r => { - result = r; - }).run(); - - t.is(passed, true); - t.is(result.result.planCount, 2); - t.is(result.result.assertCount, 2); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.planCount, 2); + t.is(instance.assertCount, 2); + }); }); test('throws and notThrows work with promises', t => { let asyncCalled = false; - let result; - ava(a => { + const instance = ava(a => { a.plan(2); return Promise.all([ a.throws(delay.reject(10, new Error('foo')), 'foo'), @@ -491,50 +400,39 @@ test('throws and notThrows work with promises', t => { asyncCalled = true; })) ]); - }, null, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.planCount, 2); - t.is(result.result.assertCount, 2); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.planCount, 2); + t.is(instance.assertCount, 2); t.is(asyncCalled, true); - t.end(); }); }); test('end should not be called multiple times', t => { - let result; - const passed = ava.cb(a => { + return ava.cb(a => { a.pass(); a.end(); a.end(); - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.is(result.reason.message, '`t.end()` called more than once'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.message, '`t.end()` called more than once'); + }); }); test('cb test that throws sync', t => { - let result; const err = new Error('foo'); - const passed = ava.cb(() => { + return ava.cb(() => { throw err; - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.is(result.reason.message, 'Error thrown in test'); - t.is(result.reason.name, 'AssertionError'); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.message, 'Error thrown in test'); + t.is(result.error.name, 'AssertionError'); + }); }); test('multiple resolving and rejecting promises passed to t.throws/t.notThrows', t => { - let result; - ava(a => { + const instance = ava(a => { a.plan(6); const promises = []; for (let i = 0; i < 3; i++) { @@ -544,57 +442,43 @@ test('multiple resolving and rejecting promises passed to t.throws/t.notThrows', ); } return Promise.all(promises); - }, null, r => { - result = r; - }).run().then(passed => { - t.is(passed, true); - t.is(result.result.planCount, 6); - t.is(result.result.assertCount, 6); - t.end(); + }); + return instance.run().then(result => { + t.is(result.passed, true); + t.is(instance.planCount, 6); + t.is(instance.assertCount, 6); }); }); test('fails if test ends while there are pending assertions', t => { - let result; - const passed = ava(a => { + return ava(a => { a.throws(Promise.reject(new Error())); - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.is(result.reason.name, 'Error'); - t.match(result.reason.message, /Test finished, but an assertion is still pending/); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'Error'); + t.match(result.error.message, /Test finished, but an assertion is still pending/); + }); }); test('fails if callback test ends while there are pending assertions', t => { - let result; - const passed = ava.cb(a => { + return ava.cb(a => { a.throws(Promise.reject(new Error())); a.end(); - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.is(result.reason.name, 'Error'); - t.match(result.reason.message, /Test finished, but an assertion is still pending/); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'Error'); + t.match(result.error.message, /Test finished, but an assertion is still pending/); + }); }); test('fails if async test ends while there are pending assertions', t => { - let result; - ava(a => { + return ava(a => { a.throws(Promise.reject(new Error())); return Promise.resolve(); - }, null, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.name, 'Error'); - t.match(result.reason.message, /Test finished, but an assertion is still pending/); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.name, 'Error'); + t.match(result.error.message, /Test finished, but an assertion is still pending/); }); }); @@ -645,117 +529,90 @@ test('contextRef', t => { }); test('failing tests should fail', t => { - const passed = ava.failing('foo', a => { + return ava.failing('foo', a => { a.fail(); - }).run(); - - t.is(passed, true); - t.end(); + }).run().then(result => { + t.is(result.passed, true); + }); }); test('failing callback tests should end without error', t => { const err = new Error('failed'); - const passed = ava.cb.failing(a => { + return ava.cb.failing(a => { a.end(err); - }).run(); - - t.is(passed, true); - t.end(); + }).run().then(result => { + t.is(result.passed, true); + }); }); test('failing tests must not pass', t => { - let result; - const passed = ava.failing(a => { + return ava.failing(a => { a.pass(); - }, null, r => { - result = r; - }).run(); - - t.is(passed, false); - t.is(result.reason.message, failingTestHint); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.message, failingTestHint); + }); }); test('failing callback tests must not pass', t => { - const passed = ava.cb.failing(a => { + return ava.cb.failing(a => { a.pass(); a.end(); - }).run(); - - t.is(passed, false); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + }); }); test('failing tests must not return a fulfilled promise', t => { - let result; - ava.failing(a => { - return Promise.resolve() - .then(() => { - a.pass(); - }); - }, null, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.message, failingTestHint); - t.end(); + return ava.failing(a => { + return Promise.resolve().then(() => a.pass()); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.message, failingTestHint); }); }); test('failing tests pass when returning a rejected promise', t => { - ava.failing(a => { + return ava.failing(a => { a.plan(1); - return a.notThrows(delay(10), 'foo') - .then(() => Promise.reject()); - }).run().then(passed => { - t.is(passed, true); - t.end(); + return a.notThrows(delay(10), 'foo').then(() => Promise.reject()); + }).run().then(result => { + t.is(result.passed, true); }); }); test('failing tests pass with `t.throws(nonThrowingPromise)`', t => { - ava.failing(a => { + return ava.failing(a => { return a.throws(Promise.resolve(10)); - }).run().then(passed => { - t.is(passed, true); - t.end(); + }).run().then(result => { + t.is(result.passed, true); }); }); test('failing tests fail with `t.notThrows(throws)`', t => { - let result; - ava.failing(a => { + return ava.failing(a => { return a.notThrows(Promise.resolve('foo')); - }, null, r => { - result = r; - }).run().then(passed => { - t.is(passed, false); - t.is(result.reason.message, failingTestHint); - t.end(); + }).run().then(result => { + t.is(result.passed, false); + t.is(result.error.message, failingTestHint); }); }); test('log from tests', t => { - let result; - - ava(a => { + return ava(a => { a.log('a log message from a test'); t.true(true); a.log('another log message from a test'); a.log({b: 1, c: {d: 2}}, 'complex log', 5, 5.1); a.log(); - }, null, r => { - result = r; - }).run(); - - t.deepEqual( - result.result.logs, - [ - 'a log message from a test', - 'another log message from a test', - '{\n b: 1,\n c: {\n d: 2,\n },\n} complex log 5 5.1' - ] - ); - - t.end(); + }).run().then(result => { + t.deepEqual( + result.logs, + [ + 'a log message from a test', + 'another log message from a test', + '{\n b: 1,\n c: {\n d: 2,\n },\n} complex log 5 5.1' + ] + ); + }); }); diff --git a/test/watcher.js b/test/watcher.js index bb8e70bdc..7b3ae388b 100644 --- a/test/watcher.js +++ b/test/watcher.js @@ -10,7 +10,7 @@ const sinon = require('sinon'); const test = require('tap').test; const AvaFiles = require('../lib/ava-files'); -const setImmediate = require('../lib/globals').setImmediate; +const setImmediate = require('../lib/now-and-timers').setImmediate; // Helper to make using beforeEach less arduous function makeGroup(test) {