replace jest with ava
This commit is contained in:
parent
27cc8b23fe
commit
0347b72305
11775 changed files with 84546 additions and 1440575 deletions
514
node_modules/ava/lib/runner.js
generated
vendored
Normal file
514
node_modules/ava/lib/runner.js
generated
vendored
Normal file
|
|
@ -0,0 +1,514 @@
|
|||
'use strict';
|
||||
const Emittery = require('emittery');
|
||||
const matcher = require('matcher');
|
||||
const ContextRef = require('./context-ref');
|
||||
const createChain = require('./create-chain');
|
||||
const parseTestArgs = require('./parse-test-args');
|
||||
const snapshotManager = require('./snapshot-manager');
|
||||
const serializeError = require('./serialize-error');
|
||||
const Runnable = require('./test');
|
||||
|
||||
class Runner extends Emittery {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
this.experiments = options.experiments || {};
|
||||
this.failFast = options.failFast === true;
|
||||
this.failWithoutAssertions = options.failWithoutAssertions !== false;
|
||||
this.file = options.file;
|
||||
this.checkSelectedByLineNumbers = options.checkSelectedByLineNumbers;
|
||||
this.match = options.match || [];
|
||||
this.powerAssert = undefined; // Assigned later.
|
||||
this.projectDir = options.projectDir;
|
||||
this.recordNewSnapshots = options.recordNewSnapshots === true;
|
||||
this.runOnlyExclusive = options.runOnlyExclusive === true;
|
||||
this.serial = options.serial === true;
|
||||
this.snapshotDir = options.snapshotDir;
|
||||
this.updateSnapshots = options.updateSnapshots;
|
||||
|
||||
this.activeRunnables = new Set();
|
||||
this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
|
||||
this.interrupted = false;
|
||||
this.snapshots = null;
|
||||
this.tasks = {
|
||||
after: [],
|
||||
afterAlways: [],
|
||||
afterEach: [],
|
||||
afterEachAlways: [],
|
||||
before: [],
|
||||
beforeEach: [],
|
||||
concurrent: [],
|
||||
serial: [],
|
||||
todo: []
|
||||
};
|
||||
|
||||
const uniqueTestTitles = new Set();
|
||||
this.registerUniqueTitle = title => {
|
||||
if (uniqueTestTitles.has(title)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uniqueTestTitles.add(title);
|
||||
return true;
|
||||
};
|
||||
|
||||
let hasStarted = false;
|
||||
let scheduledStart = false;
|
||||
const meta = Object.freeze({
|
||||
file: options.file,
|
||||
get snapshotDirectory() {
|
||||
const {file, snapshotDir: fixedLocation, projectDir} = options;
|
||||
return snapshotManager.determineSnapshotDir({file, fixedLocation, projectDir});
|
||||
}
|
||||
});
|
||||
this.chain = createChain((metadata, testArgs) => { // eslint-disable-line complexity
|
||||
if (hasStarted) {
|
||||
throw new Error('All tests and hooks must be declared synchronously in your test file, and cannot be nested within other tests or hooks.');
|
||||
}
|
||||
|
||||
if (!scheduledStart) {
|
||||
scheduledStart = true;
|
||||
process.nextTick(() => {
|
||||
hasStarted = true;
|
||||
this.start();
|
||||
});
|
||||
}
|
||||
|
||||
const {args, buildTitle, implementations, rawTitle} = parseTestArgs(testArgs);
|
||||
|
||||
if (this.checkSelectedByLineNumbers) {
|
||||
metadata.selected = this.checkSelectedByLineNumbers();
|
||||
}
|
||||
|
||||
if (metadata.todo) {
|
||||
if (implementations.length > 0) {
|
||||
throw new TypeError('`todo` tests are not allowed to have an implementation. Use `test.skip()` for tests with an implementation.');
|
||||
}
|
||||
|
||||
if (!rawTitle) { // Either undefined or a string.
|
||||
throw new TypeError('`todo` tests require a title');
|
||||
}
|
||||
|
||||
if (!this.registerUniqueTitle(rawTitle)) {
|
||||
throw new Error(`Duplicate test title: ${rawTitle}`);
|
||||
}
|
||||
|
||||
if (this.match.length > 0) {
|
||||
// --match selects TODO tests.
|
||||
if (matcher([rawTitle], this.match).length === 1) {
|
||||
metadata.exclusive = true;
|
||||
this.runOnlyExclusive = true;
|
||||
}
|
||||
}
|
||||
|
||||
this.tasks.todo.push({title: rawTitle, metadata});
|
||||
this.emit('stateChange', {
|
||||
type: 'declared-test',
|
||||
title: rawTitle,
|
||||
knownFailing: false,
|
||||
todo: true
|
||||
});
|
||||
} else {
|
||||
if (implementations.length === 0) {
|
||||
throw new TypeError('Expected an implementation. Use `test.todo()` for tests without an implementation.');
|
||||
}
|
||||
|
||||
for (const implementation of implementations) {
|
||||
let {title, isSet, isValid, isEmpty} = buildTitle(implementation);
|
||||
|
||||
if (isSet && !isValid) {
|
||||
throw new TypeError('Test & hook titles must be strings');
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
if (metadata.type === 'test') {
|
||||
throw new TypeError('Tests must have a title');
|
||||
} else if (metadata.always) {
|
||||
title = `${metadata.type}.always hook`;
|
||||
} else {
|
||||
title = `${metadata.type} hook`;
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata.type === 'test' && !this.registerUniqueTitle(title)) {
|
||||
throw new Error(`Duplicate test title: ${title}`);
|
||||
}
|
||||
|
||||
const task = {
|
||||
title,
|
||||
implementation,
|
||||
args,
|
||||
metadata: {...metadata}
|
||||
};
|
||||
|
||||
if (metadata.type === 'test') {
|
||||
if (this.match.length > 0) {
|
||||
// --match overrides .only()
|
||||
task.metadata.exclusive = matcher([title], this.match).length === 1;
|
||||
}
|
||||
|
||||
if (task.metadata.exclusive) {
|
||||
this.runOnlyExclusive = true;
|
||||
}
|
||||
|
||||
this.tasks[metadata.serial ? 'serial' : 'concurrent'].push(task);
|
||||
this.emit('stateChange', {
|
||||
type: 'declared-test',
|
||||
title,
|
||||
knownFailing: metadata.failing,
|
||||
todo: false
|
||||
});
|
||||
} else if (!metadata.skipped) {
|
||||
this.tasks[metadata.type + (metadata.always ? 'Always' : '')].push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
serial: false,
|
||||
exclusive: false,
|
||||
skipped: false,
|
||||
todo: false,
|
||||
failing: false,
|
||||
callback: false,
|
||||
inline: false, // Set for attempt metadata created by `t.try()`
|
||||
always: false
|
||||
}, meta);
|
||||
}
|
||||
|
||||
compareTestSnapshot(options) {
|
||||
if (!this.snapshots) {
|
||||
this.snapshots = snapshotManager.load({
|
||||
file: this.file,
|
||||
fixedLocation: this.snapshotDir,
|
||||
projectDir: this.projectDir,
|
||||
recordNewSnapshots: this.recordNewSnapshots,
|
||||
updating: this.updateSnapshots
|
||||
});
|
||||
this.emit('dependency', this.snapshots.snapPath);
|
||||
}
|
||||
|
||||
return this.snapshots.compare(options);
|
||||
}
|
||||
|
||||
saveSnapshotState() {
|
||||
if (this.snapshots) {
|
||||
return this.snapshots.save();
|
||||
}
|
||||
|
||||
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?
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
onRun(runnable) {
|
||||
this.activeRunnables.add(runnable);
|
||||
}
|
||||
|
||||
onRunComplete(runnable) {
|
||||
this.activeRunnables.delete(runnable);
|
||||
}
|
||||
|
||||
attributeLeakedError(err) {
|
||||
for (const runnable of this.activeRunnables) {
|
||||
if (runnable.attributeLeakedError(err)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
beforeExitHandler() {
|
||||
for (const runnable of this.activeRunnables) {
|
||||
runnable.finishDueToInactivity();
|
||||
}
|
||||
}
|
||||
|
||||
async runMultiple(runnables) {
|
||||
let allPassed = true;
|
||||
const storedResults = [];
|
||||
const runAndStoreResult = async runnable => {
|
||||
const result = await this.runSingle(runnable);
|
||||
if (!result.passed) {
|
||||
allPassed = false;
|
||||
}
|
||||
|
||||
storedResults.push(result);
|
||||
};
|
||||
|
||||
let waitForSerial = Promise.resolve();
|
||||
await runnables.reduce((previous, runnable) => {
|
||||
if (runnable.metadata.serial || this.serial) {
|
||||
waitForSerial = previous.then(() => {
|
||||
// Serial runnables run as long as there was no previous failure, unless
|
||||
// the runnable should always be run.
|
||||
return (allPassed || runnable.metadata.always) && runAndStoreResult(runnable);
|
||||
});
|
||||
return waitForSerial;
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
previous,
|
||||
waitForSerial.then(() => {
|
||||
// Concurrent runnables are kicked off after the previous serial
|
||||
// runnables have completed, as long as there was no previous failure
|
||||
// (or if the runnable should always be run). One concurrent runnable's
|
||||
// failure does not prevent the next runnable from running.
|
||||
return (allPassed || runnable.metadata.always) && runAndStoreResult(runnable);
|
||||
})
|
||||
]);
|
||||
}, waitForSerial);
|
||||
|
||||
return {allPassed, storedResults};
|
||||
}
|
||||
|
||||
async runSingle(runnable) {
|
||||
this.onRun(runnable);
|
||||
const result = await runnable.run();
|
||||
// If run() throws or rejects then the entire test run crashes, so
|
||||
// onRunComplete() doesn't *have* to be inside a finally.
|
||||
this.onRunComplete(runnable);
|
||||
return result;
|
||||
}
|
||||
|
||||
async runHooks(tasks, contextRef, titleSuffix, testPassed) {
|
||||
const hooks = tasks.map(task => new Runnable({
|
||||
contextRef,
|
||||
experiments: this.experiments,
|
||||
failWithoutAssertions: false,
|
||||
fn: task.args.length === 0 ?
|
||||
task.implementation :
|
||||
t => task.implementation.apply(null, [t].concat(task.args)),
|
||||
compareTestSnapshot: this.boundCompareTestSnapshot,
|
||||
updateSnapshots: this.updateSnapshots,
|
||||
metadata: task.metadata,
|
||||
powerAssert: this.powerAssert,
|
||||
title: `${task.title}${titleSuffix || ''}`,
|
||||
isHook: true,
|
||||
testPassed
|
||||
}));
|
||||
const outcome = await this.runMultiple(hooks, this.serial);
|
||||
for (const result of outcome.storedResults) {
|
||||
if (result.passed) {
|
||||
this.emit('stateChange', {
|
||||
type: 'hook-finished',
|
||||
title: result.title,
|
||||
duration: result.duration,
|
||||
logs: result.logs
|
||||
});
|
||||
} else {
|
||||
this.emit('stateChange', {
|
||||
type: 'hook-failed',
|
||||
title: result.title,
|
||||
err: serializeError('Hook failure', true, result.error),
|
||||
duration: result.duration,
|
||||
logs: result.logs
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return outcome.allPassed;
|
||||
}
|
||||
|
||||
async runTest(task, contextRef) {
|
||||
const hookSuffix = ` for ${task.title}`;
|
||||
let hooksOk = await this.runHooks(this.tasks.beforeEach, contextRef, hookSuffix);
|
||||
|
||||
let testOk = false;
|
||||
if (hooksOk) {
|
||||
// Only run the test if all `beforeEach` hooks passed.
|
||||
const test = new Runnable({
|
||||
contextRef,
|
||||
experiments: this.experiments,
|
||||
failWithoutAssertions: this.failWithoutAssertions,
|
||||
fn: task.args.length === 0 ?
|
||||
task.implementation :
|
||||
t => task.implementation.apply(null, [t].concat(task.args)),
|
||||
compareTestSnapshot: this.boundCompareTestSnapshot,
|
||||
updateSnapshots: this.updateSnapshots,
|
||||
metadata: task.metadata,
|
||||
powerAssert: this.powerAssert,
|
||||
title: task.title,
|
||||
registerUniqueTitle: this.registerUniqueTitle
|
||||
});
|
||||
|
||||
const result = await this.runSingle(test);
|
||||
testOk = result.passed;
|
||||
|
||||
if (testOk) {
|
||||
this.emit('stateChange', {
|
||||
type: 'test-passed',
|
||||
title: result.title,
|
||||
duration: result.duration,
|
||||
knownFailing: result.metadata.failing,
|
||||
logs: result.logs
|
||||
});
|
||||
|
||||
hooksOk = await this.runHooks(this.tasks.afterEach, contextRef, hookSuffix, testOk);
|
||||
} else {
|
||||
this.emit('stateChange', {
|
||||
type: 'test-failed',
|
||||
title: result.title,
|
||||
err: serializeError('Test failure', true, result.error, this.file),
|
||||
duration: result.duration,
|
||||
knownFailing: result.metadata.failing,
|
||||
logs: result.logs
|
||||
});
|
||||
// Don't run `afterEach` hooks if the test failed.
|
||||
}
|
||||
}
|
||||
|
||||
const alwaysOk = await this.runHooks(this.tasks.afterEachAlways, contextRef, hookSuffix, testOk);
|
||||
return alwaysOk && hooksOk && testOk;
|
||||
}
|
||||
|
||||
async start() {
|
||||
const concurrentTests = [];
|
||||
const serialTests = [];
|
||||
for (const task of this.tasks.serial) {
|
||||
if (this.runOnlyExclusive && !task.metadata.exclusive) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.emit('stateChange', {
|
||||
type: 'selected-test',
|
||||
title: task.title,
|
||||
knownFailing: task.metadata.failing,
|
||||
skip: task.metadata.skipped,
|
||||
todo: false
|
||||
});
|
||||
|
||||
if (!task.metadata.skipped) {
|
||||
serialTests.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
for (const task of this.tasks.concurrent) {
|
||||
if (this.runOnlyExclusive && !task.metadata.exclusive) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.emit('stateChange', {
|
||||
type: 'selected-test',
|
||||
title: task.title,
|
||||
knownFailing: task.metadata.failing,
|
||||
skip: task.metadata.skipped,
|
||||
todo: false
|
||||
});
|
||||
|
||||
if (!task.metadata.skipped) {
|
||||
if (this.serial) {
|
||||
serialTests.push(task);
|
||||
} else {
|
||||
concurrentTests.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const task of this.tasks.todo) {
|
||||
if (this.runOnlyExclusive && !task.metadata.exclusive) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this.checkSelectedByLineNumbers && !task.metadata.selected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.emit('stateChange', {
|
||||
type: 'selected-test',
|
||||
title: task.title,
|
||||
knownFailing: false,
|
||||
skip: false,
|
||||
todo: true
|
||||
});
|
||||
}
|
||||
|
||||
if (concurrentTests.length === 0 && serialTests.length === 0) {
|
||||
this.emit('finish');
|
||||
// Don't run any hooks if there are no tests to run.
|
||||
return;
|
||||
}
|
||||
|
||||
const contextRef = new ContextRef();
|
||||
|
||||
// Note that the hooks and tests always begin running asynchronously.
|
||||
const beforePromise = this.runHooks(this.tasks.before, contextRef);
|
||||
const serialPromise = beforePromise.then(beforeHooksOk => { // eslint-disable-line promise/prefer-await-to-then
|
||||
// Don't run tests if a `before` hook failed.
|
||||
if (!beforeHooksOk) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return serialTests.reduce(async (previous, task) => {
|
||||
const previousOk = await previous;
|
||||
// Don't start tests after an interrupt.
|
||||
if (this.interrupted) {
|
||||
return previousOk;
|
||||
}
|
||||
|
||||
// Prevent subsequent tests from running if `failFast` is enabled and
|
||||
// the previous test failed.
|
||||
if (!previousOk && this.failFast) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.runTest(task, contextRef.copy());
|
||||
}, true);
|
||||
});
|
||||
const concurrentPromise = Promise.all([beforePromise, serialPromise]).then(async ([beforeHooksOk, serialOk]) => { // eslint-disable-line promise/prefer-await-to-then
|
||||
// Don't run tests if a `before` hook failed, or if `failFast` is enabled
|
||||
// and a previous serial test failed.
|
||||
if (!beforeHooksOk || (!serialOk && this.failFast)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't start tests after an interrupt.
|
||||
if (this.interrupted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If a concurrent test fails, even if `failFast` is enabled it won't
|
||||
// stop other concurrent tests from running.
|
||||
const allOkays = await Promise.all(concurrentTests.map(task => {
|
||||
return this.runTest(task, contextRef.copy());
|
||||
}));
|
||||
return allOkays.every(ok => ok);
|
||||
});
|
||||
|
||||
const beforeExitHandler = this.beforeExitHandler.bind(this);
|
||||
process.on('beforeExit', beforeExitHandler);
|
||||
|
||||
try {
|
||||
const ok = await concurrentPromise;
|
||||
// Only run `after` hooks if all hooks and tests passed.
|
||||
if (ok) {
|
||||
await this.runHooks(this.tasks.after, contextRef);
|
||||
}
|
||||
|
||||
// Always run `after.always` hooks.
|
||||
await this.runHooks(this.tasks.afterAlways, contextRef);
|
||||
process.removeListener('beforeExit', beforeExitHandler);
|
||||
await this.emit('finish');
|
||||
} catch (error) {
|
||||
await this.emit('error', error);
|
||||
}
|
||||
}
|
||||
|
||||
interrupt() {
|
||||
this.interrupted = true;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Runner;
|
||||
Loading…
Add table
Add a link
Reference in a new issue