diff options
author | Florian Dold <florian.dold@gmail.com> | 2017-05-28 00:38:50 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2017-05-28 00:40:43 +0200 |
commit | 7fff4499fd915bcea3fa93b1aa8b35f4fe7a6027 (patch) | |
tree | 6de9a1aebd150a23b7f8c273ec657a5d0a18fe3e /node_modules/ava/lib | |
parent | 963b7a41feb29cc4be090a2446bdfe0c1f1bcd81 (diff) |
add linting (and some initial fixes)
Diffstat (limited to 'node_modules/ava/lib')
32 files changed, 4263 insertions, 0 deletions
diff --git a/node_modules/ava/lib/assert.js b/node_modules/ava/lib/assert.js new file mode 100644 index 000000000..c16e11a1a --- /dev/null +++ b/node_modules/ava/lib/assert.js @@ -0,0 +1,378 @@ +'use strict'; +const coreAssert = require('core-assert'); +const deepEqual = require('lodash.isequal'); +const observableToPromise = require('observable-to-promise'); +const isObservable = require('is-observable'); +const isPromise = require('is-promise'); +const jestDiff = require('jest-diff'); +const enhanceAssert = require('./enhance-assert'); +const formatAssertError = require('./format-assert-error'); + +class AssertionError extends Error { + constructor(opts) { + super(opts.message || ''); + this.name = 'AssertionError'; + + this.assertion = opts.assertion; + this.fixedSource = opts.fixedSource; + this.improperUsage = opts.improperUsage || false; + this.operator = opts.operator; + this.values = opts.values || []; + + // Reserved for power-assert statements + this.statements = []; + + if (opts.stack) { + this.stack = opts.stack; + } + } +} +exports.AssertionError = AssertionError; + +function getStack() { + const obj = {}; + Error.captureStackTrace(obj, getStack); + return obj.stack; +} + +function wrapAssertions(callbacks) { + const pass = callbacks.pass; + const pending = callbacks.pending; + const fail = callbacks.fail; + + const noop = () => {}; + const makeNoop = () => noop; + const makeRethrow = reason => () => { + throw reason; + }; + + const assertions = { + pass() { + pass(this); + }, + + fail(message) { + fail(this, new AssertionError({ + assertion: 'fail', + message: message || 'Test failed via `t.fail()`' + })); + }, + + is(actual, expected, message) { + if (actual === expected) { + pass(this); + } else { + const diff = formatAssertError.formatDiff(actual, expected); + const values = diff ? [diff] : [ + formatAssertError.formatWithLabel('Actual:', actual), + formatAssertError.formatWithLabel('Must be strictly equal to:', expected) + ]; + + fail(this, new AssertionError({ + assertion: 'is', + message, + operator: '===', + values + })); + } + }, + + not(actual, expected, message) { + if (actual === expected) { + fail(this, new AssertionError({ + assertion: 'not', + message, + operator: '!==', + values: [formatAssertError.formatWithLabel('Value is strictly equal:', actual)] + })); + } else { + pass(this); + } + }, + + deepEqual(actual, expected, message) { + if (deepEqual(actual, expected)) { + pass(this); + } else { + const diff = formatAssertError.formatDiff(actual, expected); + const values = diff ? [diff] : [ + formatAssertError.formatWithLabel('Actual:', actual), + formatAssertError.formatWithLabel('Must be deeply equal to:', expected) + ]; + + fail(this, new AssertionError({ + assertion: 'deepEqual', + message, + values + })); + } + }, + + notDeepEqual(actual, expected, message) { + if (deepEqual(actual, expected)) { + fail(this, new AssertionError({ + assertion: 'notDeepEqual', + message, + values: [formatAssertError.formatWithLabel('Value is deeply equal:', actual)] + })); + } else { + pass(this); + } + }, + + throws(fn, err, message) { + let promise; + if (isPromise(fn)) { + promise = fn; + } else if (isObservable(fn)) { + promise = observableToPromise(fn); + } else if (typeof fn !== 'function') { + fail(this, new AssertionError({ + assertion: 'throws', + improperUsage: true, + message: '`t.throws()` must be called with a function, Promise, or Observable', + values: [formatAssertError.formatWithLabel('Called with:', fn)] + })); + return; + } + + let coreAssertThrowsErrorArg; + if (typeof err === 'string') { + const expectedMessage = err; + coreAssertThrowsErrorArg = error => error.message === expectedMessage; + } else { + // Assume it's a constructor function or regular expression + coreAssertThrowsErrorArg = err; + } + + const test = (fn, stack) => { + let actual; + let threw = false; + try { + coreAssert.throws(() => { + try { + fn(); + } catch (err) { + actual = err; + threw = true; + throw err; + } + }, coreAssertThrowsErrorArg); + return actual; + } catch (err) { + const values = threw ? + [formatAssertError.formatWithLabel('Threw unexpected exception:', actual)] : + null; + + throw new AssertionError({ + assertion: 'throws', + message, + stack, + values + }); + } + }; + + if (promise) { + // Record stack before it gets lost in the promise chain. + const stack = getStack(); + const intermediate = promise.then(makeNoop, makeRethrow).then(fn => test(fn, stack)); + pending(this, intermediate); + // Don't reject the returned promise, even if the assertion fails. + return intermediate.catch(noop); + } + + try { + const retval = test(fn); + pass(this); + return retval; + } catch (err) { + fail(this, err); + } + }, + + notThrows(fn, message) { + let promise; + if (isPromise(fn)) { + promise = fn; + } else if (isObservable(fn)) { + promise = observableToPromise(fn); + } else if (typeof fn !== 'function') { + fail(this, new AssertionError({ + assertion: 'notThrows', + improperUsage: true, + message: '`t.notThrows()` must be called with a function, Promise, or Observable', + values: [formatAssertError.formatWithLabel('Called with:', fn)] + })); + return; + } + + const test = (fn, stack) => { + try { + coreAssert.doesNotThrow(fn); + } catch (err) { + throw new AssertionError({ + assertion: 'notThrows', + message, + stack, + values: [formatAssertError.formatWithLabel('Threw:', err.actual)] + }); + } + }; + + if (promise) { + // Record stack before it gets lost in the promise chain. + const stack = getStack(); + const intermediate = promise.then(noop, reason => test(makeRethrow(reason), stack)); + pending(this, intermediate); + // Don't reject the returned promise, even if the assertion fails. + return intermediate.catch(noop); + } + + try { + test(fn); + pass(this); + } catch (err) { + fail(this, err); + } + }, + + ifError(actual, message) { + if (actual) { + fail(this, new AssertionError({ + assertion: 'ifError', + message, + values: [formatAssertError.formatWithLabel('Error:', actual)] + })); + } else { + pass(this); + } + }, + + snapshot(actual, message) { + const state = this._test.getSnapshotState(); + const result = state.match(this.title, actual); + if (result.pass) { + pass(this); + } else { + const diff = jestDiff(result.expected.trim(), result.actual.trim(), {expand: true}) + // Remove annotation + .split('\n') + .slice(3) + .join('\n'); + fail(this, new AssertionError({ + assertion: 'snapshot', + message: message || 'Did not match snapshot', + values: [{label: 'Difference:', formatted: diff}] + })); + } + } + }; + + const enhancedAssertions = enhanceAssert(pass, fail, { + truthy(actual, message) { + if (!actual) { + throw new AssertionError({ + assertion: 'truthy', + message, + operator: '!!', + values: [formatAssertError.formatWithLabel('Value is not truthy:', actual)] + }); + } + }, + + falsy(actual, message) { + if (actual) { + throw new AssertionError({ + assertion: 'falsy', + message, + operator: '!', + values: [formatAssertError.formatWithLabel('Value is not falsy:', actual)] + }); + } + }, + + true(actual, message) { + if (actual !== true) { + throw new AssertionError({ + assertion: 'true', + message, + values: [formatAssertError.formatWithLabel('Value is not `true`:', actual)] + }); + } + }, + + false(actual, message) { + if (actual !== false) { + throw new AssertionError({ + assertion: 'false', + message, + values: [formatAssertError.formatWithLabel('Value is not `false`:', actual)] + }); + } + }, + + regex(string, regex, message) { + if (typeof string !== 'string') { + throw new AssertionError({ + assertion: 'regex', + improperUsage: true, + message: '`t.regex()` must be called with a string', + values: [formatAssertError.formatWithLabel('Called with:', string)] + }); + } + if (!(regex instanceof RegExp)) { + throw new AssertionError({ + assertion: 'regex', + improperUsage: true, + message: '`t.regex()` must be called with a regular expression', + values: [formatAssertError.formatWithLabel('Called with:', regex)] + }); + } + + if (!regex.test(string)) { + throw new AssertionError({ + assertion: 'regex', + message, + values: [ + formatAssertError.formatWithLabel('Value must match expression:', string), + formatAssertError.formatWithLabel('Regular expression:', regex) + ] + }); + } + }, + + notRegex(string, regex, message) { + if (typeof string !== 'string') { + throw new AssertionError({ + assertion: 'notRegex', + improperUsage: true, + message: '`t.notRegex()` must be called with a string', + values: [formatAssertError.formatWithLabel('Called with:', string)] + }); + } + if (!(regex instanceof RegExp)) { + throw new AssertionError({ + assertion: 'notRegex', + improperUsage: true, + message: '`t.notRegex()` must be called with a regular expression', + values: [formatAssertError.formatWithLabel('Called with:', regex)] + }); + } + + if (regex.test(string)) { + throw new AssertionError({ + assertion: 'notRegex', + message, + values: [ + formatAssertError.formatWithLabel('Value must not match expression:', string), + formatAssertError.formatWithLabel('Regular expression:', regex) + ] + }); + } + } + }); + + return Object.assign(assertions, enhancedAssertions); +} +exports.wrapAssertions = wrapAssertions; diff --git a/node_modules/ava/lib/ava-error.js b/node_modules/ava/lib/ava-error.js new file mode 100644 index 000000000..05df6b349 --- /dev/null +++ b/node_modules/ava/lib/ava-error.js @@ -0,0 +1,10 @@ +'use strict'; + +class AvaError extends Error { + constructor(message) { + super(message); + this.name = 'AvaError'; + } +} + +module.exports = AvaError; diff --git a/node_modules/ava/lib/ava-files.js b/node_modules/ava/lib/ava-files.js new file mode 100644 index 000000000..dd9a2ee6d --- /dev/null +++ b/node_modules/ava/lib/ava-files.js @@ -0,0 +1,282 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const Promise = require('bluebird'); +const slash = require('slash'); +const globby = require('globby'); +const flatten = require('lodash.flatten'); +const autoBind = require('auto-bind'); +const defaultIgnore = require('ignore-by-default').directories(); +const multimatch = require('multimatch'); + +function handlePaths(files, excludePatterns, globOptions) { + // Convert Promise to Bluebird + files = Promise.resolve(globby(files.concat(excludePatterns), globOptions)); + + const searchedParents = new Set(); + const foundFiles = new Set(); + + function alreadySearchingParent(dir) { + if (searchedParents.has(dir)) { + return true; + } + + const parentDir = path.dirname(dir); + + if (parentDir === dir) { + // We have reached the root path + return false; + } + + return alreadySearchingParent(parentDir); + } + + return files + .map(file => { + file = path.resolve(globOptions.cwd, file); + + if (fs.statSync(file).isDirectory()) { + if (alreadySearchingParent(file)) { + return null; + } + + searchedParents.add(file); + + let pattern = path.join(file, '**', '*.js'); + + if (process.platform === 'win32') { + // Always use `/` in patterns, harmonizing matching across platforms + pattern = slash(pattern); + } + + return handlePaths([pattern], excludePatterns, globOptions); + } + + // `globby` returns slashes even on Windows. Normalize here so the file + // paths are consistently platform-accurate as tests are run. + return path.normalize(file); + }) + .then(flatten) + .filter(file => file && path.extname(file) === '.js') + .filter(file => { + if (path.basename(file)[0] === '_' && globOptions.includeUnderscoredFiles !== true) { + return false; + } + + return true; + }) + .map(file => path.resolve(file)) + .filter(file => { + const alreadyFound = foundFiles.has(file); + foundFiles.add(file); + return !alreadyFound; + }); +} + +const defaultExcludePatterns = () => [ + '!**/node_modules/**', + '!**/fixtures/**', + '!**/helpers/**' +]; + +const defaultIncludePatterns = () => [ + 'test.js', + 'test-*.js', + 'test', + '**/__tests__', + '**/*.test.js' +]; + +const defaultHelperPatterns = () => [ + '**/__tests__/helpers/**/*.js', + '**/__tests__/**/_*.js', + '**/test/helpers/**/*.js', + '**/test/**/_*.js' +]; + +const getDefaultIgnorePatterns = () => defaultIgnore.map(dir => `${dir}/**/*`); + +// Used on paths before they're passed to multimatch to harmonize matching +// across platforms +const matchable = process.platform === 'win32' ? slash : (path => path); + +class AvaFiles { + constructor(options) { + options = options || {}; + + let files = (options.files || []).map(file => { + // `./` should be removed from the beginning of patterns because + // otherwise they won't match change events from Chokidar + if (file.slice(0, 2) === './') { + return file.slice(2); + } + + return file; + }); + + if (files.length === 0) { + files = defaultIncludePatterns(); + } + + this.excludePatterns = defaultExcludePatterns(); + this.files = files; + this.sources = options.sources || []; + this.cwd = options.cwd || process.cwd(); + + autoBind(this); + } + findTestFiles() { + return handlePaths(this.files, this.excludePatterns, { + cwd: this.cwd, + cache: Object.create(null), + statCache: Object.create(null), + realpathCache: Object.create(null), + symlinks: Object.create(null) + }); + } + findTestHelpers() { + return handlePaths(defaultHelperPatterns(), ['!**/node_modules/**'], { + cwd: this.cwd, + includeUnderscoredFiles: true, + cache: Object.create(null), + statCache: Object.create(null), + realpathCache: Object.create(null), + symlinks: Object.create(null) + }); + } + isSource(filePath) { + let mixedPatterns = []; + const defaultIgnorePatterns = getDefaultIgnorePatterns(); + const overrideDefaultIgnorePatterns = []; + + let hasPositivePattern = false; + this.sources.forEach(pattern => { + mixedPatterns.push(pattern); + + // TODO: Why not just `pattern[0] !== '!'`? + if (!hasPositivePattern && pattern[0] !== '!') { + hasPositivePattern = true; + } + + // Extract patterns that start with an ignored directory. These need to be + // rematched separately. + if (defaultIgnore.indexOf(pattern.split('/')[0]) >= 0) { + overrideDefaultIgnorePatterns.push(pattern); + } + }); + + // Same defaults as used for Chokidar + if (!hasPositivePattern) { + mixedPatterns = ['package.json', '**/*.js'].concat(mixedPatterns); + } + + filePath = matchable(filePath); + + // Ignore paths outside the current working directory. + // They can't be matched to a pattern. + if (/^\.\.\//.test(filePath)) { + return false; + } + + const isSource = multimatch(filePath, mixedPatterns).length === 1; + if (!isSource) { + return false; + } + + const isIgnored = multimatch(filePath, defaultIgnorePatterns).length === 1; + if (!isIgnored) { + return true; + } + + const isErroneouslyIgnored = multimatch(filePath, overrideDefaultIgnorePatterns).length === 1; + if (isErroneouslyIgnored) { + return true; + } + + return false; + } + isTest(filePath) { + const excludePatterns = this.excludePatterns; + const initialPatterns = this.files.concat(excludePatterns); + + // Like in `api.js`, tests must be `.js` files and not start with `_` + if (path.extname(filePath) !== '.js' || path.basename(filePath)[0] === '_') { + return false; + } + + // Check if the entire path matches a pattern + if (multimatch(matchable(filePath), initialPatterns).length === 1) { + return true; + } + + // Check if the path contains any directory components + const dirname = path.dirname(filePath); + if (dirname === '.') { + return false; + } + + // Compute all possible subpaths. Note that the dirname is assumed to be + // relative to the working directory, without a leading `./`. + const subpaths = dirname.split(/[\\/]/).reduce((subpaths, component) => { + const parent = subpaths[subpaths.length - 1]; + + if (parent) { + // Always use `/`` to makes multimatch consistent across platforms + subpaths.push(`${parent}/${component}`); + } else { + subpaths.push(component); + } + + return subpaths; + }, []); + + // Check if any of the possible subpaths match a pattern. If so, generate a + // new pattern with **/*.js. + const recursivePatterns = subpaths + .filter(subpath => multimatch(subpath, initialPatterns).length === 1) + // Always use `/` to makes multimatch consistent across platforms + .map(subpath => `${subpath}/**/*.js`); + + // See if the entire path matches any of the subpaths patterns, taking the + // excludePatterns into account. This mimicks the behavior in api.js + return multimatch(matchable(filePath), recursivePatterns.concat(excludePatterns)).length === 1; + } + getChokidarPatterns() { + let paths = []; + let ignored = []; + + this.sources.forEach(pattern => { + if (pattern[0] === '!') { + ignored.push(pattern.slice(1)); + } else { + paths.push(pattern); + } + }); + + // Allow source patterns to override the default ignore patterns. Chokidar + // ignores paths that match the list of ignored patterns. It uses anymatch + // under the hood, which supports negation patterns. For any source pattern + // that starts with an ignored directory, ensure the corresponding negation + // pattern is added to the ignored paths. + const overrideDefaultIgnorePatterns = paths + .filter(pattern => defaultIgnore.indexOf(pattern.split('/')[0]) >= 0) + .map(pattern => `!${pattern}`); + + ignored = getDefaultIgnorePatterns().concat(ignored, overrideDefaultIgnorePatterns); + + if (paths.length === 0) { + paths = ['package.json', '**/*.js']; + } + + paths = paths.concat(this.files); + + return { + paths, + ignored + }; + } +} + +module.exports = AvaFiles; +module.exports.defaultIncludePatterns = defaultIncludePatterns; +module.exports.defaultExcludePatterns = defaultExcludePatterns; diff --git a/node_modules/ava/lib/babel-config.js b/node_modules/ava/lib/babel-config.js new file mode 100644 index 000000000..c3be0dcfb --- /dev/null +++ b/node_modules/ava/lib/babel-config.js @@ -0,0 +1,148 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); +const figures = require('figures'); +const configManager = require('hullabaloo-config-manager'); +const md5Hex = require('md5-hex'); +const mkdirp = require('mkdirp'); +const colors = require('./colors'); + +function validate(conf) { + if (conf === undefined || conf === null) { + conf = 'default'; + } + + // Check for valid babel config shortcuts (can be either `default` or `inherit`) + const isValidShortcut = conf === 'default' || conf === 'inherit'; + + if (!conf || (typeof conf === 'string' && !isValidShortcut)) { + let message = colors.error(figures.cross); + message += ' Unexpected Babel configuration for AVA. '; + message += 'See ' + chalk.underline('https://github.com/avajs/ava#es2015-support') + ' for allowed values.'; + + throw new Error(message); + } + + return conf; +} + +const SOURCE = '(AVA) Base Babel config'; +const AVA_DIR = path.join(__dirname, '..'); + +function verifyExistingOptions(verifierFile, baseConfig, cache) { + return new Promise((resolve, reject) => { + try { + resolve(fs.readFileSync(verifierFile)); + } catch (err) { + if (err && err.code === 'ENOENT') { + resolve(null); + } else { + reject(err); + } + } + }) + .then(buffer => { + if (!buffer) { + return null; + } + + const verifier = configManager.restoreVerifier(buffer); + const fixedSourceHashes = new Map(); + fixedSourceHashes.set(baseConfig.source, baseConfig.hash); + if (baseConfig.extends) { + fixedSourceHashes.set(baseConfig.extends.source, baseConfig.extends.hash); + } + return verifier.verifyCurrentEnv({sources: fixedSourceHashes}, cache) + .then(result => { + if (!result.cacheKeys) { + return null; + } + + if (result.dependenciesChanged) { + fs.writeFileSync(verifierFile, result.verifier.toBuffer()); + } + + return result.cacheKeys; + }); + }); +} + +function resolveOptions(baseConfig, cache, optionsFile, verifierFile) { + return configManager.fromConfig(baseConfig, {cache}) + .then(result => { + fs.writeFileSync(optionsFile, result.generateModule()); + + return result.createVerifier() + .then(verifier => { + fs.writeFileSync(verifierFile, verifier.toBuffer()); + return verifier.cacheKeysForCurrentEnv(); + }); + }); +} + +function build(projectDir, cacheDir, userOptions, powerAssert) { + // Compute a seed based on the Node.js version and the project directory. + // Dependency hashes may vary based on the Node.js version, e.g. with the + // @ava/stage-4 Babel preset. Sources and dependencies paths are absolute in + // the generated module and verifier state. Those paths wouldn't necessarily + // be valid if the project directory changes. + const seed = md5Hex([process.versions.node, projectDir]); + + // Ensure cacheDir exists + mkdirp.sync(cacheDir); + + // The file names predict where valid options may be cached, and thus should + // include the seed. + const optionsFile = path.join(cacheDir, `${seed}.babel-options.js`); + const verifierFile = path.join(cacheDir, `${seed}.verifier.bin`); + + const baseOptions = { + babelrc: false, + presets: [ + ['@ava/transform-test-files', {powerAssert}] + ] + }; + if (userOptions === 'default') { + baseOptions.presets.unshift('@ava/stage-4'); + } + + const baseConfig = configManager.createConfig({ + dir: AVA_DIR, // Presets are resolved relative to this directory + hash: md5Hex(JSON.stringify(baseOptions)), + json5: false, + options: baseOptions, + source: SOURCE + }); + + if (userOptions !== 'default') { + baseConfig.extend(configManager.createConfig({ + dir: projectDir, + options: userOptions === 'inherit' ? + {babelrc: true} : + userOptions, + source: path.join(projectDir, 'package.json') + '#ava.babel', + hash: md5Hex(JSON.stringify(userOptions)) + })); + } + + const cache = configManager.prepareCache(); + return verifyExistingOptions(verifierFile, baseConfig, cache) + .then(cacheKeys => { + if (cacheKeys) { + return cacheKeys; + } + + return resolveOptions(baseConfig, cache, optionsFile, verifierFile); + }) + .then(cacheKeys => ({ + getOptions: require(optionsFile).getOptions, // eslint-disable-line import/no-dynamic-require + // Include the seed in the cache keys used to store compilation results. + cacheKeys: Object.assign({seed}, cacheKeys) + })); +} + +module.exports = { + validate, + build +}; diff --git a/node_modules/ava/lib/beautify-stack.js b/node_modules/ava/lib/beautify-stack.js new file mode 100644 index 000000000..189ed0714 --- /dev/null +++ b/node_modules/ava/lib/beautify-stack.js @@ -0,0 +1,37 @@ +'use strict'; +const StackUtils = require('stack-utils'); +const cleanStack = require('clean-stack'); +const debug = require('debug')('ava'); + +// Ignore unimportant stack trace lines +let ignoreStackLines = []; + +const avaInternals = /\/ava\/(?:lib\/)?[\w-]+\.js:\d+:\d+\)?$/; +const avaDependencies = /\/node_modules\/(?:bluebird|empower-core|(?:ava\/node_modules\/)?(?:babel-runtime|core-js))\//; + +if (!debug.enabled) { + ignoreStackLines = StackUtils.nodeInternals(); + ignoreStackLines.push(avaInternals); + ignoreStackLines.push(avaDependencies); +} + +const stackUtils = new StackUtils({internals: ignoreStackLines}); + +module.exports = stack => { + if (!stack) { + return ''; + } + + // Workaround for https://github.com/tapjs/stack-utils/issues/14 + // TODO: fix it in `stack-utils` + stack = cleanStack(stack); + + const title = stack.split('\n')[0]; + const lines = stackUtils + .clean(stack) + .split('\n') + .map(x => ` ${x}`) + .join('\n'); + + return `${title}\n${lines}`; +}; diff --git a/node_modules/ava/lib/caching-precompiler.js b/node_modules/ava/lib/caching-precompiler.js new file mode 100644 index 000000000..937309bf0 --- /dev/null +++ b/node_modules/ava/lib/caching-precompiler.js @@ -0,0 +1,103 @@ +'use strict'; +const path = require('path'); +const fs = require('fs'); +const convertSourceMap = require('convert-source-map'); +const cachingTransform = require('caching-transform'); +const packageHash = require('package-hash'); +const stripBomBuf = require('strip-bom-buf'); +const autoBind = require('auto-bind'); +const md5Hex = require('md5-hex'); + +function getSourceMap(filePath, code) { + let sourceMap = convertSourceMap.fromSource(code); + + if (!sourceMap) { + const dirPath = path.dirname(filePath); + sourceMap = convertSourceMap.fromMapFileSource(code, dirPath); + } + + if (sourceMap) { + sourceMap = sourceMap.toObject(); + } + + return sourceMap; +} + +class CachingPrecompiler { + constructor(options) { + autoBind(this); + + this.getBabelOptions = options.getBabelOptions; + this.babelCacheKeys = options.babelCacheKeys; + this.cacheDirPath = options.path; + this.fileHashes = {}; + this.transform = this._createTransform(); + } + precompileFile(filePath) { + if (!this.fileHashes[filePath]) { + const source = stripBomBuf(fs.readFileSync(filePath)); + this.transform(source, filePath); + } + + return this.fileHashes[filePath]; + } + // Conditionally called by caching-transform when precompiling is required + _init() { + this.babel = require('babel-core'); + return this._transform; + } + _transform(code, filePath, hash) { + code = code.toString(); + + let result; + const originalBabelDisableCache = process.env.BABEL_DISABLE_CACHE; + try { + // Disable Babel's cache. AVA has good cache management already. + process.env.BABEL_DISABLE_CACHE = '1'; + + result = this.babel.transform(code, Object.assign(this.getBabelOptions(), { + inputSourceMap: getSourceMap(filePath, code), + filename: filePath, + sourceMaps: true, + ast: false + })); + } finally { + // Restore the original value. It is passed to workers, where users may + // not want Babel's cache to be disabled. + process.env.BABEL_DISABLE_CACHE = originalBabelDisableCache; + } + + // Save source map + const mapPath = path.join(this.cacheDirPath, `${hash}.js.map`); + fs.writeFileSync(mapPath, JSON.stringify(result.map)); + + // Append source map comment to transformed code + // So that other libraries (like nyc) can find the source map + const dirPath = path.dirname(filePath); + const relativeMapPath = path.relative(dirPath, mapPath); + const comment = convertSourceMap.generateMapFileComment(relativeMapPath); + + return `${result.code}\n${comment}`; + } + _createTransform() { + const salt = packageHash.sync([ + require.resolve('../package.json'), + require.resolve('babel-core/package.json') + ], this.babelCacheKeys); + + return cachingTransform({ + factory: this._init, + cacheDir: this.cacheDirPath, + hash: this._generateHash, + salt, + ext: '.js' + }); + } + _generateHash(code, filePath, salt) { + const hash = md5Hex([code, filePath, salt]); + this.fileHashes[filePath] = hash; + return hash; + } +} + +module.exports = CachingPrecompiler; diff --git a/node_modules/ava/lib/cli.js b/node_modules/ava/lib/cli.js new file mode 100644 index 000000000..f6213f107 --- /dev/null +++ b/node_modules/ava/lib/cli.js @@ -0,0 +1,200 @@ +'use strict'; +const path = require('path'); +const updateNotifier = require('update-notifier'); +const figures = require('figures'); +const arrify = require('arrify'); +const meow = require('meow'); +const Promise = require('bluebird'); +const pkgConf = require('pkg-conf'); +const isCi = require('is-ci'); +const hasFlag = require('has-flag'); +const Api = require('../api'); +const colors = require('./colors'); +const VerboseReporter = require('./reporters/verbose'); +const MiniReporter = require('./reporters/mini'); +const TapReporter = require('./reporters/tap'); +const Logger = require('./logger'); +const Watcher = require('./watcher'); +const babelConfigHelper = require('./babel-config'); + +// Bluebird specific +Promise.longStackTraces(); + +exports.run = () => { + const conf = pkgConf.sync('ava'); + + const filepath = pkgConf.filepath(conf); + const projectDir = filepath === null ? process.cwd() : path.dirname(filepath); + + const cli = meow(` + Usage + ava [<file|directory|glob> ...] + + Options + --init Add AVA to your project + --fail-fast Stop after first test failure + --serial, -s Run tests serially + --tap, -t Generate TAP output + --verbose, -v Enable verbose output + --no-cache Disable the transpiler cache + --no-power-assert Disable Power Assert + --color Force color output + --no-color Disable color output + --match, -m Only run tests with matching title (Can be repeated) + --watch, -w Re-run tests when tests and source files change + --timeout, -T Set global timeout + --concurrency, -c Maximum number of test files running at the same time (EXPERIMENTAL) + --update-snapshots, -u Update snapshots + + Examples + ava + ava test.js test2.js + ava test-*.js + ava test + ava --init + ava --init foo.js + + Default patterns when no arguments: + test.js test-*.js test/**/*.js **/__tests__/**/*.js **/*.test.js + `, { + string: [ + '_', + 'match', + 'timeout', + 'concurrency' + ], + boolean: [ + 'init', + 'fail-fast', + 'serial', + 'tap', + 'verbose', + 'watch', + 'update-snapshots', + 'color' + ], + default: { + cache: conf.cache, + color: 'color' in conf ? conf.color : require('supports-color') !== false, + concurrency: conf.concurrency, + failFast: conf.failFast, + init: conf.init, + match: conf.match, + powerAssert: conf.powerAssert, + serial: conf.serial, + tap: conf.tap, + timeout: conf.timeout, + updateSnapshots: conf.updateSnapshots, + verbose: conf.verbose, + watch: conf.watch + }, + alias: { + t: 'tap', + v: 'verbose', + s: 'serial', + m: 'match', + w: 'watch', + T: 'timeout', + c: 'concurrency', + u: 'update-snapshots' + } + }); + + updateNotifier({pkg: cli.pkg}).notify(); + + if (cli.flags.init) { + require('ava-init')(); + return; + } + + if ( + ((hasFlag('--watch') || hasFlag('-w')) && (hasFlag('--tap') || hasFlag('-t'))) || + (conf.watch && conf.tap) + ) { + throw new Error(colors.error(figures.cross) + ' The TAP reporter is not available when using watch mode.'); + } + + if ((hasFlag('--watch') || hasFlag('-w')) && isCi) { + throw new Error(colors.error(figures.cross) + ' Watch mode is not available in CI, as it prevents AVA from terminating.'); + } + + if (hasFlag('--require') || hasFlag('-r')) { + throw new Error(colors.error(figures.cross) + ' The --require and -r flags are deprecated. Requirements should be configured in package.json - see documentation.'); + } + + // Copy resultant cli.flags into conf for use with Api and elsewhere + Object.assign(conf, cli.flags); + + const api = new Api({ + failFast: conf.failFast, + failWithoutAssertions: conf.failWithoutAssertions !== false, + serial: conf.serial, + require: arrify(conf.require), + cacheEnabled: conf.cache !== false, + powerAssert: conf.powerAssert !== false, + explicitTitles: conf.watch, + match: arrify(conf.match), + babelConfig: babelConfigHelper.validate(conf.babel), + resolveTestsFrom: cli.input.length === 0 ? projectDir : process.cwd(), + projectDir, + timeout: conf.timeout, + concurrency: conf.concurrency ? parseInt(conf.concurrency, 10) : 0, + updateSnapshots: conf.updateSnapshots, + color: conf.color + }); + + let reporter; + + if (conf.tap && !conf.watch) { + reporter = new TapReporter(); + } else if (conf.verbose || isCi) { + reporter = new VerboseReporter({color: conf.color}); + } else { + reporter = new MiniReporter({color: conf.color, watching: conf.watch}); + } + + reporter.api = api; + const logger = new Logger(reporter); + + logger.start(); + + api.on('test-run', runStatus => { + reporter.api = runStatus; + runStatus.on('test', logger.test); + runStatus.on('error', logger.unhandledError); + + runStatus.on('stdout', logger.stdout); + runStatus.on('stderr', logger.stderr); + }); + + const files = cli.input.length ? cli.input : arrify(conf.files); + + if (conf.watch) { + try { + const watcher = new Watcher(logger, api, files, arrify(conf.source)); + watcher.observeStdin(process.stdin); + } catch (err) { + if (err.name === 'AvaError') { + // An AvaError may be thrown if `chokidar` is not installed. Log it nicely. + console.error(` ${colors.error(figures.cross)} ${err.message}`); + logger.exit(1); + } else { + // Rethrow so it becomes an uncaught exception + throw err; + } + } + } else { + api.run(files) + .then(runStatus => { + logger.finish(runStatus); + logger.exit(runStatus.failCount > 0 || runStatus.rejectionCount > 0 || runStatus.exceptionCount > 0 ? 1 : 0); + }) + .catch(err => { + // Don't swallow exceptions. Note that any expected error should already + // have been logged. + setImmediate(() => { + throw err; + }); + }); + } +}; diff --git a/node_modules/ava/lib/code-excerpt.js b/node_modules/ava/lib/code-excerpt.js new file mode 100644 index 000000000..aa619a0b2 --- /dev/null +++ b/node_modules/ava/lib/code-excerpt.js @@ -0,0 +1,57 @@ +'use strict'; +const fs = require('fs'); +const equalLength = require('equal-length'); +const codeExcerpt = require('code-excerpt'); +const truncate = require('cli-truncate'); +const chalk = require('chalk'); + +const formatLineNumber = (lineNumber, maxLineNumber) => + ' '.repeat(Math.max(0, String(maxLineNumber).length - String(lineNumber).length)) + lineNumber; + +module.exports = (source, options) => { + if (!source.isWithinProject || source.isDependency) { + return null; + } + + const file = source.file; + const line = source.line; + + options = options || {}; + const maxWidth = options.maxWidth || 80; + + let contents; + try { + contents = fs.readFileSync(file, 'utf8'); + } catch (err) { + return null; + } + + const excerpt = codeExcerpt(contents, line, {around: 1}); + if (!excerpt) { + return null; + } + + const lines = excerpt.map(item => ({ + line: item.line, + value: truncate(item.value, maxWidth - String(line).length - 5) + })); + + const joinedLines = lines.map(line => line.value).join('\n'); + const extendedLines = equalLength(joinedLines).split('\n'); + + return lines + .map((item, index) => ({ + line: item.line, + value: extendedLines[index] + })) + .map(item => { + const isErrorSource = item.line === line; + + const lineNumber = formatLineNumber(item.line, line) + ':'; + const coloredLineNumber = isErrorSource ? lineNumber : chalk.grey(lineNumber); + const result = ` ${coloredLineNumber} ${item.value}`; + + return isErrorSource ? chalk.bgRed(result) : result; + }) + .join('\n'); +}; diff --git a/node_modules/ava/lib/colors.js b/node_modules/ava/lib/colors.js new file mode 100644 index 000000000..74be14bb1 --- /dev/null +++ b/node_modules/ava/lib/colors.js @@ -0,0 +1,15 @@ +'use strict'; +const chalk = require('chalk'); + +module.exports = { + title: chalk.bold.white, + error: chalk.red, + skip: chalk.yellow, + todo: chalk.blue, + pass: chalk.green, + duration: chalk.gray.dim, + errorSource: chalk.gray, + errorStack: chalk.gray, + stack: chalk.red, + information: chalk.magenta +}; diff --git a/node_modules/ava/lib/concurrent.js b/node_modules/ava/lib/concurrent.js new file mode 100644 index 000000000..3cdbb41c3 --- /dev/null +++ b/node_modules/ava/lib/concurrent.js @@ -0,0 +1,64 @@ +'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/node_modules/ava/lib/enhance-assert.js b/node_modules/ava/lib/enhance-assert.js new file mode 100644 index 000000000..7808765b7 --- /dev/null +++ b/node_modules/ava/lib/enhance-assert.js @@ -0,0 +1,63 @@ +'use strict'; +const dotProp = require('dot-prop'); +const formatValue = require('./format-assert-error').formatValue; + +// When adding patterns, don't forget to add to +// https://github.com/avajs/babel-preset-transform-test-files/blob/master/espower-patterns.json +// Then release a new version of that preset and bump the SemVer range here. +const PATTERNS = [ + 't.truthy(value, [message])', + 't.falsy(value, [message])', + 't.true(value, [message])', + 't.false(value, [message])', + 't.regex(contents, regex, [message])', + 't.notRegex(contents, regex, [message])' +]; + +const isRangeMatch = (a, b) => { + return (a[0] === b[0] && a[1] === b[1]) || + (a[0] > b[0] && a[0] < b[1]) || + (a[1] > b[0] && a[1] < b[1]); +}; + +const computeStatement = (tokens, range) => { + return tokens + .filter(token => isRangeMatch(token.range, range)) + .map(token => token.value === undefined ? token.type.label : token.value) + .join(''); +}; + +const getNode = (ast, path) => dotProp.get(ast, path.replace(/\//g, '.')); + +const formatter = context => { + const ast = JSON.parse(context.source.ast); + const tokens = JSON.parse(context.source.tokens); + const args = context.args[0].events; + + return args + .map(arg => { + const range = getNode(ast, arg.espath).range; + return [computeStatement(tokens, range), formatValue(arg.value, {maxDepth: 1})]; + }) + .reverse(); +}; + +const enhanceAssert = (pass, fail, assertions) => { + const empower = require('empower-core'); + return empower(assertions, { + destructive: true, + onError(event) { + const error = event.error; + if (event.powerAssertContext) { // Context may be missing in internal tests. + error.statements = formatter(event.powerAssertContext); + } + fail(this, error); + }, + onSuccess() { + pass(this); + }, + patterns: PATTERNS, + bindReceiver: false + }); +}; +module.exports = enhanceAssert; diff --git a/node_modules/ava/lib/extract-stack.js b/node_modules/ava/lib/extract-stack.js new file mode 100644 index 000000000..64f63db1c --- /dev/null +++ b/node_modules/ava/lib/extract-stack.js @@ -0,0 +1,10 @@ +'use strict'; +const stackLineRegex = /^.+ \(.+:[0-9]+:[0-9]+\)$/; + +module.exports = stack => { + return stack + .split('\n') + .filter(line => stackLineRegex.test(line)) + .map(line => line.trim()) + .join('\n'); +}; diff --git a/node_modules/ava/lib/fork.js b/node_modules/ava/lib/fork.js new file mode 100644 index 000000000..bf918d391 --- /dev/null +++ b/node_modules/ava/lib/fork.js @@ -0,0 +1,178 @@ +'use strict'; +const childProcess = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const Promise = require('bluebird'); +const debug = require('debug')('ava'); +const AvaError = require('./ava-error'); + +if (fs.realpathSync(__filename) !== __filename) { + console.warn('WARNING: `npm link ava` and the `--preserve-symlink` flag are incompatible. We have detected that AVA is linked via `npm link`, and that you are using either an early version of Node 6, or the `--preserve-symlink` flag. This breaks AVA. You should upgrade to Node 6.2.0+, avoid the `--preserve-symlink` flag, or avoid using `npm link ava`.'); +} + +let env = process.env; + +// Ensure NODE_PATH paths are absolute +if (env.NODE_PATH) { + env = Object.assign({}, env); + + env.NODE_PATH = env.NODE_PATH + .split(path.delimiter) + .map(x => path.resolve(x)) + .join(path.delimiter); +} + +// In case the test file imports a different AVA install, +// the presence of this variable allows it to require this one instead +env.AVA_PATH = path.resolve(__dirname, '..'); + +module.exports = (file, opts, execArgv) => { + opts = Object.assign({ + file, + baseDir: process.cwd(), + tty: process.stdout.isTTY ? { + columns: process.stdout.columns, + rows: process.stdout.rows + } : false + }, opts); + + const args = [JSON.stringify(opts), opts.color ? '--color' : '--no-color']; + + const ps = childProcess.fork(path.join(__dirname, 'test-worker.js'), args, { + cwd: opts.projectDir, + silent: true, + env, + execArgv: execArgv || process.execArgv + }); + + const relFile = path.relative('.', file); + + let exiting = false; + const send = (name, data) => { + if (!exiting) { + // This seems to trigger a Node bug which kills the AVA master process, at + // least while running AVA's tests. See + // <https://github.com/novemberborn/_ava-tap-crash> for more details. + ps.send({ + name: `ava-${name}`, + data, + ava: true + }); + } + }; + + const testResults = []; + let results; + + const promise = new Promise((resolve, reject) => { + ps.on('error', reject); + + // Emit `test` and `stats` events + ps.on('message', event => { + if (!event.ava) { + return; + } + + event.name = event.name.replace(/^ava-/, ''); + event.data.file = relFile; + + debug('ipc %s:\n%o', event.name, event.data); + + ps.emit(event.name, event.data); + }); + + ps.on('test', props => { + testResults.push(props); + }); + + ps.on('results', data => { + results = data; + data.tests = testResults; + send('teardown'); + }); + + ps.on('exit', (code, signal) => { + if (code > 0) { + return reject(new AvaError(`${relFile} exited with a non-zero exit code: ${code}`)); + } + + if (code === null && signal) { + return reject(new AvaError(`${relFile} exited due to ${signal}`)); + } + + if (results) { + resolve(results); + } 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}`; + + if (!data.avaRequired) { + message += ', make sure to import "ava" at the top of your test file'; + } + + reject(new AvaError(message)); + }); + }); + + // Teardown finished, now exit + ps.on('teardown', () => { + send('exit'); + exiting = true; + }); + + // Uncaught exception in fork, need to exit + ps.on('uncaughtException', () => { + send('teardown'); + }); + + ps.stdout.on('data', data => { + ps.emit('stdout', data); + }); + + ps.stderr.on('data', data => { + ps.emit('stderr', data); + }); + + promise.on = function () { + ps.on.apply(ps, arguments); + 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; + }; + + return promise; +}; diff --git a/node_modules/ava/lib/format-assert-error.js b/node_modules/ava/lib/format-assert-error.js new file mode 100644 index 000000000..a899af463 --- /dev/null +++ b/node_modules/ava/lib/format-assert-error.js @@ -0,0 +1,121 @@ +'use strict'; +const prettyFormat = require('@ava/pretty-format'); +const reactTestPlugin = require('@ava/pretty-format/plugins/ReactTestComponent'); +const chalk = require('chalk'); +const diff = require('diff'); +const DiffMatchPatch = require('diff-match-patch'); +const indentString = require('indent-string'); +const globals = require('./globals'); + +function formatValue(value, options) { + return prettyFormat(value, Object.assign({ + callToJSON: false, + plugins: [reactTestPlugin], + highlight: globals.options.color !== false + }, options)); +} +exports.formatValue = formatValue; + +const cleanUp = line => { + if (line[0] === '+') { + return `${chalk.green('+')} ${line.slice(1)}`; + } + + if (line[0] === '-') { + return `${chalk.red('-')} ${line.slice(1)}`; + } + + if (line.match(/@@/)) { + return null; + } + + if (line.match(/\\ No newline/)) { + return null; + } + + return ` ${line}`; +}; + +const getType = value => { + const type = typeof value; + if (type === 'object') { + if (type === null) { + return 'null'; + } + if (Array.isArray(value)) { + return 'array'; + } + } + return type; +}; + +function formatDiff(actual, expected) { + const actualType = getType(actual); + const expectedType = getType(expected); + if (actualType !== expectedType) { + return null; + } + + if (actualType === 'array' || actualType === 'object') { + const formatted = diff.createPatch('string', formatValue(actual), formatValue(expected)) + .split('\n') + .slice(4) + .map(cleanUp) + .filter(Boolean) + .join('\n') + .trimRight(); + + return {label: 'Difference:', formatted}; + } + + if (actualType === 'string') { + const formatted = new DiffMatchPatch() + .diff_main(formatValue(actual, {highlight: false}), formatValue(expected, {highlight: false})) + .map(part => { + if (part[0] === 1) { + return chalk.bgGreen.black(part[1]); + } + + if (part[0] === -1) { + return chalk.bgRed.black(part[1]); + } + + return chalk.red(part[1]); + }) + .join('') + .trimRight(); + + return {label: 'Difference:', formatted}; + } + + return null; +} +exports.formatDiff = formatDiff; + +function formatWithLabel(label, value) { + return {label, formatted: formatValue(value)}; +} +exports.formatWithLabel = formatWithLabel; + +function formatSerializedError(error) { + if (error.statements.length === 0 && error.values.length === 0) { + return null; + } + + let result = error.values + .map(value => `${value.label}\n\n${indentString(value.formatted, 2).trimRight()}\n`) + .join('\n'); + + if (error.statements.length > 0) { + if (error.values.length > 0) { + result += '\n'; + } + + result += error.statements + .map(statement => `${statement[0]}\n${chalk.grey('=>')} ${statement[1]}\n`) + .join('\n'); + } + + return result; +} +exports.formatSerializedError = formatSerializedError; diff --git a/node_modules/ava/lib/globals.js b/node_modules/ava/lib/globals.js new file mode 100644 index 000000000..51176c113 --- /dev/null +++ b/node_modules/ava/lib/globals.js @@ -0,0 +1,10 @@ +'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/node_modules/ava/lib/logger.js b/node_modules/ava/lib/logger.js new file mode 100644 index 000000000..54bd23c94 --- /dev/null +++ b/node_modules/ava/lib/logger.js @@ -0,0 +1,81 @@ +'use strict'; +const autoBind = require('auto-bind'); + +class Logger { + constructor(reporter) { + this.reporter = reporter; + autoBind(this); + } + start(runStatus) { + if (!this.reporter.start) { + return; + } + + this.write(this.reporter.start(runStatus), runStatus); + } + reset(runStatus) { + if (!this.reporter.reset) { + return; + } + + this.write(this.reporter.reset(runStatus), runStatus); + } + test(test, runStatus) { + this.write(this.reporter.test(test, runStatus), runStatus); + } + unhandledError(err, runStatus) { + if (!this.reporter.unhandledError) { + return; + } + + this.write(this.reporter.unhandledError(err, runStatus), runStatus); + } + finish(runStatus) { + if (!this.reporter.finish) { + return; + } + + this.write(this.reporter.finish(runStatus), runStatus); + } + section() { + if (!this.reporter.section) { + return; + } + + this.write(this.reporter.section()); + } + clear() { + if (!this.reporter.clear) { + return false; + } + + this.write(this.reporter.clear()); + return true; + } + write(str, runStatus) { + if (typeof str === 'undefined') { + return; + } + + this.reporter.write(str, runStatus); + } + stdout(data, runStatus) { + if (!this.reporter.stdout) { + return; + } + + this.reporter.stdout(data, runStatus); + } + stderr(data, runStatus) { + if (!this.reporter.stderr) { + return; + } + + this.reporter.stderr(data, runStatus); + } + exit(code) { + process.exit(code); // eslint-disable-line unicorn/no-process-exit + } +} + +module.exports = Logger; diff --git a/node_modules/ava/lib/main.js b/node_modules/ava/lib/main.js new file mode 100644 index 000000000..52618e8b7 --- /dev/null +++ b/node_modules/ava/lib/main.js @@ -0,0 +1,103 @@ +'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 = globals.options; +const runner = new Runner({ + bail: opts.failFast, + failWithoutAssertions: opts.failWithoutAssertions, + file: opts.file, + match: opts.match, + serial: opts.serial, + 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; + +Error.stackTraceLimit = Infinity; + +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(); + }); +}); + +module.exports = runner.chain; + +// TypeScript imports the `default` property for +// an ES2015 default import (`import test from 'ava'`) +// See: https://github.com/Microsoft/TypeScript/issues/2242#issuecomment-83694181 +module.exports.default = runner.chain; diff --git a/node_modules/ava/lib/prefix-title.js b/node_modules/ava/lib/prefix-title.js new file mode 100644 index 000000000..a1c7b4f3b --- /dev/null +++ b/node_modules/ava/lib/prefix-title.js @@ -0,0 +1,21 @@ +'use strict'; +const path = require('path'); + +module.exports = (file, base, separator) => { + let prefix = file + // Only replace this.base if it is found at the start of the path + .replace(base, (match, offset) => offset === 0 ? '' : match) + .replace(/\.spec/, '') + .replace(/\.test/, '') + .replace(/test-/g, '') + .replace(/\.js$/, '') + .split(path.sep) + .filter(p => p !== '__tests__') + .join(separator); + + if (prefix.length > 0) { + prefix += separator; + } + + return prefix; +}; diff --git a/node_modules/ava/lib/process-adapter.js b/node_modules/ava/lib/process-adapter.js new file mode 100644 index 000000000..b50f37398 --- /dev/null +++ b/node_modules/ava/lib/process-adapter.js @@ -0,0 +1,103 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const debug = require('debug')('ava'); +const sourceMapSupport = require('source-map-support'); +const installPrecompiler = require('require-precompiled'); + +// Parse and re-emit AVA messages +process.on('message', message => { + if (!message.ava) { + return; + } + + process.emit(message.name, message.data); +}); + +exports.send = (name, data) => { + process.send({ + name: `ava-${name}`, + data, + ava: true + }); +}; + +// `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 opts = JSON.parse(process.argv[2]); +exports.opts = opts; + +// Fake TTY support +if (opts.tty) { + process.stdout.isTTY = true; + process.stdout.columns = opts.tty.columns || 80; + process.stdout.rows = opts.tty.rows; + + const tty = require('tty'); + const isatty = tty.isatty; + + tty.isatty = function (fd) { + if (fd === 1 || fd === process.stdout) { + return true; + } + + return isatty(fd); + }; +} + +if (debug.enabled) { + // Forward the `time-require` `--sorted` flag. + // Intended for internal optimization tests only. + if (opts._sorted) { + process.argv.push('--sorted'); + } + + require('time-require'); // eslint-disable-line import/no-unassigned-import +} + +const sourceMapCache = new Map(); +const cacheDir = opts.cacheDir; + +exports.installSourceMapSupport = () => { + sourceMapSupport.install({ + environment: 'node', + handleUncaughtExceptions: false, + retrieveSourceMap(source) { + if (sourceMapCache.has(source)) { + return { + url: source, + map: fs.readFileSync(sourceMapCache.get(source), 'utf8') + }; + } + } + }); +}; + +exports.installPrecompilerHook = () => { + installPrecompiler(filename => { + const precompiled = opts.precompiled[filename]; + + if (precompiled) { + sourceMapCache.set(filename, path.join(cacheDir, `${precompiled}.js.map`)); + return fs.readFileSync(path.join(cacheDir, `${precompiled}.js`), 'utf8'); + } + + return null; + }); +}; + +exports.installDependencyTracking = (dependencies, testPath) => { + Object.keys(require.extensions).forEach(ext => { + const wrappedHandler = require.extensions[ext]; + + require.extensions[ext] = (module, filename) => { + if (filename !== testPath) { + dependencies.push(filename); + } + + wrappedHandler(module, filename); + }; + }); +}; diff --git a/node_modules/ava/lib/reporters/improper-usage-messages.js b/node_modules/ava/lib/reporters/improper-usage-messages.js new file mode 100644 index 000000000..0a2626638 --- /dev/null +++ b/node_modules/ava/lib/reporters/improper-usage-messages.js @@ -0,0 +1,21 @@ +'use strict'; +const chalk = require('chalk'); + +exports.forError = error => { + if (!error.improperUsage) { + return null; + } + + const assertion = error.assertion; + if (assertion !== 'throws' || !assertion === 'notThrows') { + return null; + } + + return `Try wrapping the first argument to \`t.${assertion}()\` in a function: + + ${chalk.cyan(`t.${assertion}(() => { `)}${chalk.grey('/* your code here */')}${chalk.cyan(' })')} + +Visit the following URL for more details: + + ${chalk.blue.underline('https://github.com/avajs/ava#throwsfunctionpromise-error-message')}`; +}; diff --git a/node_modules/ava/lib/reporters/mini.js b/node_modules/ava/lib/reporters/mini.js new file mode 100644 index 000000000..df481a76a --- /dev/null +++ b/node_modules/ava/lib/reporters/mini.js @@ -0,0 +1,318 @@ +'use strict'; +const StringDecoder = require('string_decoder').StringDecoder; +const cliCursor = require('cli-cursor'); +const lastLineTracker = require('last-line-stream/tracker'); +const plur = require('plur'); +const spinners = require('cli-spinners'); +const chalk = require('chalk'); +const cliTruncate = require('cli-truncate'); +const cross = require('figures').cross; +const indentString = require('indent-string'); +const formatAssertError = require('../format-assert-error'); +const extractStack = require('../extract-stack'); +const codeExcerpt = require('../code-excerpt'); +const colors = require('../colors'); +const improperUsageMessages = require('./improper-usage-messages'); + +// TODO(@jamestalamge): This should be fixed in log-update and ansi-escapes once we are confident it's a good solution. +const CSI = '\u001B['; +const ERASE_LINE = CSI + '2K'; +const CURSOR_TO_COLUMN_0 = CSI + '0G'; +const CURSOR_UP = CSI + '1A'; + +// Returns a string that will erase `count` lines from the end of the terminal. +function eraseLines(count) { + let clear = ''; + + for (let i = 0; i < count; i++) { + clear += ERASE_LINE + (i < count - 1 ? CURSOR_UP : ''); + } + + if (count) { + clear += CURSOR_TO_COLUMN_0; + } + + return clear; +} + +class MiniReporter { + constructor(options) { + this.options = Object.assign({}, options); + + chalk.enabled = this.options.color; + for (const key of Object.keys(colors)) { + colors[key].enabled = this.options.color; + } + + const spinnerDef = spinners[process.platform === 'win32' ? 'line' : 'dots']; + this.spinnerFrames = spinnerDef.frames.map(c => chalk.gray.dim(c)); + this.spinnerInterval = spinnerDef.interval; + + this.reset(); + this.stream = process.stderr; + this.stringDecoder = new StringDecoder(); + } + start() { + this.interval = setInterval(() => { + this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length; + this.write(this.prefix()); + }, this.spinnerInterval); + + return this.prefix(''); + } + reset() { + this.clearInterval(); + this.passCount = 0; + this.knownFailureCount = 0; + this.failCount = 0; + this.skipCount = 0; + this.todoCount = 0; + this.rejectionCount = 0; + this.exceptionCount = 0; + this.currentStatus = ''; + this.currentTest = ''; + this.statusLineCount = 0; + this.spinnerIndex = 0; + this.lastLineTracker = lastLineTracker(); + } + spinnerChar() { + return this.spinnerFrames[this.spinnerIndex]; + } + clearInterval() { + clearInterval(this.interval); + this.interval = null; + } + test(test) { + if (test.todo) { + this.todoCount++; + } else if (test.skip) { + this.skipCount++; + } else if (test.error) { + this.failCount++; + } else { + this.passCount++; + if (test.failing) { + this.knownFailureCount++; + } + } + + if (test.todo || test.skip) { + return; + } + + return this.prefix(this._test(test)); + } + prefix(str) { + str = str || this.currentTest; + this.currentTest = str; + + // The space before the newline is required for proper formatting + // TODO(jamestalmage): Figure out why it's needed and document it here + return ` \n ${this.spinnerChar()} ${str}`; + } + _test(test) { + const SPINNER_WIDTH = 3; + const PADDING = 1; + let title = cliTruncate(test.title, process.stdout.columns - SPINNER_WIDTH - PADDING); + + if (test.error || test.failing) { + title = colors.error(test.title); + } + + return title + '\n' + this.reportCounts(); + } + unhandledError(err) { + if (err.type === 'exception') { + this.exceptionCount++; + } else { + this.rejectionCount++; + } + } + reportCounts(time) { + const lines = [ + this.passCount > 0 ? '\n ' + colors.pass(this.passCount, 'passed') : '', + this.knownFailureCount > 0 ? '\n ' + colors.error(this.knownFailureCount, plur('known failure', this.knownFailureCount)) : '', + this.failCount > 0 ? '\n ' + colors.error(this.failCount, 'failed') : '', + this.skipCount > 0 ? '\n ' + colors.skip(this.skipCount, 'skipped') : '', + this.todoCount > 0 ? '\n ' + colors.todo(this.todoCount, 'todo') : '' + ].filter(Boolean); + + if (time && lines.length > 0) { + lines[0] += ' ' + time; + } + + return lines.join(''); + } + finish(runStatus) { + this.clearInterval(); + let time; + + if (this.options.watching) { + time = chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']'); + } + + let status = this.reportCounts(time); + + if (this.rejectionCount > 0) { + status += '\n ' + colors.error(this.rejectionCount, plur('rejection', this.rejectionCount)); + } + + if (this.exceptionCount > 0) { + status += '\n ' + colors.error(this.exceptionCount, plur('exception', this.exceptionCount)); + } + + if (runStatus.previousFailCount > 0) { + status += '\n ' + colors.error(runStatus.previousFailCount, 'previous', plur('failure', runStatus.previousFailCount), 'in test files that were not rerun'); + } + + if (this.knownFailureCount > 0) { + for (const test of runStatus.knownFailures) { + const title = test.title; + status += '\n\n ' + colors.title(title); + // TODO: Output description with link + // status += colors.stack(description); + } + } + + if (this.failCount > 0) { + runStatus.errors.forEach((test, index) => { + if (!test.error) { + return; + } + + const beforeSpacing = index === 0 ? '\n\n' : '\n\n\n\n'; + + status += beforeSpacing + ' ' + colors.title(test.title) + '\n'; + if (test.error.source) { + status += ' ' + colors.errorSource(test.error.source.file + ':' + test.error.source.line) + '\n'; + + const excerpt = codeExcerpt(test.error.source, {maxWidth: process.stdout.columns}); + if (excerpt) { + status += '\n' + indentString(excerpt, 2) + '\n'; + } + } + + if (test.error.message) { + status += '\n' + indentString(test.error.message, 2) + '\n'; + } + + if (test.error.avaAssertionError) { + const formatted = formatAssertError.formatSerializedError(test.error); + if (formatted) { + status += '\n' + indentString(formatted, 2); + } + + const message = improperUsageMessages.forError(test.error); + if (message) { + status += '\n' + indentString(message, 2) + '\n'; + } + } + + if (test.error.stack) { + const extracted = extractStack(test.error.stack); + if (extracted.includes('\n')) { + status += '\n' + indentString(colors.errorStack(extracted), 2); + } + } + }); + } + + if (this.rejectionCount > 0 || this.exceptionCount > 0) { + // TODO(sindresorhus): Figure out why this causes a test failure when switched to a for-of loop + runStatus.errors.forEach(err => { + if (err.title) { + return; + } + + if (err.type === 'exception' && err.name === 'AvaError') { + status += '\n\n ' + colors.error(cross + ' ' + err.message); + } else { + const title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception'; + let description = err.stack ? err.stack.trimRight() : JSON.stringify(err); + description = description.split('\n'); + const errorTitle = err.name ? description[0] : 'Threw non-error: ' + description[0]; + const errorStack = description.slice(1).join('\n'); + + status += '\n\n ' + colors.title(title) + '\n'; + status += ' ' + colors.stack(errorTitle) + '\n'; + status += colors.errorStack(errorStack); + } + }); + } + + 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 += '\n\n ' + colors.information('`--fail-fast` is on. ' + remaining); + } + + if (runStatus.hasExclusive === true && runStatus.remainingCount > 0) { + status += '\n\n ' + colors.information('The .only() modifier is used in some tests.', runStatus.remainingCount, plur('test', runStatus.remainingCount), plur('was', 'were', runStatus.remainingCount), 'not run'); + } + + return status + '\n\n'; + } + section() { + return '\n' + chalk.gray.dim('\u2500'.repeat(process.stdout.columns || 80)); + } + clear() { + return ''; + } + write(str) { + cliCursor.hide(); + this.currentStatus = str; + this._update(); + this.statusLineCount = this.currentStatus.split('\n').length; + } + stdout(data) { + this._update(data); + } + stderr(data) { + this._update(data); + } + _update(data) { + let str = ''; + let ct = this.statusLineCount; + const columns = process.stdout.columns; + let lastLine = this.lastLineTracker.lastLine(); + + // Terminals automatically wrap text. We only need the last log line as seen on the screen. + lastLine = lastLine.substring(lastLine.length - (lastLine.length % columns)); + + // Don't delete the last log line if it's completely empty. + if (lastLine.length > 0) { + ct++; + } + + // Erase the existing status message, plus the last log line. + str += eraseLines(ct); + + // Rewrite the last log line. + str += lastLine; + + if (str.length > 0) { + this.stream.write(str); + } + + if (data) { + // Send new log data to the terminal, and update the last line status. + this.lastLineTracker.update(this.stringDecoder.write(data)); + this.stream.write(data); + } + + let currentStatus = this.currentStatus; + + if (currentStatus.length > 0) { + lastLine = this.lastLineTracker.lastLine(); + // We need a newline at the end of the last log line, before the status message. + // However, if the last log line is the exact width of the terminal a newline is implied, + // and adding a second will cause problems. + if (lastLine.length % columns) { + currentStatus = '\n' + currentStatus; + } + // Rewrite the status message. + this.stream.write(currentStatus); + } + } +} + +module.exports = MiniReporter; diff --git a/node_modules/ava/lib/reporters/tap.js b/node_modules/ava/lib/reporters/tap.js new file mode 100644 index 000000000..37c2cfd95 --- /dev/null +++ b/node_modules/ava/lib/reporters/tap.js @@ -0,0 +1,119 @@ +'use strict'; +const format = require('util').format; +const indentString = require('indent-string'); +const stripAnsi = require('strip-ansi'); +const yaml = require('js-yaml'); +const extractStack = require('../extract-stack'); + +// Parses stack trace and extracts original function name, file name and line +function getSourceFromStack(stack) { + return extractStack(stack).split('\n')[0]; +} + +function dumpError(error, includeMessage) { + const obj = Object.assign({}, error.object); + if (error.name) { + obj.name = error.name; + } + if (includeMessage && error.message) { + obj.message = error.message; + } + + if (error.avaAssertionError) { + if (error.assertion) { + obj.assertion = error.assertion; + } + if (error.operator) { + obj.operator = error.operator; + } + if (error.values.length > 0) { + obj.values = error.values.reduce((acc, value) => { + acc[value.label] = stripAnsi(value.formatted); + return acc; + }, {}); + } + } + + if (error.stack) { + obj.at = getSourceFromStack(error.stack); + } + + return ` ---\n${indentString(yaml.safeDump(obj).trim(), 4)}\n ...`; +} + +class TapReporter { + constructor() { + this.i = 0; + } + start() { + return 'TAP version 13'; + } + test(test) { + let output; + + let directive = ''; + const passed = test.todo ? 'not ok' : 'ok'; + + if (test.todo) { + directive = '# TODO'; + } else if (test.skip) { + directive = '# SKIP'; + } + + const title = stripAnsi(test.title); + + if (test.error) { + output = [ + '# ' + title, + format('not ok %d - %s', ++this.i, title), + dumpError(test.error, true) + ]; + } else { + output = [ + `# ${title}`, + format('%s %d - %s %s', passed, ++this.i, title, directive).trim() + ]; + } + + return output.join('\n'); + } + unhandledError(err) { + const output = [ + `# ${err.message}`, + format('not ok %d - %s', ++this.i, err.message) + ]; + // AvaErrors don't have stack traces + if (err.type !== 'exception' || err.name !== 'AvaError') { + output.push(dumpError(err, false)); + } + + return output.join('\n'); + } + finish(runStatus) { + const output = [ + '', + '1..' + (runStatus.passCount + runStatus.failCount + runStatus.skipCount), + '# tests ' + (runStatus.passCount + runStatus.failCount + runStatus.skipCount), + '# pass ' + runStatus.passCount + ]; + + if (runStatus.skipCount > 0) { + output.push(`# skip ${runStatus.skipCount}`); + } + + output.push('# fail ' + (runStatus.failCount + runStatus.rejectionCount + runStatus.exceptionCount), ''); + + return output.join('\n'); + } + write(str) { + console.log(str); + } + stdout(data) { + process.stderr.write(data); + } + stderr(data) { + this.stdout(data); + } +} + +module.exports = TapReporter; diff --git a/node_modules/ava/lib/reporters/verbose.js b/node_modules/ava/lib/reporters/verbose.js new file mode 100644 index 000000000..1be43ce5e --- /dev/null +++ b/node_modules/ava/lib/reporters/verbose.js @@ -0,0 +1,165 @@ +'use strict'; +const indentString = require('indent-string'); +const prettyMs = require('pretty-ms'); +const figures = require('figures'); +const chalk = require('chalk'); +const plur = require('plur'); +const formatAssertError = require('../format-assert-error'); +const extractStack = require('../extract-stack'); +const codeExcerpt = require('../code-excerpt'); +const colors = require('../colors'); +const improperUsageMessages = require('./improper-usage-messages'); + +class VerboseReporter { + constructor(options) { + this.options = Object.assign({}, options); + + chalk.enabled = this.options.color; + for (const key of Object.keys(colors)) { + colors[key].enabled = this.options.color; + } + } + start() { + return ''; + } + test(test, runStatus) { + if (test.error) { + return ' ' + colors.error(figures.cross) + ' ' + test.title + ' ' + colors.error(test.error.message); + } + + if (test.todo) { + return ' ' + colors.todo('- ' + test.title); + } else if (test.skip) { + return ' ' + colors.skip('- ' + test.title); + } + + if (test.failing) { + return ' ' + colors.error(figures.tick) + ' ' + colors.error(test.title); + } + + if (runStatus.fileCount === 1 && runStatus.testCount === 1 && test.title === '[anonymous]') { + return undefined; + } + + // Display duration only over a threshold + const threshold = 100; + const duration = test.duration > threshold ? colors.duration(' (' + prettyMs(test.duration) + ')') : ''; + + return ' ' + colors.pass(figures.tick) + ' ' + test.title + duration; + } + unhandledError(err) { + if (err.type === 'exception' && err.name === 'AvaError') { + return colors.error(' ' + figures.cross + ' ' + err.message); + } + + const types = { + rejection: 'Unhandled Rejection', + exception: 'Uncaught Exception' + }; + + let output = colors.error(types[err.type] + ':', err.file) + '\n'; + + if (err.stack) { + output += ' ' + colors.stack(err.stack) + '\n'; + } else { + output += ' ' + colors.stack(JSON.stringify(err)) + '\n'; + } + + output += '\n'; + + return output; + } + finish(runStatus) { + let output = '\n'; + + const lines = [ + runStatus.failCount > 0 ? + ' ' + colors.error(runStatus.failCount, plur('test', runStatus.failCount), 'failed') : + ' ' + colors.pass(runStatus.passCount, plur('test', runStatus.passCount), 'passed'), + runStatus.knownFailureCount > 0 ? ' ' + colors.error(runStatus.knownFailureCount, plur('known failure', runStatus.knownFailureCount)) : '', + runStatus.skipCount > 0 ? ' ' + colors.skip(runStatus.skipCount, plur('test', runStatus.skipCount), 'skipped') : '', + runStatus.todoCount > 0 ? ' ' + colors.todo(runStatus.todoCount, plur('test', runStatus.todoCount), 'todo') : '', + runStatus.rejectionCount > 0 ? ' ' + colors.error(runStatus.rejectionCount, 'unhandled', plur('rejection', runStatus.rejectionCount)) : '', + runStatus.exceptionCount > 0 ? ' ' + colors.error(runStatus.exceptionCount, 'uncaught', plur('exception', runStatus.exceptionCount)) : '', + runStatus.previousFailCount > 0 ? ' ' + colors.error(runStatus.previousFailCount, 'previous', plur('failure', runStatus.previousFailCount), 'in test files that were not rerun') : '' + ].filter(Boolean); + + if (lines.length > 0) { + lines[0] += ' ' + chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']'); + output += lines.join('\n'); + } + + if (runStatus.knownFailureCount > 0) { + runStatus.knownFailures.forEach(test => { + output += '\n\n\n ' + colors.error(test.title); + }); + } + + if (runStatus.failCount > 0) { + runStatus.tests.forEach((test, index) => { + if (!test.error) { + return; + } + + const beforeSpacing = index === 0 ? '\n\n' : '\n\n\n\n'; + output += beforeSpacing + ' ' + colors.title(test.title) + '\n'; + if (test.error.source) { + output += ' ' + colors.errorSource(test.error.source.file + ':' + test.error.source.line) + '\n'; + + const excerpt = codeExcerpt(test.error.source, {maxWidth: process.stdout.columns}); + if (excerpt) { + output += '\n' + indentString(excerpt, 2) + '\n'; + } + } + + if (test.error.message) { + output += '\n' + indentString(test.error.message, 2) + '\n'; + } + + if (test.error.avaAssertionError) { + const formatted = formatAssertError.formatSerializedError(test.error); + if (formatted) { + output += '\n' + indentString(formatted, 2); + } + + const message = improperUsageMessages.forError(test.error); + if (message) { + output += '\n' + indentString(message, 2) + '\n'; + } + } + + if (test.error.stack) { + const extracted = extractStack(test.error.stack); + if (extracted.includes('\n')) { + output += '\n' + indentString(colors.errorStack(extracted), 2); + } + } + }); + } + + 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 += '\n\n\n ' + colors.information('`--fail-fast` is on. ' + remaining); + } + + if (runStatus.hasExclusive === true && runStatus.remainingCount > 0) { + output += '\n\n\n ' + colors.information('The .only() modifier is used in some tests.', runStatus.remainingCount, plur('test', runStatus.remainingCount), plur('was', 'were', runStatus.remainingCount), 'not run'); + } + + return output + '\n'; + } + section() { + return chalk.gray.dim('\u2500'.repeat(process.stdout.columns || 80)); + } + write(str) { + console.error(str); + } + stdout(data) { + process.stderr.write(data); + } + stderr(data) { + process.stderr.write(data); + } +} + +module.exports = VerboseReporter; diff --git a/node_modules/ava/lib/run-status.js b/node_modules/ava/lib/run-status.js new file mode 100644 index 000000000..6526f7bdc --- /dev/null +++ b/node_modules/ava/lib/run-status.js @@ -0,0 +1,125 @@ +'use strict'; +const EventEmitter = require('events'); +const chalk = require('chalk'); +const flatten = require('arr-flatten'); +const figures = require('figures'); +const autoBind = require('auto-bind'); +const prefixTitle = require('./prefix-title'); + +function sum(arr, key) { + let result = 0; + + arr.forEach(item => { + result += item[key]; + }); + + return result; +} + +class RunStatus extends EventEmitter { + constructor(opts) { + super(); + + opts = opts || {}; + this.prefixTitles = opts.prefixTitles !== false; + this.hasExclusive = Boolean(opts.runOnlyExclusive); + this.base = opts.base || ''; + this.rejectionCount = 0; + this.exceptionCount = 0; + this.passCount = 0; + this.knownFailureCount = 0; + this.skipCount = 0; + this.todoCount = 0; + this.failCount = 0; + this.fileCount = 0; + this.testCount = 0; + this.remainingCount = 0; + this.previousFailCount = 0; + this.knownFailures = []; + this.errors = []; + this.stats = []; + this.tests = []; + this.failFastEnabled = opts.failFast || false; + + autoBind(this); + } + observeFork(emitter) { + emitter + .on('teardown', this.handleTeardown) + .on('stats', this.handleStats) + .on('test', this.handleTest) + .on('unhandledRejections', this.handleRejections) + .on('uncaughtException', this.handleExceptions) + .on('stdout', this.handleOutput.bind(this, 'stdout')) + .on('stderr', this.handleOutput.bind(this, 'stderr')); + } + handleRejections(data) { + this.rejectionCount += data.rejections.length; + + data.rejections.forEach(err => { + err.type = 'rejection'; + err.file = data.file; + this.emit('error', err, this); + this.errors.push(err); + }); + } + handleExceptions(data) { + this.exceptionCount++; + const err = data.exception; + err.type = 'exception'; + err.file = data.file; + this.emit('error', err, this); + this.errors.push(err); + } + handleTeardown(data) { + this.emit('dependencies', data.file, data.dependencies, this); + } + handleStats(stats) { + this.emit('stats', stats, this); + + if (stats.hasExclusive) { + this.hasExclusive = true; + } + + this.testCount += stats.testCount; + } + handleTest(test) { + test.title = this.prefixTitle(test.file) + test.title; + + if (test.error) { + this.errors.push(test); + } + + if (test.failing && !test.error) { + this.knownFailures.push(test); + } + + this.emit('test', test, this); + } + prefixTitle(file) { + if (!this.prefixTitles) { + return ''; + } + + const separator = ' ' + chalk.gray.dim(figures.pointerSmall) + ' '; + + return prefixTitle(file, this.base, separator); + } + handleOutput(channel, data) { + this.emit(channel, data, this); + } + processResults(results) { + // Assemble stats from all tests + this.stats = results.map(result => result.stats); + this.tests = results.map(result => result.tests); + this.tests = flatten(this.tests); + this.passCount = sum(this.stats, 'passCount'); + this.knownFailureCount = sum(this.stats, 'knownFailureCount'); + this.skipCount = sum(this.stats, 'skipCount'); + this.todoCount = sum(this.stats, 'todoCount'); + this.failCount = sum(this.stats, 'failCount'); + this.remainingCount = this.testCount - this.passCount - this.failCount - this.skipCount - this.todoCount - this.knownFailureCount; + } +} + +module.exports = RunStatus; diff --git a/node_modules/ava/lib/runner.js b/node_modules/ava/lib/runner.js new file mode 100644 index 000000000..5f0edacb2 --- /dev/null +++ b/node_modules/ava/lib/runner.js @@ -0,0 +1,221 @@ +'use strict'; +const EventEmitter = require('events'); +const path = require('path'); +const Bluebird = require('bluebird'); +const jestSnapshot = require('jest-snapshot'); +const optionChain = require('option-chain'); +const matcher = require('matcher'); +const TestCollection = require('./test-collection'); +const validateTest = require('./validate-test'); + +const chainableMethods = { + defaults: { + type: 'test', + serial: false, + exclusive: false, + skipped: false, + todo: false, + failing: false, + callback: false, + always: false + }, + chainableMethods: { + test: {}, + serial: {serial: true}, + before: {type: 'before'}, + after: {type: 'after'}, + skip: {skipped: true}, + todo: {todo: true}, + failing: {failing: true}, + only: {exclusive: true}, + beforeEach: {type: 'beforeEach'}, + afterEach: {type: 'afterEach'}, + cb: {callback: true}, + always: {always: true} + } +}; + +function wrapFunction(fn, args) { + return function (t) { + return fn.apply(this, [t].concat(args)); + }; +} + +class Runner extends EventEmitter { + constructor(options) { + super(); + + options = options || {}; + + this.file = options.file; + this.match = options.match || []; + this.serial = options.serial; + this.updateSnapshots = options.updateSnapshots; + + this.hasStarted = false; + this.results = []; + this.snapshotState = null; + this.tests = new TestCollection({ + bail: options.bail, + failWithoutAssertions: options.failWithoutAssertions, + getSnapshotState: () => this.getSnapshotState() + }); + + this.chain = optionChain(chainableMethods, (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; + } + + if (args.length > macroArgIndex) { + args = args.slice(macroArgIndex); + } else { + args = null; + } + + if (Array.isArray(fn)) { + fn.forEach(fn => { + this.addTest(title, opts, fn, args); + }); + } else { + this.addTest(title, opts, fn, args); + } + }); + } + + addTest(title, metadata, fn, args) { + if (args) { + if (fn.title) { + title = fn.title.apply(fn, [title || ''].concat(args)); + } + + fn = wrapFunction(fn, args); + } + + if (metadata.type === 'test' && this.match.length > 0) { + metadata.exclusive = title !== null && matcher([title], this.match).length === 1; + } + + const validationError = validateTest(title, fn, metadata); + if (validationError !== null) { + throw new TypeError(validationError); + } + + this.tests.add({ + metadata, + fn, + title + }); + } + + addTestResult(result) { + const test = result.result; + const props = { + 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.results.push(result); + this.emit('test', props); + } + + buildStats() { + const stats = { + failCount: 0, + knownFailureCount: 0, + passCount: 0, + skipCount: 0, + testCount: 0, + todoCount: 0 + }; + + for (const result of this.results) { + if (!result.passed) { + // Includes hooks + stats.failCount++; + } + + 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++; + } + } + } + } + + return stats; + } + + getSnapshotState() { + if (this.snapshotState) { + return this.snapshotState; + } + + const name = path.basename(this.file) + '.snap'; + const dir = path.dirname(this.file); + + const snapshotPath = path.join(dir, '__snapshots__', name); + const testPath = this.file; + const update = this.updateSnapshots; + + const state = jestSnapshot.initializeSnapshotState(testPath, update, snapshotPath); + this.snapshotState = state; + return state; + } + + saveSnapshotState() { + if (this.snapshotState) { + this.snapshotState.save(this.updateSnapshots); + } + } + + run(options) { + if (options.runOnlyExclusive && !this.tests.hasExclusive) { + return Promise.resolve(null); + } + + this.hasStarted = true; + this.tests.on('test', result => { + this.addTestResult(result); + }); + return Bluebird.try(() => this.tests.build().run()); + } + attributeLeakedError(err) { + return this.tests.attributeLeakedError(err); + } +} + +module.exports = Runner; diff --git a/node_modules/ava/lib/sequence.js b/node_modules/ava/lib/sequence.js new file mode 100644 index 000000000..1e5960a98 --- /dev/null +++ b/node_modules/ava/lib/sequence.js @@ -0,0 +1,94 @@ +'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/node_modules/ava/lib/serialize-error.js b/node_modules/ava/lib/serialize-error.js new file mode 100644 index 000000000..55717e161 --- /dev/null +++ b/node_modules/ava/lib/serialize-error.js @@ -0,0 +1,94 @@ +'use strict'; +const path = require('path'); +const cleanYamlObject = require('clean-yaml-object'); +const StackUtils = require('stack-utils'); +const assert = require('./assert'); +const beautifyStack = require('./beautify-stack'); +const extractStack = require('./extract-stack'); + +function isAvaAssertionError(source) { + return source instanceof assert.AssertionError; +} + +function filter(propertyName, isRoot) { + return !isRoot || (propertyName !== 'message' && propertyName !== 'name' && propertyName !== 'stack'); +} + +const stackUtils = new StackUtils(); +function extractSource(stack) { + if (!stack) { + return null; + } + + const firstStackLine = extractStack(stack).split('\n')[0]; + return stackUtils.parseLine(firstStackLine); +} +function buildSource(source) { + if (!source) { + return null; + } + + // Assume the CWD is the project directory. This holds since this function + // is only called in test workers, which are created with their working + // directory set to the project directory. + const projectDir = process.cwd(); + + const file = path.resolve(projectDir, source.file.trim()); + const rel = path.relative(projectDir, file); + + const isWithinProject = rel.split(path.sep)[0] !== '..'; + const isDependency = isWithinProject && path.dirname(rel).split(path.sep).indexOf('node_modules') > -1; + + return { + isDependency, + isWithinProject, + file, + line: source.line + }; +} + +module.exports = error => { + const stack = typeof error.stack === 'string' ? + beautifyStack(error.stack) : + null; + + const retval = { + avaAssertionError: isAvaAssertionError(error), + source: buildSource(extractSource(stack)) + }; + if (stack) { + retval.stack = stack; + } + + if (retval.avaAssertionError) { + retval.improperUsage = error.improperUsage; + retval.message = error.message; + retval.name = error.name; + retval.statements = error.statements; + retval.values = error.values; + + if (error.fixedSource) { + const source = buildSource(error.fixedSource); + if (source) { + retval.source = source; + } + } + + if (error.assertion) { + retval.assertion = error.assertion; + } + if (error.operator) { + retval.operator = error.operator; + } + } else { + retval.object = cleanYamlObject(error, filter); // Cleanly copy non-standard properties + if (typeof error.message === 'string') { + retval.message = error.message; + } + if (typeof error.name === 'string') { + retval.name = error.name; + } + } + + return retval; +}; diff --git a/node_modules/ava/lib/test-collection.js b/node_modules/ava/lib/test-collection.js new file mode 100644 index 000000000..5404cb119 --- /dev/null +++ b/node_modules/ava/lib/test-collection.js @@ -0,0 +1,206 @@ +'use strict'; +const EventEmitter = require('events'); +const fnName = require('fn-name'); +const Concurrent = require('./concurrent'); +const Sequence = require('./sequence'); +const Test = require('./test'); + +class TestCollection extends EventEmitter { + constructor(options) { + super(); + + this.bail = options.bail; + this.failWithoutAssertions = options.failWithoutAssertions; + this.getSnapshotState = options.getSnapshotState; + this.hasExclusive = false; + this.testCount = 0; + + this.tests = { + concurrent: [], + serial: [] + }; + + this.hooks = { + before: [], + beforeEach: [], + after: [], + afterAlways: [], + afterEach: [], + afterEachAlways: [] + }; + + this.pendingTestInstances = new Set(); + + this._emitTestResult = this._emitTestResult.bind(this); + } + add(test) { + const metadata = test.metadata; + const type = metadata.type; + + if (!type) { + throw new Error('Test type must be specified'); + } + + if (!test.title && test.fn) { + test.title = fnName(test.fn); + } + + // Workaround for Babel giving anonymous functions a name + if (test.title === 'callee$0$0') { + test.title = null; + } + + if (!test.title) { + if (type === 'test') { + test.title = '[anonymous]'; + } else { + test.title = type; + } + } + + if (metadata.always && type !== 'after' && type !== 'afterEach') { + throw new Error('"always" can only be used with after and afterEach hooks'); + } + + // Add a hook + if (type !== 'test') { + if (metadata.exclusive) { + throw new Error(`"only" cannot be used with a ${type} hook`); + } + + 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, context) { + return hooks.map(hook => { + const test = this._buildHook(hook, testTitle, context); + + 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}`; + } + + if (!contextRef) { + contextRef = null; + } + + const test = new Test({ + contextRef, + failWithoutAssertions: false, + fn: hook.fn, + getSnapshotState: this.getSnapshotState, + metadata: hook.metadata, + onResult: this._emitTestResult, + title + }); + this.pendingTestInstances.add(test); + return test; + } + _buildTest(test, contextRef) { + if (!contextRef) { + contextRef = null; + } + + test = new Test({ + contextRef, + failWithoutAssertions: this.failWithoutAssertions, + fn: test.fn, + getSnapshotState: this.getSnapshotState, + metadata: test.metadata, + onResult: this._emitTestResult, + title: test.title + }); + this.pendingTestInstances.add(test); + return test; + } + _buildTestWithHooks(test) { + if (test.metadata.skipped || test.metadata.todo) { + return new Sequence([this._skippedTest(this._buildTest(test))], true); + } + + const context = {context: {}}; + + const beforeHooks = this._buildHooks(this.hooks.beforeEach, test.title, context); + const afterHooks = this._buildHooks(this.hooks.afterEach, test.title, context); + + let sequence = new Sequence([].concat(beforeHooks, this._buildTest(test, context), afterHooks), true); + if (this.hooks.afterEachAlways.length > 0) { + const afterAlwaysHooks = new Sequence(this._buildHooks(this.hooks.afterEachAlways, test.title, context)); + sequence = new Sequence([sequence, afterAlwaysHooks], false); + } + return sequence; + } + _buildTests(tests) { + return tests.map(test => this._buildTestWithHooks(test)); + } + build() { + const beforeHooks = new Sequence(this._buildHooks(this.hooks.before)); + const afterHooks = new Sequence(this._buildHooks(this.hooks.after)); + + const serialTests = new Sequence(this._buildTests(this.tests.serial), this.bail); + const concurrentTests = new Concurrent(this._buildTests(this.tests.concurrent), this.bail); + const allTests = new Sequence([serialTests, concurrentTests]); + + let finalTests = new Sequence([beforeHooks, allTests, afterHooks], true); + if (this.hooks.afterAlways.length > 0) { + const afterAlwaysHooks = new Sequence(this._buildHooks(this.hooks.afterAlways)); + 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/node_modules/ava/lib/test-worker.js b/node_modules/ava/lib/test-worker.js new file mode 100644 index 000000000..2df7f745d --- /dev/null +++ b/node_modules/ava/lib/test-worker.js @@ -0,0 +1,130 @@ +'use strict'; + +// Check if the test is being run without AVA cli +{ + /* eslint-disable import/order */ + const path = require('path'); + const chalk = require('chalk'); + + const isForked = typeof process.send === 'function'; + if (!isForked) { + const fp = path.relative('.', process.argv[1]); + + console.log(); + console.error('Test files must be run with the AVA CLI:\n\n ' + chalk.grey.dim('$') + ' ' + chalk.cyan('ava ' + fp) + '\n'); + + process.exit(1); // eslint-disable-line unicorn/no-process-exit + } +} + +/* eslint-enable import/order */ +const Bluebird = require('bluebird'); +const currentlyUnhandled = require('currently-unhandled')(); +const isObj = require('is-obj'); +const adapter = require('./process-adapter'); +const globals = require('./globals'); +const serializeError = require('./serialize-error'); + +const opts = adapter.opts; +const testPath = opts.file; +globals.options = opts; + +// Bluebird specific +Bluebird.longStackTraces(); + +(opts.require || []).forEach(require); + +adapter.installSourceMapSupport(); +adapter.installPrecompilerHook(); + +const dependencies = []; +adapter.installDependencyTracking(dependencies, testPath); + +// Set when main.js is required (since test files should have `require('ava')`). +let runner = null; +exports.setRunner = newRunner => { + runner = newRunner; +}; + +require(testPath); // eslint-disable-line import/no-dynamic-require + +// If AVA was not required, show an error +if (!runner) { + adapter.send('no-tests', {avaRequired: false}); +} + +function attributeLeakedError(err) { + if (!runner) { + return false; + } + + return runner.attributeLeakedError(err); +} + +const attributedRejections = new Set(); +process.on('unhandledRejection', (reason, promise) => { + if (attributeLeakedError(reason)) { + attributedRejections.add(promise); + } +}); + +process.on('uncaughtException', exception => { + if (attributeLeakedError(exception)) { + return; + } + + let serialized; + try { + serialized = serializeError(exception); + } catch (ignore) { // eslint-disable-line unicorn/catch-error-name + // Avoid using serializeError + const err = new Error('Failed to serialize uncaught exception'); + serialized = { + avaAssertionError: false, + name: err.name, + message: err.message, + stack: err.stack + }; + } + + // Ensure the IPC channel is refereced. The uncaught exception will kick off + // the teardown sequence, for which the messages must be received. + adapter.ipcChannel.ref(); + + adapter.send('uncaughtException', {exception: serialized}); +}); + +let tearingDown = false; +process.on('ava-teardown', () => { + // AVA-teardown can be sent more than once + if (tearingDown) { + return; + } + tearingDown = true; + + let rejections = currentlyUnhandled() + .filter(rejection => !attributedRejections.has(rejection.promise)); + + if (rejections.length > 0) { + rejections = rejections.map(rejection => { + let reason = rejection.reason; + if (!isObj(reason) || typeof reason.message !== 'string') { + reason = { + message: String(reason) + }; + } + return serializeError(reason); + }); + + adapter.send('unhandledRejections', {rejections}); + } + + // Include dependencies in the final teardown message. This ensures the full + // set of dependencies is included no matter how the process exits, unless + // it flat out crashes. + adapter.send('teardown', {dependencies}); +}); + +process.on('ava-exit', () => { + process.exit(0); // eslint-disable-line xo/no-process-exit +}); diff --git a/node_modules/ava/lib/test.js b/node_modules/ava/lib/test.js new file mode 100644 index 000000000..a9b0fb1d9 --- /dev/null +++ b/node_modules/ava/lib/test.js @@ -0,0 +1,416 @@ +'use strict'; +const isGeneratorFn = require('is-generator-fn'); +const co = require('co-with-promise'); +const observableToPromise = require('observable-to-promise'); +const isPromise = require('is-promise'); +const isObservable = require('is-observable'); +const plur = require('plur'); +const assert = require('./assert'); +const formatAssertError = require('./format-assert-error'); +const globals = require('./globals'); + +class SkipApi { + constructor(test) { + this._test = test; + } +} + +const captureStack = start => { + const limitBefore = Error.stackTraceLimit; + Error.stackTraceLimit = 1; + const obj = {}; + Error.captureStackTrace(obj, start); + Error.stackTraceLimit = limitBefore; + return obj.stack; +}; + +class ExecutionContext { + constructor(test) { + this._test = test; + this.skip = new SkipApi(test); + } + + plan(ct) { + this._test.plan(ct, captureStack(this.plan)); + } + + get end() { + const end = this._test.bindEndCallback(); + const endFn = err => end(err, captureStack(endFn)); + return endFn; + } + + get title() { + return this._test.title; + } + + get context() { + const contextRef = this._test.contextRef; + return contextRef && contextRef.context; + } + + set context(context) { + const contextRef = this._test.contextRef; + + if (!contextRef) { + this._test.saveFirstError(new Error(`\`t.context\` is not available in ${this._test.metadata.type} tests`)); + return; + } + + contextRef.context = context; + } + + _throwsArgStart(assertion, file, line) { + this._test.trackThrows({assertion, file, line}); + } + _throwsArgEnd() { + this._test.trackThrows(null); + } +} +Object.defineProperty(ExecutionContext.prototype, 'context', {enumerable: true}); + +{ + const assertions = assert.wrapAssertions({ + pass(executionContext) { + executionContext._test.countPassedAssertion(); + }, + + pending(executionContext, promise) { + executionContext._test.addPendingAssertion(promise); + }, + + fail(executionContext, error) { + executionContext._test.addFailedAssertion(error); + } + }); + Object.assign(ExecutionContext.prototype, assertions); + + function skipFn() { + this._test.countPassedAssertion(); + } + Object.keys(assertions).forEach(el => { + SkipApi.prototype[el] = skipFn; + }); +} + +class Test { + constructor(options) { + this.contextRef = options.contextRef; + this.failWithoutAssertions = options.failWithoutAssertions; + this.fn = isGeneratorFn(options.fn) ? co.wrap(options.fn) : options.fn; + this.getSnapshotState = options.getSnapshotState; + this.metadata = options.metadata; + this.onResult = options.onResult; + this.title = options.title; + + this.assertCount = 0; + this.assertError = undefined; + this.calledEnd = false; + this.duration = null; + this.endCallbackFinisher = null; + this.finishDueToAttributedError = null; + this.finishDueToInactivity = null; + this.finishing = false; + this.pendingAssertionCount = 0; + this.pendingThrowsAssertion = null; + this.planCount = null; + this.startedAt = 0; + } + + bindEndCallback() { + if (this.metadata.callback) { + return (err, stack) => { + this.endCallback(err, stack); + }; + } + + throw new Error('`t.end()`` is not supported in this context. To use `t.end()` as a callback, you must use "callback mode" via `test.cb(testName, fn)`'); + } + + endCallback(err, stack) { + if (this.calledEnd) { + this.saveFirstError(new Error('`t.end()` called more than once')); + return; + } + this.calledEnd = true; + + if (err) { + this.saveFirstError(new assert.AssertionError({ + actual: err, + message: 'Callback called with an error', + stack, + values: [formatAssertError.formatWithLabel('Error:', err)] + })); + } + + if (this.endCallbackFinisher) { + this.endCallbackFinisher(); + } + } + + createExecutionContext() { + return new ExecutionContext(this); + } + + countPassedAssertion() { + if (this.finishing) { + this.saveFirstError(new Error('Assertion passed, but test has already finished')); + } + + this.assertCount++; + } + + addPendingAssertion(promise) { + if (this.finishing) { + this.saveFirstError(new Error('Assertion passed, but test has already finished')); + } + + this.assertCount++; + this.pendingAssertionCount++; + promise + .catch(err => this.saveFirstError(err)) + .then(() => this.pendingAssertionCount--); + } + + addFailedAssertion(error) { + if (this.finishing) { + this.saveFirstError(new Error('Assertion failed, but test has already finished')); + } + + this.assertCount++; + this.saveFirstError(error); + } + + saveFirstError(err) { + if (!this.assertError) { + this.assertError = err; + } + } + + plan(count, planStack) { + if (typeof count !== 'number') { + throw new TypeError('Expected a number'); + } + + this.planCount = count; + + // In case the `planCount` doesn't match `assertCount, we need the stack of + // this function to throw with a useful stack. + this.planStack = planStack; + } + + verifyPlan() { + if (!this.assertError && this.planCount !== null && this.planCount !== this.assertCount) { + this.saveFirstError(new assert.AssertionError({ + assertion: 'plan', + message: `Planned for ${this.planCount} ${plur('assertion', this.planCount)}, but got ${this.assertCount}.`, + operator: '===', + stack: this.planStack + })); + } + } + + verifyAssertions() { + if (!this.assertError) { + if (this.failWithoutAssertions && !this.calledEnd && this.planCount === null && this.assertCount === 0) { + this.saveFirstError(new Error('Test finished without running any assertions')); + } else if (this.pendingAssertionCount > 0) { + this.saveFirstError(new Error('Test finished, but an assertion is still pending')); + } + } + } + + trackThrows(pending) { + this.pendingThrowsAssertion = pending; + } + + detectImproperThrows(err) { + if (!this.pendingThrowsAssertion) { + return false; + } + + const pending = this.pendingThrowsAssertion; + this.pendingThrowsAssertion = null; + + const values = []; + if (err) { + values.push(formatAssertError.formatWithLabel(`The following error was thrown, possibly before \`t.${pending.assertion}()\` could be called:`, err)); + } + + this.saveFirstError(new assert.AssertionError({ + assertion: pending.assertion, + fixedSource: {file: pending.file, line: pending.line}, + improperUsage: true, + message: `Improper usage of \`t.${pending.assertion}()\` detected`, + stack: err instanceof Error && err.stack, + values + })); + return true; + } + + waitForPendingThrowsAssertion() { + return new Promise(resolve => { + this.finishDueToAttributedError = () => { + resolve(this.finishPromised()); + }; + + this.finishDueToInactivity = () => { + this.detectImproperThrows(); + resolve(this.finishPromised()); + }; + + // Wait up to a second to see if an error can be attributed to the + // pending assertion. + globals.setTimeout(() => this.finishDueToInactivity(), 1000).unref(); + }); + } + + attributeLeakedError(err) { + if (!this.detectImproperThrows(err)) { + return false; + } + + this.finishDueToAttributedError(); + return true; + } + + callFn() { + try { + return { + ok: true, + retval: this.fn(this.createExecutionContext()) + }; + } catch (err) { + return { + ok: false, + error: err + }; + } + } + + run() { + this.startedAt = globals.now(); + + const result = this.callFn(); + if (!result.ok) { + if (!this.detectImproperThrows(result.error)) { + this.saveFirstError(new assert.AssertionError({ + message: 'Error thrown in test', + stack: result.error instanceof Error && result.error.stack, + values: [formatAssertError.formatWithLabel('Error:', result.error)] + })); + } + return this.finish(); + } + + const returnedObservable = isObservable(result.retval); + const returnedPromise = isPromise(result.retval); + + let promise; + if (returnedObservable) { + promise = observableToPromise(result.retval); + } else if (returnedPromise) { + // `retval` can be any thenable, so convert to a proper promise. + promise = Promise.resolve(result.retval); + } + + if (this.metadata.callback) { + 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(); + } + + if (this.calledEnd) { + return this.finish(); + } + + return new Promise(resolve => { + this.endCallbackFinisher = () => { + resolve(this.finishPromised()); + }; + + this.finishDueToAttributedError = () => { + resolve(this.finishPromised()); + }; + + this.finishDueToInactivity = () => { + this.saveFirstError(new Error('`t.end()` was never called')); + resolve(this.finishPromised()); + }; + }); + } + + if (promise) { + return new Promise(resolve => { + this.finishDueToAttributedError = () => { + resolve(this.finishPromised()); + }; + + this.finishDueToInactivity = () => { + const err = returnedObservable ? + new Error('Observable returned by test never completed') : + new Error('Promise returned by test never resolved'); + this.saveFirstError(err); + resolve(this.finishPromised()); + }; + + promise + .catch(err => { + if (!this.detectImproperThrows(err)) { + this.saveFirstError(new assert.AssertionError({ + message: 'Rejected promise returned by test', + stack: err instanceof Error && err.stack, + values: [formatAssertError.formatWithLabel('Rejection reason:', err)] + })); + } + }) + .then(() => resolve(this.finishPromised())); + }); + } + + return this.finish(); + } + + finish() { + this.finishing = true; + + if (!this.assertError && this.pendingThrowsAssertion) { + return this.waitForPendingThrowsAssertion(); + } + + this.verifyPlan(); + this.verifyAssertions(); + + this.duration = globals.now() - this.startedAt; + + let reason = this.assertError; + let passed = !reason; + + if (this.metadata.failing) { + passed = !passed; + + if (passed) { + reason = undefined; + } else { + reason = new Error('Test was expected to fail, but succeeded, you should stop marking the test as failing'); + } + } + + this.onResult({ + passed, + result: this, + reason + }); + + return passed; + } + + finishPromised() { + return new Promise(resolve => { + resolve(this.finish()); + }); + } +} + +module.exports = Test; diff --git a/node_modules/ava/lib/validate-test.js b/node_modules/ava/lib/validate-test.js new file mode 100644 index 000000000..8258a5990 --- /dev/null +++ b/node_modules/ava/lib/validate-test.js @@ -0,0 +1,48 @@ +'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/node_modules/ava/lib/watcher.js b/node_modules/ava/lib/watcher.js new file mode 100644 index 000000000..3d7094ffb --- /dev/null +++ b/node_modules/ava/lib/watcher.js @@ -0,0 +1,322 @@ +'use strict'; +const nodePath = require('path'); +const debug = require('debug')('ava:watcher'); +const diff = require('lodash.difference'); +const chokidar = require('chokidar'); +const flatten = require('arr-flatten'); +const union = require('array-union'); +const uniq = require('array-uniq'); +const AvaFiles = require('./ava-files'); + +function rethrowAsync(err) { + // Don't swallow exceptions. Note that any + // expected error should already have been logged + setImmediate(() => { + throw err; + }); +} + +class Debouncer { + constructor(watcher) { + this.watcher = watcher; + this.timer = null; + this.repeat = false; + } + debounce() { + if (this.timer) { + this.again = true; + return; + } + + const timer = setTimeout(() => { + this.watcher.busy.then(() => { + // Do nothing if debouncing was canceled while waiting for the busy + // promise to fulfil + if (this.timer !== timer) { + return; + } + + if (this.again) { + this.timer = null; + this.again = false; + this.debounce(); + } else { + this.watcher.runAfterChanges(); + this.timer = null; + this.again = false; + } + }); + }, 10); + + this.timer = timer; + } + cancel() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + this.again = false; + } + } +} + +class TestDependency { + constructor(file, sources) { + this.file = file; + this.sources = sources; + } + contains(source) { + return this.sources.indexOf(source) !== -1; + } +} + +class Watcher { + constructor(logger, api, files, sources) { + this.debouncer = new Debouncer(this); + this.avaFiles = new AvaFiles({ + files, + sources + }); + + this.clearLogOnNextRun = true; + this.runVector = 0; + this.run = specificFiles => { + if (this.runVector > 0) { + const cleared = this.clearLogOnNextRun && logger.clear(); + if (!cleared) { + logger.reset(); + logger.section(); + } + this.clearLogOnNextRun = true; + + logger.reset(); + logger.start(); + } + + this.runVector += 1; + + const currentVector = this.runVector; + + let runOnlyExclusive = false; + + if (specificFiles) { + const exclusiveFiles = specificFiles.filter(file => this.filesWithExclusiveTests.indexOf(file) !== -1); + + runOnlyExclusive = exclusiveFiles.length !== this.filesWithExclusiveTests.length; + + if (runOnlyExclusive) { + // The test files that previously contained exclusive tests are always + // run, together with the remaining specific files. + const remainingFiles = diff(specificFiles, exclusiveFiles); + specificFiles = this.filesWithExclusiveTests.concat(remainingFiles); + } + } + + this.busy = api.run(specificFiles || files, {runOnlyExclusive}) + .then(runStatus => { + runStatus.previousFailCount = this.sumPreviousFailures(currentVector); + logger.finish(runStatus); + + const badCounts = runStatus.failCount + runStatus.rejectionCount + runStatus.exceptionCount; + this.clearLogOnNextRun = this.clearLogOnNextRun && badCounts === 0; + }) + .catch(rethrowAsync); + }; + + this.testDependencies = []; + this.trackTestDependencies(api, sources); + + this.filesWithExclusiveTests = []; + this.trackExclusivity(api); + + this.filesWithFailures = []; + this.trackFailures(api); + + this.dirtyStates = {}; + this.watchFiles(); + this.rerunAll(); + } + watchFiles() { + const patterns = this.avaFiles.getChokidarPatterns(); + + chokidar.watch(patterns.paths, { + ignored: patterns.ignored, + ignoreInitial: true + }).on('all', (event, path) => { + if (event === 'add' || event === 'change' || event === 'unlink') { + debug('Detected %s of %s', event, path); + this.dirtyStates[path] = event; + this.debouncer.debounce(); + } + }); + } + trackTestDependencies(api) { + const relative = absPath => nodePath.relative(process.cwd(), absPath); + + api.on('test-run', runStatus => { + runStatus.on('dependencies', (file, dependencies) => { + const sourceDeps = dependencies.map(relative).filter(this.avaFiles.isSource); + this.updateTestDependencies(file, sourceDeps); + }); + }); + } + updateTestDependencies(file, sources) { + if (sources.length === 0) { + this.testDependencies = this.testDependencies.filter(dep => dep.file !== file); + return; + } + + const isUpdate = this.testDependencies.some(dep => { + if (dep.file !== file) { + return false; + } + + dep.sources = sources; + + return true; + }); + + if (!isUpdate) { + this.testDependencies.push(new TestDependency(file, sources)); + } + } + trackExclusivity(api) { + api.on('stats', stats => { + this.updateExclusivity(stats.file, stats.hasExclusive); + }); + } + updateExclusivity(file, hasExclusiveTests) { + const index = this.filesWithExclusiveTests.indexOf(file); + + if (hasExclusiveTests && index === -1) { + this.filesWithExclusiveTests.push(file); + } else if (!hasExclusiveTests && index !== -1) { + this.filesWithExclusiveTests.splice(index, 1); + } + } + trackFailures(api) { + api.on('test-run', (runStatus, files) => { + files.forEach(file => { + this.pruneFailures(nodePath.relative(process.cwd(), file)); + }); + + const currentVector = this.runVector; + runStatus.on('error', err => { + this.countFailure(err.file, currentVector); + }); + runStatus.on('test', result => { + if (result.error) { + this.countFailure(result.file, currentVector); + } + }); + }); + } + pruneFailures(file) { + this.filesWithFailures = this.filesWithFailures.filter(state => state.file !== file); + } + countFailure(file, vector) { + const isUpdate = this.filesWithFailures.some(state => { + if (state.file !== file) { + return false; + } + + state.count++; + return true; + }); + + if (!isUpdate) { + this.filesWithFailures.push({ + file, + vector, + count: 1 + }); + } + } + sumPreviousFailures(beforeVector) { + let total = 0; + + this.filesWithFailures.forEach(state => { + if (state.vector < beforeVector) { + total += state.count; + } + }); + + return total; + } + cleanUnlinkedTests(unlinkedTests) { + unlinkedTests.forEach(testFile => { + this.updateTestDependencies(testFile, []); + this.updateExclusivity(testFile, false); + this.pruneFailures(testFile); + }); + } + observeStdin(stdin) { + stdin.resume(); + stdin.setEncoding('utf8'); + + stdin.on('data', data => { + data = data.trim().toLowerCase(); + if (data !== 'r' && data !== 'rs') { + return; + } + + // Cancel the debouncer, it might rerun specific tests whereas *all* tests + // need to be rerun + this.debouncer.cancel(); + this.busy.then(() => { + // Cancel the debouncer again, it might have restarted while waiting for + // the busy promise to fulfil + this.debouncer.cancel(); + this.clearLogOnNextRun = false; + this.rerunAll(); + }); + }); + } + rerunAll() { + this.dirtyStates = {}; + this.run(); + } + runAfterChanges() { + const dirtyStates = this.dirtyStates; + this.dirtyStates = {}; + + const dirtyPaths = Object.keys(dirtyStates); + const dirtyTests = dirtyPaths.filter(this.avaFiles.isTest); + const dirtySources = diff(dirtyPaths, dirtyTests); + const addedOrChangedTests = dirtyTests.filter(path => dirtyStates[path] !== 'unlink'); + const unlinkedTests = diff(dirtyTests, addedOrChangedTests); + + this.cleanUnlinkedTests(unlinkedTests); + + // No need to rerun tests if the only change is that tests were deleted + if (unlinkedTests.length === dirtyPaths.length) { + return; + } + + if (dirtySources.length === 0) { + // Run any new or changed tests + this.run(addedOrChangedTests); + return; + } + + // Try to find tests that depend on the changed source files + const testsBySource = dirtySources.map(path => { + return this.testDependencies.filter(dep => dep.contains(path)).map(dep => { + debug('%s is a dependency of %s', path, dep.file); + return dep.file; + }); + }, this).filter(tests => tests.length > 0); + + // Rerun all tests if source files were changed that could not be traced to + // specific tests + if (testsBySource.length !== dirtySources.length) { + debug('Sources remain that cannot be traced to specific tests. Rerunning all tests'); + this.run(); + return; + } + + // Run all affected tests + this.run(union(addedOrChangedTests, uniq(flatten(testsBySource)))); + } +} + +module.exports = Watcher; |