aboutsummaryrefslogtreecommitdiff
path: root/node_modules/ava/lib
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2017-08-14 05:01:11 +0200
committerFlorian Dold <florian.dold@gmail.com>2017-08-14 05:02:09 +0200
commit363723fc84f7b8477592e0105aeb331ec9a017af (patch)
tree29f92724f34131bac64d6a318dd7e30612e631c7 /node_modules/ava/lib
parent5634e77ad96bfe1818f6b6ee70b7379652e5487f (diff)
node_modules
Diffstat (limited to 'node_modules/ava/lib')
-rw-r--r--node_modules/ava/lib/assert.js171
-rw-r--r--node_modules/ava/lib/ava-files.js2
-rw-r--r--node_modules/ava/lib/babel-config.js8
-rw-r--r--node_modules/ava/lib/cli.js4
-rw-r--r--node_modules/ava/lib/concordance-options.js130
-rw-r--r--node_modules/ava/lib/enhance-assert.js8
-rw-r--r--node_modules/ava/lib/format-assert-error.js121
-rw-r--r--node_modules/ava/lib/main.js1
-rw-r--r--node_modules/ava/lib/process-adapter.js2
-rw-r--r--node_modules/ava/lib/reporters/format-serialized-error.js26
-rw-r--r--node_modules/ava/lib/reporters/improper-usage-messages.js43
-rw-r--r--node_modules/ava/lib/reporters/mini.js76
-rw-r--r--node_modules/ava/lib/reporters/verbose.js41
-rw-r--r--node_modules/ava/lib/run-status.js2
-rw-r--r--node_modules/ava/lib/runner.js42
-rw-r--r--node_modules/ava/lib/snapshot-manager.js396
-rw-r--r--node_modules/ava/lib/test-collection.js6
-rw-r--r--node_modules/ava/lib/test-worker.js36
-rw-r--r--node_modules/ava/lib/test.js33
-rw-r--r--node_modules/ava/lib/watcher.js54
20 files changed, 897 insertions, 305 deletions
diff --git a/node_modules/ava/lib/assert.js b/node_modules/ava/lib/assert.js
index c16e11a1a..a0e9fe82c 100644
--- a/node_modules/ava/lib/assert.js
+++ b/node_modules/ava/lib/assert.js
@@ -1,12 +1,32 @@
'use strict';
+const concordance = require('concordance');
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 concordanceOptions = require('./concordance-options').default;
+const concordanceDiffOptions = require('./concordance-options').diff;
const enhanceAssert = require('./enhance-assert');
-const formatAssertError = require('./format-assert-error');
+const snapshotManager = require('./snapshot-manager');
+
+function formatDescriptorDiff(actualDescriptor, expectedDescriptor, options) {
+ options = Object.assign({}, options, concordanceDiffOptions);
+ return {
+ label: 'Difference:',
+ formatted: concordance.diffDescriptors(actualDescriptor, expectedDescriptor, options)
+ };
+}
+
+function formatDescriptorWithLabel(label, descriptor) {
+ return {
+ label,
+ formatted: concordance.formatDescriptor(descriptor, concordanceOptions)
+ };
+}
+
+function formatWithLabel(label, value) {
+ return formatDescriptorWithLabel(label, concordance.describe(value, concordanceOptions));
+}
class AssertionError extends Error {
constructor(opts) {
@@ -19,6 +39,11 @@ class AssertionError extends Error {
this.operator = opts.operator;
this.values = opts.values || [];
+ // Raw expected and actual objects are stored for custom reporters
+ // (such as wallaby.js), that manage worker processes directly and
+ // use the values for custom diff views
+ this.raw = opts.raw;
+
// Reserved for power-assert statements
this.statements = [];
@@ -41,7 +66,6 @@ function wrapAssertions(callbacks) {
const fail = callbacks.fail;
const noop = () => {};
- const makeNoop = () => noop;
const makeRethrow = reason => () => {
throw reason;
};
@@ -59,31 +83,27 @@ function wrapAssertions(callbacks) {
},
is(actual, expected, message) {
- if (actual === expected) {
+ if (Object.is(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)
- ];
-
+ const actualDescriptor = concordance.describe(actual, concordanceOptions);
+ const expectedDescriptor = concordance.describe(expected, concordanceOptions);
fail(this, new AssertionError({
assertion: 'is',
message,
- operator: '===',
- values
+ raw: {actual, expected},
+ values: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)]
}));
}
},
not(actual, expected, message) {
- if (actual === expected) {
+ if (Object.is(actual, expected)) {
fail(this, new AssertionError({
assertion: 'not',
message,
- operator: '!==',
- values: [formatAssertError.formatWithLabel('Value is strictly equal:', actual)]
+ raw: {actual, expected},
+ values: [formatWithLabel('Value is the same as:', actual)]
}));
} else {
pass(this);
@@ -91,29 +111,30 @@ function wrapAssertions(callbacks) {
},
deepEqual(actual, expected, message) {
- if (deepEqual(actual, expected)) {
+ const result = concordance.compare(actual, expected, concordanceOptions);
+ if (result.pass) {
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)
- ];
-
+ const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions);
+ const expectedDescriptor = result.expected || concordance.describe(expected, concordanceOptions);
fail(this, new AssertionError({
assertion: 'deepEqual',
message,
- values
+ raw: {actual, expected},
+ values: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)]
}));
}
},
notDeepEqual(actual, expected, message) {
- if (deepEqual(actual, expected)) {
+ const result = concordance.compare(actual, expected, concordanceOptions);
+ if (result.pass) {
+ const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions);
fail(this, new AssertionError({
assertion: 'notDeepEqual',
message,
- values: [formatAssertError.formatWithLabel('Value is deeply equal:', actual)]
+ raw: {actual, expected},
+ values: [formatDescriptorWithLabel('Value is deeply equal:', actualDescriptor)]
}));
} else {
pass(this);
@@ -131,7 +152,7 @@ function wrapAssertions(callbacks) {
assertion: 'throws',
improperUsage: true,
message: '`t.throws()` must be called with a function, Promise, or Observable',
- values: [formatAssertError.formatWithLabel('Called with:', fn)]
+ values: [formatWithLabel('Called with:', fn)]
}));
return;
}
@@ -160,15 +181,13 @@ function wrapAssertions(callbacks) {
}, coreAssertThrowsErrorArg);
return actual;
} catch (err) {
- const values = threw ?
- [formatAssertError.formatWithLabel('Threw unexpected exception:', actual)] :
- null;
-
throw new AssertionError({
assertion: 'throws',
message,
stack,
- values
+ values: threw ?
+ [formatWithLabel('Threw unexpected exception:', actual)] :
+ null
});
}
};
@@ -176,7 +195,14 @@ function wrapAssertions(callbacks) {
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));
+ const intermediate = promise.then(value => {
+ throw new AssertionError({
+ assertion: 'throws',
+ message: 'Expected promise to be rejected, but it was resolved instead',
+ values: [formatWithLabel('Resolved with:', value)]
+ });
+ }, reason => test(makeRethrow(reason), stack));
+
pending(this, intermediate);
// Don't reject the returned promise, even if the assertion fails.
return intermediate.catch(noop);
@@ -202,7 +228,7 @@ function wrapAssertions(callbacks) {
assertion: 'notThrows',
improperUsage: true,
message: '`t.notThrows()` must be called with a function, Promise, or Observable',
- values: [formatAssertError.formatWithLabel('Called with:', fn)]
+ values: [formatWithLabel('Called with:', fn)]
}));
return;
}
@@ -215,7 +241,7 @@ function wrapAssertions(callbacks) {
assertion: 'notThrows',
message,
stack,
- values: [formatAssertError.formatWithLabel('Threw:', err.actual)]
+ values: [formatWithLabel('Threw:', err.actual)]
});
}
};
@@ -242,28 +268,57 @@ function wrapAssertions(callbacks) {
fail(this, new AssertionError({
assertion: 'ifError',
message,
- values: [formatAssertError.formatWithLabel('Error:', actual)]
+ values: [formatWithLabel('Error:', actual)]
}));
} else {
pass(this);
}
},
- snapshot(actual, message) {
- const state = this._test.getSnapshotState();
- const result = state.match(this.title, actual);
+ snapshot(expected, optionsOrMessage, message) {
+ const options = {};
+ if (typeof optionsOrMessage === 'string') {
+ message = optionsOrMessage;
+ } else if (optionsOrMessage) {
+ options.id = optionsOrMessage.id;
+ }
+ options.expected = expected;
+ options.message = message;
+
+ let result;
+ try {
+ result = this._test.compareWithSnapshot(options);
+ } catch (err) {
+ if (!(err instanceof snapshotManager.SnapshotError)) {
+ throw err;
+ }
+
+ const improperUsage = {name: err.name, snapPath: err.snapPath};
+ if (err instanceof snapshotManager.VersionMismatchError) {
+ improperUsage.snapVersion = err.snapVersion;
+ improperUsage.expectedVersion = err.expectedVersion;
+ }
+
+ fail(this, new AssertionError({
+ assertion: 'snapshot',
+ message: message || 'Could not compare snapshot',
+ improperUsage
+ }));
+ return;
+ }
+
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');
+ } else if (result.actual) {
fail(this, new AssertionError({
assertion: 'snapshot',
message: message || 'Did not match snapshot',
- values: [{label: 'Difference:', formatted: diff}]
+ values: [formatDescriptorDiff(result.actual, result.expected, {invert: true})]
+ }));
+ } else {
+ fail(this, new AssertionError({
+ assertion: 'snapshot',
+ message: message || 'No snapshot available, run with --update-snapshots'
}));
}
}
@@ -276,7 +331,7 @@ function wrapAssertions(callbacks) {
assertion: 'truthy',
message,
operator: '!!',
- values: [formatAssertError.formatWithLabel('Value is not truthy:', actual)]
+ values: [formatWithLabel('Value is not truthy:', actual)]
});
}
},
@@ -287,7 +342,7 @@ function wrapAssertions(callbacks) {
assertion: 'falsy',
message,
operator: '!',
- values: [formatAssertError.formatWithLabel('Value is not falsy:', actual)]
+ values: [formatWithLabel('Value is not falsy:', actual)]
});
}
},
@@ -297,7 +352,7 @@ function wrapAssertions(callbacks) {
throw new AssertionError({
assertion: 'true',
message,
- values: [formatAssertError.formatWithLabel('Value is not `true`:', actual)]
+ values: [formatWithLabel('Value is not `true`:', actual)]
});
}
},
@@ -307,7 +362,7 @@ function wrapAssertions(callbacks) {
throw new AssertionError({
assertion: 'false',
message,
- values: [formatAssertError.formatWithLabel('Value is not `false`:', actual)]
+ values: [formatWithLabel('Value is not `false`:', actual)]
});
}
},
@@ -318,7 +373,7 @@ function wrapAssertions(callbacks) {
assertion: 'regex',
improperUsage: true,
message: '`t.regex()` must be called with a string',
- values: [formatAssertError.formatWithLabel('Called with:', string)]
+ values: [formatWithLabel('Called with:', string)]
});
}
if (!(regex instanceof RegExp)) {
@@ -326,7 +381,7 @@ function wrapAssertions(callbacks) {
assertion: 'regex',
improperUsage: true,
message: '`t.regex()` must be called with a regular expression',
- values: [formatAssertError.formatWithLabel('Called with:', regex)]
+ values: [formatWithLabel('Called with:', regex)]
});
}
@@ -335,8 +390,8 @@ function wrapAssertions(callbacks) {
assertion: 'regex',
message,
values: [
- formatAssertError.formatWithLabel('Value must match expression:', string),
- formatAssertError.formatWithLabel('Regular expression:', regex)
+ formatWithLabel('Value must match expression:', string),
+ formatWithLabel('Regular expression:', regex)
]
});
}
@@ -348,7 +403,7 @@ function wrapAssertions(callbacks) {
assertion: 'notRegex',
improperUsage: true,
message: '`t.notRegex()` must be called with a string',
- values: [formatAssertError.formatWithLabel('Called with:', string)]
+ values: [formatWithLabel('Called with:', string)]
});
}
if (!(regex instanceof RegExp)) {
@@ -356,7 +411,7 @@ function wrapAssertions(callbacks) {
assertion: 'notRegex',
improperUsage: true,
message: '`t.notRegex()` must be called with a regular expression',
- values: [formatAssertError.formatWithLabel('Called with:', regex)]
+ values: [formatWithLabel('Called with:', regex)]
});
}
@@ -365,8 +420,8 @@ function wrapAssertions(callbacks) {
assertion: 'notRegex',
message,
values: [
- formatAssertError.formatWithLabel('Value must not match expression:', string),
- formatAssertError.formatWithLabel('Regular expression:', regex)
+ formatWithLabel('Value must not match expression:', string),
+ formatWithLabel('Regular expression:', regex)
]
});
}
diff --git a/node_modules/ava/lib/ava-files.js b/node_modules/ava/lib/ava-files.js
index dd9a2ee6d..cfdc9f202 100644
--- a/node_modules/ava/lib/ava-files.js
+++ b/node_modules/ava/lib/ava-files.js
@@ -265,7 +265,7 @@ class AvaFiles {
ignored = getDefaultIgnorePatterns().concat(ignored, overrideDefaultIgnorePatterns);
if (paths.length === 0) {
- paths = ['package.json', '**/*.js'];
+ paths = ['package.json', '**/*.js', '**/*.snap'];
}
paths = paths.concat(this.files);
diff --git a/node_modules/ava/lib/babel-config.js b/node_modules/ava/lib/babel-config.js
index c3be0dcfb..62e841f05 100644
--- a/node_modules/ava/lib/babel-config.js
+++ b/node_modules/ava/lib/babel-config.js
@@ -5,7 +5,7 @@ const chalk = require('chalk');
const figures = require('figures');
const configManager = require('hullabaloo-config-manager');
const md5Hex = require('md5-hex');
-const mkdirp = require('mkdirp');
+const makeDir = require('make-dir');
const colors = require('./colors');
function validate(conf) {
@@ -19,7 +19,7 @@ function validate(conf) {
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.';
+ message += 'See ' + chalk.underline('https://github.com/avajs/ava#es2017-support') + ' for allowed values.';
throw new Error(message);
}
@@ -90,7 +90,7 @@ function build(projectDir, cacheDir, userOptions, powerAssert) {
const seed = md5Hex([process.versions.node, projectDir]);
// Ensure cacheDir exists
- mkdirp.sync(cacheDir);
+ makeDir.sync(cacheDir);
// The file names predict where valid options may be cached, and thus should
// include the seed.
@@ -136,7 +136,7 @@ function build(projectDir, cacheDir, userOptions, powerAssert) {
return resolveOptions(baseConfig, cache, optionsFile, verifierFile);
})
.then(cacheKeys => ({
- getOptions: require(optionsFile).getOptions, // eslint-disable-line import/no-dynamic-require
+ getOptions: require(optionsFile).getOptions,
// Include the seed in the cache keys used to store compilation results.
cacheKeys: Object.assign({seed}, cacheKeys)
}));
diff --git a/node_modules/ava/lib/cli.js b/node_modules/ava/lib/cli.js
index f6213f107..5649a8190 100644
--- a/node_modules/ava/lib/cli.js
+++ b/node_modules/ava/lib/cli.js
@@ -118,6 +118,10 @@ exports.run = () => {
throw new Error(colors.error(figures.cross) + ' Watch mode is not available in CI, as it prevents AVA from terminating.');
}
+ if (cli.flags.concurrency === '') {
+ throw new Error(colors.error(figures.cross) + ' The --concurrency and -c flags must be provided the maximum number of test files to run at once.');
+ }
+
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.');
}
diff --git a/node_modules/ava/lib/concordance-options.js b/node_modules/ava/lib/concordance-options.js
new file mode 100644
index 000000000..18b4b0c77
--- /dev/null
+++ b/node_modules/ava/lib/concordance-options.js
@@ -0,0 +1,130 @@
+'use strict';
+const ansiStyles = require('ansi-styles');
+const chalk = require('chalk');
+const stripAnsi = require('strip-ansi');
+const cloneDeepWith = require('lodash.clonedeepwith');
+const reactPlugin = require('@concordance/react');
+const options = require('./globals').options;
+
+// Wrap Concordance's React plugin. Change the name to avoid collisions if in
+// the future users can register plugins themselves.
+const avaReactPlugin = Object.assign({}, reactPlugin, {name: 'ava-plugin-react'});
+const plugins = [avaReactPlugin];
+
+const forceColor = new chalk.constructor({enabled: true});
+
+const colorTheme = {
+ boolean: ansiStyles.yellow,
+ circular: forceColor.grey('[Circular]'),
+ date: {
+ invalid: forceColor.red('invalid'),
+ value: ansiStyles.blue
+ },
+ diffGutters: {
+ actual: forceColor.red('-') + ' ',
+ expected: forceColor.green('+') + ' ',
+ padding: ' '
+ },
+ error: {
+ ctor: {open: ansiStyles.grey.open + '(', close: ')' + ansiStyles.grey.close},
+ name: ansiStyles.magenta
+ },
+ function: {
+ name: ansiStyles.blue,
+ stringTag: ansiStyles.magenta
+ },
+ global: ansiStyles.magenta,
+ item: {after: forceColor.grey(',')},
+ list: {openBracket: forceColor.grey('['), closeBracket: forceColor.grey(']')},
+ mapEntry: {after: forceColor.grey(',')},
+ maxDepth: forceColor.grey('…'),
+ null: ansiStyles.yellow,
+ number: ansiStyles.yellow,
+ object: {
+ openBracket: forceColor.grey('{'),
+ closeBracket: forceColor.grey('}'),
+ ctor: ansiStyles.magenta,
+ stringTag: {open: ansiStyles.magenta.open + '@', close: ansiStyles.magenta.close},
+ secondaryStringTag: {open: ansiStyles.grey.open + '@', close: ansiStyles.grey.close}
+ },
+ property: {
+ after: forceColor.grey(','),
+ keyBracket: {open: forceColor.grey('['), close: forceColor.grey(']')},
+ valueFallback: forceColor.grey('…')
+ },
+ react: {
+ functionType: forceColor.grey('\u235F'),
+ openTag: {
+ start: forceColor.grey('<'),
+ end: forceColor.grey('>'),
+ selfClose: forceColor.grey('/'),
+ selfCloseVoid: ' ' + forceColor.grey('/')
+ },
+ closeTag: {
+ open: forceColor.grey('</'),
+ close: forceColor.grey('>')
+ },
+ tagName: ansiStyles.magenta,
+ attribute: {
+ separator: '=',
+ value: {
+ openBracket: forceColor.grey('{'),
+ closeBracket: forceColor.grey('}'),
+ string: {
+ line: {open: forceColor.blue('"'), close: forceColor.blue('"'), escapeQuote: '"'}
+ }
+ }
+ },
+ child: {
+ openBracket: forceColor.grey('{'),
+ closeBracket: forceColor.grey('}')
+ }
+ },
+ regexp: {
+ source: {open: ansiStyles.blue.open + '/', close: '/' + ansiStyles.blue.close},
+ flags: ansiStyles.yellow
+ },
+ stats: {separator: forceColor.grey('---')},
+ string: {
+ open: ansiStyles.blue.open,
+ close: ansiStyles.blue.close,
+ line: {open: forceColor.blue('\''), close: forceColor.blue('\'')},
+ multiline: {start: forceColor.blue('`'), end: forceColor.blue('`')},
+ controlPicture: ansiStyles.grey,
+ diff: {
+ insert: {
+ open: ansiStyles.bgGreen.open + ansiStyles.black.open,
+ close: ansiStyles.black.close + ansiStyles.bgGreen.close
+ },
+ delete: {
+ open: ansiStyles.bgRed.open + ansiStyles.black.open,
+ close: ansiStyles.black.close + ansiStyles.bgRed.close
+ },
+ equal: ansiStyles.blue,
+ insertLine: {
+ open: ansiStyles.green.open,
+ close: ansiStyles.green.close
+ },
+ deleteLine: {
+ open: ansiStyles.red.open,
+ close: ansiStyles.red.close
+ }
+ }
+ },
+ symbol: ansiStyles.yellow,
+ typedArray: {
+ bytes: ansiStyles.yellow
+ },
+ undefined: ansiStyles.yellow
+};
+
+const plainTheme = cloneDeepWith(colorTheme, value => {
+ if (typeof value === 'string') {
+ return stripAnsi(value);
+ }
+});
+
+const theme = options.color === false ? plainTheme : colorTheme;
+exports.default = {maxDepth: 3, plugins, theme};
+exports.diff = {maxDepth: 1, plugins, theme};
+exports.snapshotManager = {plugins, theme: plainTheme};
diff --git a/node_modules/ava/lib/enhance-assert.js b/node_modules/ava/lib/enhance-assert.js
index 7808765b7..6e127b3d6 100644
--- a/node_modules/ava/lib/enhance-assert.js
+++ b/node_modules/ava/lib/enhance-assert.js
@@ -1,6 +1,7 @@
'use strict';
+const concordance = require('concordance');
const dotProp = require('dot-prop');
-const formatValue = require('./format-assert-error').formatValue;
+const concordanceOptions = require('./concordance-options').default;
// When adding patterns, don't forget to add to
// https://github.com/avajs/babel-preset-transform-test-files/blob/master/espower-patterns.json
@@ -37,7 +38,10 @@ const formatter = context => {
return args
.map(arg => {
const range = getNode(ast, arg.espath).range;
- return [computeStatement(tokens, range), formatValue(arg.value, {maxDepth: 1})];
+ const statement = computeStatement(tokens, range);
+
+ const formatted = concordance.format(arg.value, concordanceOptions);
+ return [statement, formatted];
})
.reverse();
};
diff --git a/node_modules/ava/lib/format-assert-error.js b/node_modules/ava/lib/format-assert-error.js
deleted file mode 100644
index a899af463..000000000
--- a/node_modules/ava/lib/format-assert-error.js
+++ /dev/null
@@ -1,121 +0,0 @@
-'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/main.js b/node_modules/ava/lib/main.js
index 52618e8b7..1b03cc854 100644
--- a/node_modules/ava/lib/main.js
+++ b/node_modules/ava/lib/main.js
@@ -11,6 +11,7 @@ const runner = new Runner({
failWithoutAssertions: opts.failWithoutAssertions,
file: opts.file,
match: opts.match,
+ projectDir: opts.projectDir,
serial: opts.serial,
updateSnapshots: opts.updateSnapshots
});
diff --git a/node_modules/ava/lib/process-adapter.js b/node_modules/ava/lib/process-adapter.js
index b50f37398..5f9c0d79d 100644
--- a/node_modules/ava/lib/process-adapter.js
+++ b/node_modules/ava/lib/process-adapter.js
@@ -94,7 +94,7 @@ exports.installDependencyTracking = (dependencies, testPath) => {
require.extensions[ext] = (module, filename) => {
if (filename !== testPath) {
- dependencies.push(filename);
+ dependencies.add(filename);
}
wrappedHandler(module, filename);
diff --git a/node_modules/ava/lib/reporters/format-serialized-error.js b/node_modules/ava/lib/reporters/format-serialized-error.js
new file mode 100644
index 000000000..6ab59e47c
--- /dev/null
+++ b/node_modules/ava/lib/reporters/format-serialized-error.js
@@ -0,0 +1,26 @@
+'use strict';
+const chalk = require('chalk');
+const trimOffNewlines = require('trim-off-newlines');
+
+function formatSerializedError(error) {
+ const printMessage = error.values.length === 0 ?
+ Boolean(error.message) :
+ !error.values[0].label.startsWith(error.message);
+
+ if (error.statements.length === 0 && error.values.length === 0) {
+ return {formatted: null, printMessage};
+ }
+
+ let formatted = '';
+ for (const value of error.values) {
+ formatted += `${value.label}\n\n${trimOffNewlines(value.formatted)}\n\n`;
+ }
+
+ for (const statement of error.statements) {
+ formatted += `${statement[0]}\n${chalk.grey('=>')} ${trimOffNewlines(statement[1])}\n\n`;
+ }
+
+ formatted = trimOffNewlines(formatted);
+ return {formatted, printMessage};
+}
+module.exports = formatSerializedError;
diff --git a/node_modules/ava/lib/reporters/improper-usage-messages.js b/node_modules/ava/lib/reporters/improper-usage-messages.js
index 0a2626638..298ef79a5 100644
--- a/node_modules/ava/lib/reporters/improper-usage-messages.js
+++ b/node_modules/ava/lib/reporters/improper-usage-messages.js
@@ -7,15 +7,48 @@ exports.forError = error => {
}
const assertion = error.assertion;
- if (assertion !== 'throws' || !assertion === 'notThrows') {
- return null;
- }
-
- return `Try wrapping the first argument to \`t.${assertion}()\` in a function:
+ if (assertion === 'throws' || assertion === 'notThrows') {
+ 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')}`;
+ } else if (assertion === 'snapshot') {
+ const name = error.improperUsage.name;
+ const snapPath = error.improperUsage.snapPath;
+
+ if (name === 'ChecksumError') {
+ return `The snapshot file is corrupted.
+
+File path: ${chalk.yellow(snapPath)}
+
+Please run AVA again with the ${chalk.cyan('--update-snapshots')} flag to recreate it.`;
+ }
+
+ if (name === 'LegacyError') {
+ return `The snapshot file was created with AVA 0.19. It's not supported by this AVA version.
+
+File path: ${chalk.yellow(snapPath)}
+
+Please run AVA again with the ${chalk.cyan('--update-snapshots')} flag to upgrade.`;
+ }
+
+ if (name === 'VersionMismatchError') {
+ const snapVersion = error.improperUsage.snapVersion;
+ const expectedVersion = error.improperUsage.expectedVersion;
+ const upgradeMessage = snapVersion < expectedVersion ?
+ `Please run AVA again with the ${chalk.cyan('--update-snapshots')} flag to upgrade.` :
+ 'You should upgrade AVA.';
+
+ return `The snapshot file is v${snapVersion}, but only v${expectedVersion} is supported.
+
+File path: ${chalk.yellow(snapPath)}
+
+${upgradeMessage}`;
+ }
+ }
+
+ return null;
};
diff --git a/node_modules/ava/lib/reporters/mini.js b/node_modules/ava/lib/reporters/mini.js
index df481a76a..8acfab8e7 100644
--- a/node_modules/ava/lib/reporters/mini.js
+++ b/node_modules/ava/lib/reporters/mini.js
@@ -8,33 +8,14 @@ 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 ansiEscapes = require('ansi-escapes');
+const trimOffNewlines = require('trim-off-newlines');
const extractStack = require('../extract-stack');
const codeExcerpt = require('../code-excerpt');
const colors = require('../colors');
+const formatSerializedError = require('./format-serialized-error');
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);
@@ -151,38 +132,37 @@ class MiniReporter {
time = chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']');
}
- let status = this.reportCounts(time);
+ let status = this.reportCounts(time) + '\n';
if (this.rejectionCount > 0) {
- status += '\n ' + colors.error(this.rejectionCount, plur('rejection', this.rejectionCount));
+ status += ' ' + colors.error(this.rejectionCount, plur('rejection', this.rejectionCount)) + '\n';
}
if (this.exceptionCount > 0) {
- status += '\n ' + colors.error(this.exceptionCount, plur('exception', this.exceptionCount));
+ status += ' ' + colors.error(this.exceptionCount, plur('exception', this.exceptionCount)) + '\n';
}
if (runStatus.previousFailCount > 0) {
- status += '\n ' + colors.error(runStatus.previousFailCount, 'previous', plur('failure', runStatus.previousFailCount), 'in test files that were not rerun');
+ status += ' ' + colors.error(runStatus.previousFailCount, 'previous', plur('failure', runStatus.previousFailCount), 'in test files that were not rerun') + '\n';
}
if (this.knownFailureCount > 0) {
for (const test of runStatus.knownFailures) {
const title = test.title;
- status += '\n\n ' + colors.title(title);
+ status += '\n ' + colors.title(title) + '\n';
// TODO: Output description with link
// status += colors.stack(description);
}
}
+ status += '\n';
if (this.failCount > 0) {
- runStatus.errors.forEach((test, index) => {
+ runStatus.errors.forEach(test => {
if (!test.error) {
return;
}
- const beforeSpacing = index === 0 ? '\n\n' : '\n\n\n\n';
-
- status += beforeSpacing + ' ' + colors.title(test.title) + '\n';
+ status += ' ' + colors.title(test.title) + '\n';
if (test.error.source) {
status += ' ' + colors.errorSource(test.error.source.file + ':' + test.error.source.line) + '\n';
@@ -192,28 +172,32 @@ class MiniReporter {
}
}
- 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 result = formatSerializedError(test.error);
+ if (result.printMessage) {
+ status += '\n' + indentString(test.error.message, 2) + '\n';
+ }
+
+ if (result.formatted) {
+ status += '\n' + indentString(result.formatted, 2) + '\n';
}
const message = improperUsageMessages.forError(test.error);
if (message) {
status += '\n' + indentString(message, 2) + '\n';
}
+ } else if (test.error.message) {
+ status += '\n' + indentString(test.error.message, 2) + '\n';
}
if (test.error.stack) {
const extracted = extractStack(test.error.stack);
if (extracted.includes('\n')) {
- status += '\n' + indentString(colors.errorStack(extracted), 2);
+ status += '\n' + indentString(colors.errorStack(extracted), 2) + '\n';
}
}
+
+ status += '\n\n\n';
});
}
@@ -225,7 +209,7 @@ class MiniReporter {
}
if (err.type === 'exception' && err.name === 'AvaError') {
- status += '\n\n ' + colors.error(cross + ' ' + err.message);
+ status += ' ' + colors.error(cross + ' ' + err.message) + '\n\n';
} else {
const title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception';
let description = err.stack ? err.stack.trimRight() : JSON.stringify(err);
@@ -233,23 +217,23 @@ class MiniReporter {
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.title(title) + '\n';
status += ' ' + colors.stack(errorTitle) + '\n';
- status += colors.errorStack(errorStack);
+ status += colors.errorStack(errorStack) + '\n\n';
}
});
}
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);
+ status += ' ' + colors.information('`--fail-fast` is on. ' + remaining) + '\n\n';
}
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');
+ status += ' ' + 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';
+ return '\n' + trimOffNewlines(status) + '\n';
}
section() {
return '\n' + chalk.gray.dim('\u2500'.repeat(process.stdout.columns || 80));
@@ -284,7 +268,7 @@ class MiniReporter {
}
// Erase the existing status message, plus the last log line.
- str += eraseLines(ct);
+ str += ansiEscapes.eraseLines(ct);
// Rewrite the last log line.
str += lastLine;
diff --git a/node_modules/ava/lib/reporters/verbose.js b/node_modules/ava/lib/reporters/verbose.js
index 1be43ce5e..cd47683e8 100644
--- a/node_modules/ava/lib/reporters/verbose.js
+++ b/node_modules/ava/lib/reporters/verbose.js
@@ -4,10 +4,11 @@ const prettyMs = require('pretty-ms');
const figures = require('figures');
const chalk = require('chalk');
const plur = require('plur');
-const formatAssertError = require('../format-assert-error');
+const trimOffNewlines = require('trim-off-newlines');
const extractStack = require('../extract-stack');
const codeExcerpt = require('../code-excerpt');
const colors = require('../colors');
+const formatSerializedError = require('./format-serialized-error');
const improperUsageMessages = require('./improper-usage-messages');
class VerboseReporter {
@@ -70,7 +71,7 @@ class VerboseReporter {
return output;
}
finish(runStatus) {
- let output = '\n';
+ let output = '';
const lines = [
runStatus.failCount > 0 ?
@@ -86,23 +87,23 @@ class VerboseReporter {
if (lines.length > 0) {
lines[0] += ' ' + chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']');
- output += lines.join('\n');
+ output += lines.join('\n') + '\n';
}
if (runStatus.knownFailureCount > 0) {
runStatus.knownFailures.forEach(test => {
- output += '\n\n\n ' + colors.error(test.title);
+ output += '\n\n ' + colors.error(test.title) + '\n';
});
}
+ output += '\n';
if (runStatus.failCount > 0) {
- runStatus.tests.forEach((test, index) => {
+ runStatus.tests.forEach(test => {
if (!test.error) {
return;
}
- const beforeSpacing = index === 0 ? '\n\n' : '\n\n\n\n';
- output += beforeSpacing + ' ' + colors.title(test.title) + '\n';
+ output += ' ' + colors.title(test.title) + '\n';
if (test.error.source) {
output += ' ' + colors.errorSource(test.error.source.file + ':' + test.error.source.line) + '\n';
@@ -112,41 +113,45 @@ class VerboseReporter {
}
}
- 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 result = formatSerializedError(test.error);
+ if (result.printMessage) {
+ output += '\n' + indentString(test.error.message, 2) + '\n';
+ }
+
+ if (result.formatted) {
+ output += '\n' + indentString(result.formatted, 2) + '\n';
}
const message = improperUsageMessages.forError(test.error);
if (message) {
output += '\n' + indentString(message, 2) + '\n';
}
+ } else if (test.error.message) {
+ output += '\n' + indentString(test.error.message, 2) + '\n';
}
if (test.error.stack) {
const extracted = extractStack(test.error.stack);
if (extracted.includes('\n')) {
- output += '\n' + indentString(colors.errorStack(extracted), 2);
+ output += '\n' + indentString(colors.errorStack(extracted), 2) + '\n';
}
}
+
+ output += '\n\n\n';
});
}
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);
+ output += ' ' + colors.information('`--fail-fast` is on. ' + remaining) + '\n\n';
}
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');
+ output += ' ' + 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';
+ return '\n' + trimOffNewlines(output) + '\n';
}
section() {
return chalk.gray.dim('\u2500'.repeat(process.stdout.columns || 80));
diff --git a/node_modules/ava/lib/run-status.js b/node_modules/ava/lib/run-status.js
index 6526f7bdc..8e095655a 100644
--- a/node_modules/ava/lib/run-status.js
+++ b/node_modules/ava/lib/run-status.js
@@ -40,6 +40,7 @@ class RunStatus extends EventEmitter {
this.stats = [];
this.tests = [];
this.failFastEnabled = opts.failFast || false;
+ this.updateSnapshots = opts.updateSnapshots || false;
autoBind(this);
}
@@ -73,6 +74,7 @@ class RunStatus extends EventEmitter {
}
handleTeardown(data) {
this.emit('dependencies', data.file, data.dependencies, this);
+ this.emit('touchedFiles', data.touchedFiles);
}
handleStats(stats) {
this.emit('stats', stats, this);
diff --git a/node_modules/ava/lib/runner.js b/node_modules/ava/lib/runner.js
index 5f0edacb2..bda2132fd 100644
--- a/node_modules/ava/lib/runner.js
+++ b/node_modules/ava/lib/runner.js
@@ -2,9 +2,9 @@
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 snapshotManager = require('./snapshot-manager');
const TestCollection = require('./test-collection');
const validateTest = require('./validate-test');
@@ -49,16 +49,17 @@ class Runner extends EventEmitter {
this.file = options.file;
this.match = options.match || [];
+ this.projectDir = options.projectDir;
this.serial = options.serial;
this.updateSnapshots = options.updateSnapshots;
this.hasStarted = false;
this.results = [];
- this.snapshotState = null;
+ this.snapshots = null;
this.tests = new TestCollection({
bail: options.bail,
failWithoutAssertions: options.failWithoutAssertions,
- getSnapshotState: () => this.getSnapshotState()
+ compareTestSnapshot: this.compareTestSnapshot.bind(this)
});
this.chain = optionChain(chainableMethods, (opts, args) => {
@@ -179,26 +180,31 @@ class Runner extends EventEmitter {
return stats;
}
- getSnapshotState() {
- if (this.snapshotState) {
- return this.snapshotState;
+ compareTestSnapshot(options) {
+ if (!this.snapshots) {
+ this.snapshots = snapshotManager.load({
+ name: path.basename(this.file),
+ projectDir: this.projectDir,
+ relFile: path.relative(this.projectDir, this.file),
+ testDir: path.dirname(this.file),
+ updating: this.updateSnapshots
+ });
+ this.emit('dependency', this.snapshots.snapPath);
}
- 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;
+ return this.snapshots.compare(options);
}
saveSnapshotState() {
- if (this.snapshotState) {
- this.snapshotState.save(this.updateSnapshots);
+ if (this.snapshots) {
+ const files = this.snapshots.save();
+ if (files) {
+ this.emit('touched', files);
+ }
+ } else if (this.updateSnapshots) {
+ // TODO: There may be unused snapshot files if no test caused the
+ // snapshots to be loaded. Prune them. But not if tests (including hooks!)
+ // were skipped. Perhaps emit a warning if this occurs?
}
}
diff --git a/node_modules/ava/lib/snapshot-manager.js b/node_modules/ava/lib/snapshot-manager.js
new file mode 100644
index 000000000..ea1246585
--- /dev/null
+++ b/node_modules/ava/lib/snapshot-manager.js
@@ -0,0 +1,396 @@
+'use strict';
+
+const crypto = require('crypto');
+const fs = require('fs');
+const path = require('path');
+const zlib = require('zlib');
+
+const writeFileAtomic = require('@ava/write-file-atomic');
+const concordance = require('concordance');
+const indentString = require('indent-string');
+const makeDir = require('make-dir');
+const md5Hex = require('md5-hex');
+const Buffer = require('safe-buffer').Buffer;
+
+const concordanceOptions = require('./concordance-options').snapshotManager;
+
+// Increment if encoding layout or Concordance serialization versions change. Previous AVA versions will not be able to
+// decode buffers generated by a newer version, so changing this value will require a major version bump of AVA itself.
+// The version is encoded as an unsigned 16 bit integer.
+const VERSION = 1;
+
+const VERSION_HEADER = Buffer.alloc(2);
+VERSION_HEADER.writeUInt16LE(VERSION);
+
+// The decoder matches on the trailing newline byte (0x0A).
+const READABLE_PREFIX = Buffer.from(`AVA Snapshot v${VERSION}\n`, 'ascii');
+const REPORT_SEPARATOR = Buffer.from('\n\n', 'ascii');
+const REPORT_TRAILING_NEWLINE = Buffer.from('\n', 'ascii');
+
+const MD5_HASH_LENGTH = 16;
+
+class SnapshotError extends Error {
+ constructor(message, snapPath) {
+ super(message);
+ this.name = 'SnapshotError';
+ this.snapPath = snapPath;
+ }
+}
+exports.SnapshotError = SnapshotError;
+
+class ChecksumError extends SnapshotError {
+ constructor(snapPath) {
+ super('Checksum mismatch', snapPath);
+ this.name = 'ChecksumError';
+ }
+}
+exports.ChecksumError = ChecksumError;
+
+class VersionMismatchError extends SnapshotError {
+ constructor(snapPath, version) {
+ super('Unexpected snapshot version', snapPath);
+ this.name = 'VersionMismatchError';
+ this.snapVersion = version;
+ this.expectedVersion = VERSION;
+ }
+}
+exports.VersionMismatchError = VersionMismatchError;
+
+const LEGACY_SNAPSHOT_HEADER = Buffer.from('// Jest Snapshot v1');
+function isLegacySnapshot(buffer) {
+ return LEGACY_SNAPSHOT_HEADER.equals(buffer.slice(0, LEGACY_SNAPSHOT_HEADER.byteLength));
+}
+
+class LegacyError extends SnapshotError {
+ constructor(snapPath) {
+ super('Legacy snapshot file', snapPath);
+ this.name = 'LegacyError';
+ }
+}
+exports.LegacyError = LegacyError;
+
+function tryRead(file) {
+ try {
+ return fs.readFileSync(file);
+ } catch (err) {
+ if (err.code === 'ENOENT') {
+ return null;
+ }
+
+ throw err;
+ }
+}
+
+function withoutLineEndings(buffer) {
+ let newLength = buffer.byteLength - 1;
+ while (buffer[newLength] === 0x0A || buffer[newLength] === 0x0D) {
+ newLength--;
+ }
+ return buffer.slice(0, newLength);
+}
+
+function formatEntry(label, descriptor) {
+ if (label) {
+ label = `> ${label}\n\n`;
+ }
+ const codeBlock = indentString(concordance.formatDescriptor(descriptor, concordanceOptions), 4);
+ return Buffer.from(label + codeBlock, 'utf8');
+}
+
+function combineEntries(entries) {
+ const buffers = [];
+ let byteLength = 0;
+
+ const sortedKeys = Array.from(entries.keys()).sort();
+ for (const key of sortedKeys) {
+ const keyBuffer = Buffer.from(`\n\n## ${key}\n\n`, 'utf8');
+ buffers.push(keyBuffer);
+ byteLength += keyBuffer.byteLength;
+
+ const formattedEntries = entries.get(key);
+ const last = formattedEntries[formattedEntries.length - 1];
+ for (const entry of formattedEntries) {
+ buffers.push(entry);
+ byteLength += entry.byteLength;
+
+ if (entry !== last) {
+ buffers.push(REPORT_SEPARATOR);
+ byteLength += REPORT_SEPARATOR.byteLength;
+ }
+ }
+ }
+
+ return {buffers, byteLength};
+}
+
+function generateReport(relFile, snapFile, entries) {
+ const combined = combineEntries(entries);
+ const buffers = combined.buffers;
+ let byteLength = combined.byteLength;
+
+ const header = Buffer.from(`# Snapshot report for \`${relFile}\`
+
+The actual snapshot is saved in \`${snapFile}\`.
+
+Generated by [AVA](https://ava.li).`, 'utf8');
+ buffers.unshift(header);
+ byteLength += header.byteLength;
+
+ buffers.push(REPORT_TRAILING_NEWLINE);
+ byteLength += REPORT_TRAILING_NEWLINE.byteLength;
+ return Buffer.concat(buffers, byteLength);
+}
+
+function appendReportEntries(existingReport, entries) {
+ const combined = combineEntries(entries);
+ const buffers = combined.buffers;
+ let byteLength = combined.byteLength;
+
+ const prepend = withoutLineEndings(existingReport);
+ buffers.unshift(prepend);
+ byteLength += prepend.byteLength;
+
+ return Buffer.concat(buffers, byteLength);
+}
+
+function encodeSnapshots(buffersByHash) {
+ const buffers = [];
+ let byteOffset = 0;
+
+ // Entry start and end pointers are relative to the header length. This means
+ // it's possible to append new entries to an existing snapshot file, without
+ // having to rewrite pointers for existing entries.
+ const headerLength = Buffer.alloc(4);
+ buffers.push(headerLength);
+ byteOffset += 4;
+
+ // Allows 65535 hashes (tests or identified snapshots) per file.
+ const numHashes = Buffer.alloc(2);
+ numHashes.writeUInt16LE(buffersByHash.size);
+ buffers.push(numHashes);
+ byteOffset += 2;
+
+ const entries = [];
+ for (const pair of buffersByHash) {
+ const hash = pair[0];
+ const snapshotBuffers = pair[1];
+
+ buffers.push(Buffer.from(hash, 'hex'));
+ byteOffset += MD5_HASH_LENGTH;
+
+ // Allows 65535 snapshots per hash.
+ const numSnapshots = Buffer.alloc(2);
+ numSnapshots.writeUInt16LE(snapshotBuffers.length, 0);
+ buffers.push(numSnapshots);
+ byteOffset += 2;
+
+ for (const value of snapshotBuffers) {
+ // Each pointer is 32 bits, restricting the total, uncompressed buffer to
+ // 4 GiB.
+ const start = Buffer.alloc(4);
+ const end = Buffer.alloc(4);
+ entries.push({start, end, value});
+
+ buffers.push(start, end);
+ byteOffset += 8;
+ }
+ }
+
+ headerLength.writeUInt32LE(byteOffset, 0);
+
+ let bodyOffset = 0;
+ for (const entry of entries) {
+ const start = bodyOffset;
+ const end = bodyOffset + entry.value.byteLength;
+ entry.start.writeUInt32LE(start, 0);
+ entry.end.writeUInt32LE(end, 0);
+ buffers.push(entry.value);
+ bodyOffset = end;
+ }
+ byteOffset += bodyOffset;
+
+ const compressed = zlib.gzipSync(Buffer.concat(buffers, byteOffset));
+ const md5sum = crypto.createHash('md5').update(compressed).digest();
+ return Buffer.concat([
+ READABLE_PREFIX,
+ VERSION_HEADER,
+ md5sum,
+ compressed
+ ], READABLE_PREFIX.byteLength + VERSION_HEADER.byteLength + MD5_HASH_LENGTH + compressed.byteLength);
+}
+
+function decodeSnapshots(buffer, snapPath) {
+ if (isLegacySnapshot(buffer)) {
+ throw new LegacyError(snapPath);
+ }
+
+ // The version starts after the readable prefix, which is ended by a newline
+ // byte (0x0A).
+ const versionOffset = buffer.indexOf(0x0A) + 1;
+ const version = buffer.readUInt16LE(versionOffset);
+ if (version !== VERSION) {
+ throw new VersionMismatchError(snapPath, version);
+ }
+
+ const md5sumOffset = versionOffset + 2;
+ const compressedOffset = md5sumOffset + MD5_HASH_LENGTH;
+ const compressed = buffer.slice(compressedOffset);
+
+ const md5sum = crypto.createHash('md5').update(compressed).digest();
+ const expectedSum = buffer.slice(md5sumOffset, compressedOffset);
+ if (!md5sum.equals(expectedSum)) {
+ throw new ChecksumError(snapPath);
+ }
+
+ const decompressed = zlib.gunzipSync(compressed);
+ let byteOffset = 0;
+
+ const headerLength = decompressed.readUInt32LE(byteOffset);
+ byteOffset += 4;
+
+ const snapshotsByHash = new Map();
+ const numHashes = decompressed.readUInt16LE(byteOffset);
+ byteOffset += 2;
+
+ for (let count = 0; count < numHashes; count++) {
+ const hash = decompressed.toString('hex', byteOffset, byteOffset + MD5_HASH_LENGTH);
+ byteOffset += MD5_HASH_LENGTH;
+
+ const numSnapshots = decompressed.readUInt16LE(byteOffset);
+ byteOffset += 2;
+
+ const snapshotsBuffers = new Array(numSnapshots);
+ for (let index = 0; index < numSnapshots; index++) {
+ const start = decompressed.readUInt32LE(byteOffset) + headerLength;
+ byteOffset += 4;
+ const end = decompressed.readUInt32LE(byteOffset) + headerLength;
+ byteOffset += 4;
+ snapshotsBuffers[index] = decompressed.slice(start, end);
+ }
+
+ // Allow for new entries to be appended to an existing header, which could
+ // lead to the same hash being present multiple times.
+ if (snapshotsByHash.has(hash)) {
+ snapshotsByHash.set(hash, snapshotsByHash.get(hash).concat(snapshotsBuffers));
+ } else {
+ snapshotsByHash.set(hash, snapshotsBuffers);
+ }
+ }
+
+ return snapshotsByHash;
+}
+
+class Manager {
+ constructor(options) {
+ this.appendOnly = options.appendOnly;
+ this.dir = options.dir;
+ this.relFile = options.relFile;
+ this.reportFile = options.reportFile;
+ this.snapFile = options.snapFile;
+ this.snapPath = options.snapPath;
+ this.snapshotsByHash = options.snapshotsByHash;
+
+ this.hasChanges = false;
+ this.reportEntries = new Map();
+ }
+
+ compare(options) {
+ const hash = md5Hex(options.belongsTo);
+ const entries = this.snapshotsByHash.get(hash) || [];
+ if (options.index > entries.length) {
+ throw new RangeError(`Cannot record snapshot ${options.index} for ${JSON.stringify(options.belongsTo)}, exceeds expected index of ${entries.length}`);
+ }
+ if (options.index === entries.length) {
+ this.record(hash, options);
+ return {pass: true};
+ }
+
+ const snapshotBuffer = entries[options.index];
+ const actual = concordance.deserialize(snapshotBuffer, concordanceOptions);
+
+ const expected = concordance.describe(options.expected, concordanceOptions);
+ const pass = concordance.compareDescriptors(actual, expected);
+
+ return {actual, expected, pass};
+ }
+
+ record(hash, options) {
+ const descriptor = concordance.describe(options.expected, concordanceOptions);
+
+ this.hasChanges = true;
+ const snapshot = concordance.serialize(descriptor);
+ if (this.snapshotsByHash.has(hash)) {
+ this.snapshotsByHash.get(hash).push(snapshot);
+ } else {
+ this.snapshotsByHash.set(hash, [snapshot]);
+ }
+
+ const entry = formatEntry(options.label, descriptor);
+ if (this.reportEntries.has(options.belongsTo)) {
+ this.reportEntries.get(options.belongsTo).push(entry);
+ } else {
+ this.reportEntries.set(options.belongsTo, [entry]);
+ }
+ }
+
+ save() {
+ if (!this.hasChanges) {
+ return null;
+ }
+
+ const snapPath = this.snapPath;
+ const buffer = encodeSnapshots(this.snapshotsByHash);
+
+ const reportPath = path.join(this.dir, this.reportFile);
+ const existingReport = this.appendOnly ? tryRead(reportPath) : null;
+ const reportBuffer = existingReport ?
+ appendReportEntries(existingReport, this.reportEntries) :
+ generateReport(this.relFile, this.snapFile, this.reportEntries);
+
+ makeDir.sync(this.dir);
+ const tmpSnapPath = writeFileAtomic.sync(snapPath, buffer);
+ const tmpReportPath = writeFileAtomic.sync(reportPath, reportBuffer);
+
+ return [tmpSnapPath, tmpReportPath, snapPath, reportPath];
+ }
+}
+
+function determineSnapshotDir(projectDir, testDir) {
+ const parts = new Set(path.relative(projectDir, testDir).split(path.sep));
+ if (parts.has('__tests__')) {
+ return path.join(testDir, '__snapshots__');
+ } else if (parts.has('test') || parts.has('tests')) { // Accept tests, even though it's not in the default test patterns
+ return path.join(testDir, 'snapshots');
+ }
+ return testDir;
+}
+
+function load(options) {
+ const dir = determineSnapshotDir(options.projectDir, options.testDir);
+ const reportFile = `${options.name}.md`;
+ const snapFile = `${options.name}.snap`;
+ const snapPath = path.join(dir, snapFile);
+
+ let appendOnly = !options.updating;
+ let snapshotsByHash;
+
+ if (!options.updating) {
+ const buffer = tryRead(snapPath);
+ if (buffer) {
+ snapshotsByHash = decodeSnapshots(buffer, snapPath);
+ } else {
+ appendOnly = false;
+ }
+ }
+
+ return new Manager({
+ appendOnly,
+ dir,
+ relFile: options.relFile,
+ reportFile,
+ snapFile,
+ snapPath,
+ snapshotsByHash: snapshotsByHash || new Map()
+ });
+}
+exports.load = load;
diff --git a/node_modules/ava/lib/test-collection.js b/node_modules/ava/lib/test-collection.js
index 5404cb119..91c604e06 100644
--- a/node_modules/ava/lib/test-collection.js
+++ b/node_modules/ava/lib/test-collection.js
@@ -11,7 +11,7 @@ class TestCollection extends EventEmitter {
this.bail = options.bail;
this.failWithoutAssertions = options.failWithoutAssertions;
- this.getSnapshotState = options.getSnapshotState;
+ this.compareTestSnapshot = options.compareTestSnapshot;
this.hasExclusive = false;
this.testCount = 0;
@@ -133,7 +133,7 @@ class TestCollection extends EventEmitter {
contextRef,
failWithoutAssertions: false,
fn: hook.fn,
- getSnapshotState: this.getSnapshotState,
+ compareTestSnapshot: this.compareTestSnapshot,
metadata: hook.metadata,
onResult: this._emitTestResult,
title
@@ -150,7 +150,7 @@ class TestCollection extends EventEmitter {
contextRef,
failWithoutAssertions: this.failWithoutAssertions,
fn: test.fn,
- getSnapshotState: this.getSnapshotState,
+ compareTestSnapshot: this.compareTestSnapshot,
metadata: test.metadata,
onResult: this._emitTestResult,
title: test.title
diff --git a/node_modules/ava/lib/test-worker.js b/node_modules/ava/lib/test-worker.js
index 2df7f745d..0061775f0 100644
--- a/node_modules/ava/lib/test-worker.js
+++ b/node_modules/ava/lib/test-worker.js
@@ -17,18 +17,18 @@
}
}
-/* 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;
+/* eslint-enable import/order */
+const Bluebird = require('bluebird');
+const currentlyUnhandled = require('currently-unhandled')();
+const isObj = require('is-obj');
+const serializeError = require('./serialize-error');
+
// Bluebird specific
Bluebird.longStackTraces();
@@ -37,16 +37,28 @@ Bluebird.longStackTraces();
adapter.installSourceMapSupport();
adapter.installPrecompilerHook();
-const dependencies = [];
+const testPath = opts.file;
+
+const dependencies = new Set();
adapter.installDependencyTracking(dependencies, testPath);
+const touchedFiles = new Set();
+
// Set when main.js is required (since test files should have `require('ava')`).
let runner = null;
exports.setRunner = newRunner => {
runner = newRunner;
+ runner.on('dependency', file => {
+ dependencies.add(file);
+ });
+ runner.on('touched', files => {
+ for (const file of files) {
+ touchedFiles.add(file);
+ }
+ });
};
-require(testPath); // eslint-disable-line import/no-dynamic-require
+require(testPath);
// If AVA was not required, show an error
if (!runner) {
@@ -121,8 +133,12 @@ process.on('ava-teardown', () => {
// 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});
+ // it flat out crashes. Also include any files that AVA touched during the
+ // test run. This allows the watcher to ignore modifications to those files.
+ adapter.send('teardown', {
+ dependencies: Array.from(dependencies),
+ touchedFiles: Array.from(touchedFiles)
+ });
});
process.on('ava-exit', () => {
diff --git a/node_modules/ava/lib/test.js b/node_modules/ava/lib/test.js
index a9b0fb1d9..58be54d32 100644
--- a/node_modules/ava/lib/test.js
+++ b/node_modules/ava/lib/test.js
@@ -1,13 +1,19 @@
'use strict';
const isGeneratorFn = require('is-generator-fn');
const co = require('co-with-promise');
+const concordance = require('concordance');
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');
+const concordanceOptions = require('./concordance-options').default;
+
+function formatErrorValue(label, error) {
+ const formatted = concordance.format(error, concordanceOptions);
+ return {label, formatted};
+}
class SkipApi {
constructor(test) {
@@ -26,8 +32,10 @@ const captureStack = start => {
class ExecutionContext {
constructor(test) {
- this._test = test;
- this.skip = new SkipApi(test);
+ Object.defineProperties(this, {
+ _test: {value: test},
+ skip: {value: new SkipApi(test)}
+ });
}
plan(ct) {
@@ -67,7 +75,6 @@ class ExecutionContext {
this._test.trackThrows(null);
}
}
-Object.defineProperty(ExecutionContext.prototype, 'context', {enumerable: true});
{
const assertions = assert.wrapAssertions({
@@ -98,11 +105,19 @@ class Test {
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.snapshotInvocationCount = 0;
+ this.compareWithSnapshot = assertionOptions => {
+ const belongsTo = assertionOptions.id || this.title;
+ const expected = assertionOptions.expected;
+ const index = assertionOptions.id ? 0 : this.snapshotInvocationCount++;
+ const label = assertionOptions.id ? '' : assertionOptions.message || `Snapshot ${this.snapshotInvocationCount}`;
+ return options.compareTestSnapshot({belongsTo, expected, index, label});
+ };
+
this.assertCount = 0;
this.assertError = undefined;
this.calledEnd = false;
@@ -139,7 +154,7 @@ class Test {
actual: err,
message: 'Callback called with an error',
stack,
- values: [formatAssertError.formatWithLabel('Error:', err)]
+ values: [formatErrorValue('Callback called with an error:', err)]
}));
}
@@ -234,7 +249,7 @@ class Test {
const values = [];
if (err) {
- values.push(formatAssertError.formatWithLabel(`The following error was thrown, possibly before \`t.${pending.assertion}()\` could be called:`, err));
+ values.push(formatErrorValue(`The following error was thrown, possibly before \`t.${pending.assertion}()\` could be called:`, err));
}
this.saveFirstError(new assert.AssertionError({
@@ -297,7 +312,7 @@ class Test {
this.saveFirstError(new assert.AssertionError({
message: 'Error thrown in test',
stack: result.error instanceof Error && result.error.stack,
- values: [formatAssertError.formatWithLabel('Error:', result.error)]
+ values: [formatErrorValue('Error thrown in test:', result.error)]
}));
}
return this.finish();
@@ -361,7 +376,7 @@ class Test {
this.saveFirstError(new assert.AssertionError({
message: 'Rejected promise returned by test',
stack: err instanceof Error && err.stack,
- values: [formatAssertError.formatWithLabel('Rejection reason:', err)]
+ values: [formatErrorValue('Rejected promise returned by test. Reason:', err)]
}));
}
})
diff --git a/node_modules/ava/lib/watcher.js b/node_modules/ava/lib/watcher.js
index 3d7094ffb..c90c810f0 100644
--- a/node_modules/ava/lib/watcher.js
+++ b/node_modules/ava/lib/watcher.js
@@ -16,18 +16,23 @@ function rethrowAsync(err) {
});
}
+const MIN_DEBOUNCE_DELAY = 10;
+const INITIAL_DEBOUNCE_DELAY = 100;
+
class Debouncer {
constructor(watcher) {
this.watcher = watcher;
this.timer = null;
this.repeat = false;
}
- debounce() {
+ debounce(delay) {
if (this.timer) {
this.again = true;
return;
}
+ delay = delay ? Math.max(delay, MIN_DEBOUNCE_DELAY) : INITIAL_DEBOUNCE_DELAY;
+
const timer = setTimeout(() => {
this.watcher.busy.then(() => {
// Do nothing if debouncing was canceled while waiting for the busy
@@ -39,14 +44,14 @@ class Debouncer {
if (this.again) {
this.timer = null;
this.again = false;
- this.debounce();
+ this.debounce(delay / 2);
} else {
this.watcher.runAfterChanges();
this.timer = null;
this.again = false;
}
});
- }, 10);
+ }, delay);
this.timer = timer;
}
@@ -79,7 +84,8 @@ class Watcher {
this.clearLogOnNextRun = true;
this.runVector = 0;
- this.run = specificFiles => {
+ this.previousFiles = files;
+ this.run = (specificFiles, updateSnapshots) => {
if (this.runVector > 0) {
const cleared = this.clearLogOnNextRun && logger.clear();
if (!cleared) {
@@ -111,7 +117,9 @@ class Watcher {
}
}
- this.busy = api.run(specificFiles || files, {runOnlyExclusive})
+ this.touchedFiles.clear();
+ this.previousFiles = specificFiles || files;
+ this.busy = api.run(this.previousFiles, {runOnlyExclusive, updateSnapshots: updateSnapshots === true})
.then(runStatus => {
runStatus.previousFailCount = this.sumPreviousFailures(currentVector);
logger.finish(runStatus);
@@ -125,6 +133,9 @@ class Watcher {
this.testDependencies = [];
this.trackTestDependencies(api, sources);
+ this.touchedFiles = new Set();
+ this.trackTouchedFiles(api);
+
this.filesWithExclusiveTests = [];
this.trackExclusivity(api);
@@ -179,6 +190,15 @@ class Watcher {
this.testDependencies.push(new TestDependency(file, sources));
}
}
+ trackTouchedFiles(api) {
+ api.on('test-run', runStatus => {
+ runStatus.on('touchedFiles', files => {
+ for (const file of files) {
+ this.touchedFiles.add(nodePath.relative(process.cwd(), file));
+ }
+ });
+ });
+ }
trackExclusivity(api) {
api.on('stats', stats => {
this.updateExclusivity(stats.file, stats.hasExclusive);
@@ -255,7 +275,7 @@ class Watcher {
stdin.on('data', data => {
data = data.trim().toLowerCase();
- if (data !== 'r' && data !== 'rs') {
+ if (data !== 'r' && data !== 'rs' && data !== 'u') {
return;
}
@@ -267,7 +287,11 @@ class Watcher {
// the busy promise to fulfil
this.debouncer.cancel();
this.clearLogOnNextRun = false;
- this.rerunAll();
+ if (data === 'u') {
+ this.updatePreviousSnapshots();
+ } else {
+ this.rerunAll();
+ }
});
});
}
@@ -275,11 +299,22 @@ class Watcher {
this.dirtyStates = {};
this.run();
}
+ updatePreviousSnapshots() {
+ this.dirtyStates = {};
+ this.run(this.previousFiles, true);
+ }
runAfterChanges() {
const dirtyStates = this.dirtyStates;
this.dirtyStates = {};
- const dirtyPaths = Object.keys(dirtyStates);
+ const dirtyPaths = Object.keys(dirtyStates).filter(path => {
+ if (this.touchedFiles.has(path)) {
+ debug('Ignoring known touched file %s', path);
+ this.touchedFiles.delete(path);
+ return false;
+ }
+ return true;
+ });
const dirtyTests = dirtyPaths.filter(this.avaFiles.isTest);
const dirtySources = diff(dirtyPaths, dirtyTests);
const addedOrChangedTests = dirtyTests.filter(path => dirtyStates[path] !== 'unlink');
@@ -309,7 +344,8 @@ class Watcher {
// 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');
+ debug('Sources remain that cannot be traced to specific tests: %O', dirtySources);
+ debug('Rerunning all tests');
this.run();
return;
}