Skip to content

Overhaul test implementation 👷🏗 #1314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Mar 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions lib/assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ class AssertionError extends Error {
}
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;
Expand Down Expand Up @@ -138,7 +144,7 @@ function wrapAssertions(callbacks) {
coreAssertThrowsErrorArg = err;
}

const test = fn => {
const test = (fn, stack) => {
let actual;
let threw = false;
try {
Expand All @@ -160,15 +166,19 @@ function wrapAssertions(callbacks) {
throw new AssertionError({
assertion: 'throws',
message,
stack,
values
});
}
};

if (promise) {
const result = promise.then(makeNoop, makeRethrow).then(test);
pending(this, result);
return result;
// 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 {
Expand All @@ -195,23 +205,26 @@ function wrapAssertions(callbacks) {
return;
}

const test = fn => {
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) {
const result = promise
.then(noop, reason => test(makeRethrow(reason)));
pending(this, result);
return result;
// 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 {
Expand Down
1 change: 1 addition & 0 deletions lib/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ exports.run = () => {

const api = new Api({
failFast: conf.failFast,
failWithoutAssertions: conf.failWithoutAssertions !== false,
serial: conf.serial,
require: arrify(conf.require),
cacheEnabled: conf.cache !== false,
Expand Down
101 changes: 41 additions & 60 deletions lib/concurrent.js
Original file line number Diff line number Diff line change
@@ -1,82 +1,63 @@
'use strict';
const Promise = require('bluebird');
const isPromise = require('is-promise');
const autoBind = require('auto-bind');
const AvaError = require('./ava-error');

class Concurrent {
constructor(tests, bail) {
if (!Array.isArray(tests)) {
throw new TypeError('Expected an array of tests');
constructor(runnables, bail) {
if (!Array.isArray(runnables)) {
throw new TypeError('Expected an array of runnables');
}

this.results = [];
this.passed = true;
this.reason = null;
this.tests = tests;
this.runnables = runnables;
this.bail = bail || false;

autoBind(this);
}

run() {
let results;
let allPassed = true;

try {
results = this.tests.map(this._runTest);
} catch (err) {
if (err instanceof AvaError) {
return this._results();
let pending;
let rejectPending;
let resolvePending;
const allPromises = [];
const handlePromise = promise => {
if (!pending) {
pending = new Promise((resolve, reject) => {
rejectPending = reject;
resolvePending = resolve;
});
}

throw err;
}

const isAsync = results.some(isPromise);

if (isAsync) {
return Promise.all(results)
.catch(AvaError, () => {})
.then(this._results);
}

return this._results();
}
_runTest(test, index) {
const result = test.run();
allPromises.push(promise.then(passed => {
if (!passed) {
allPassed = false;

if (isPromise(result)) {
return result.then(result => this._addResult(result, index));
}
if (this.bail) {
// Stop if the test failed and bail mode is on.
resolvePending();
}
}
}, rejectPending));
};

return this._addResult(result, index);
}
_addResult(result, index) {
// Always save result when not in bail mode or all previous tests pass
if ((this.bail && this.passed) || !this.bail) {
this.results[index] = result;
}
for (const runnable of this.runnables) {
const passedOrPromise = runnable.run();

if (result.passed === false) {
this.passed = false;
if (!passedOrPromise) {
if (this.bail) {
// Stop if the test failed and bail mode is on.
return false;
}

// Only set reason once
if (!this.reason) {
this.reason = result.reason;
allPassed = false;
} else if (passedOrPromise !== true) {
handlePromise(passedOrPromise);
}
}

if (this.bail) {
throw new AvaError('Error in Concurrent while in bail mode');
}
if (pending) {
Promise.all(allPromises).then(resolvePending);
return pending.then(() => allPassed);
}

return result;
}
_results() {
return {
passed: this.passed,
reason: this.reason,
result: this.results
};
return allPassed;
}
}

Expand Down
22 changes: 0 additions & 22 deletions lib/hook.js

This file was deleted.

24 changes: 16 additions & 8 deletions lib/main.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
'use strict';
const process = require('./process-adapter');
const worker = require('./test-worker');
const adapter = require('./process-adapter');
const serializeError = require('./serialize-error');
const globals = require('./globals');
const Runner = require('./runner');
const send = process.send;

const opts = globals.options;
const runner = new Runner({
failWithoutAssertions: opts.failWithoutAssertions,
serial: opts.serial,
bail: opts.failFast,
match: opts.match
});

// Note that test files have require('ava')
require('./test-worker').avaRequired = true;
worker.avaRequired = true;

// If fail-fast is enabled, use this variable to detect
// that no more tests should be logged
Expand All @@ -39,7 +40,7 @@ function test(props) {
props.error = null;
}

send('test', props);
adapter.send('test', props);

if (hasError && opts.failFast) {
isFailed = true;
Expand All @@ -48,28 +49,35 @@ function test(props) {
}

function exit() {
const stats = runner._buildStats();
// Reference the IPC channel now that tests have finished running.
adapter.ipcChannel.ref();

send('results', {stats});
const stats = runner._buildStats();
adapter.send('results', {stats});
}

globals.setImmediate(() => {
const hasExclusive = runner.tests.hasExclusive;
const numberOfTests = runner.tests.testCount;

if (numberOfTests === 0) {
send('no-tests', {avaRequired: true});
adapter.send('no-tests', {avaRequired: true});
return;
}

send('stats', {
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(exit)
.catch(err => {
Expand Down
28 changes: 11 additions & 17 deletions lib/process-adapter.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
'use strict';
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const debug = require('debug')('ava');
const sourceMapSupport = require('source-map-support');
const installPrecompiler = require('require-precompiled');

const debug = require('debug')('ava');

// Check if the test is being run without AVA cli
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');
// Parse and re-emit AVA messages
process.on('message', message => {
if (!message.ava) {
return;
}

process.exit(1); // eslint-disable-line unicorn/no-process-exit
}
process.emit(message.name, message.data);
});

exports.send = (name, data) => {
process.send({
Expand All @@ -27,10 +22,9 @@ exports.send = (name, data) => {
});
};

exports.on = process.on.bind(process);
exports.emit = process.emit.bind(process);
exports.exit = process.exit.bind(process);
exports.env = process.env;
// `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;
Expand Down
14 changes: 5 additions & 9 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ const matcher = require('matcher');
const TestCollection = require('./test-collection');
const validateTest = require('./validate-test');

function noop() {}

const chainableMethods = {
defaults: {
type: 'test',
Expand Down Expand Up @@ -48,9 +46,11 @@ class Runner extends EventEmitter {
options = options || {};

this.results = [];
this.tests = new TestCollection();
this.tests = new TestCollection({
bail: options.bail,
failWithoutAssertions: options.failWithoutAssertions
});
this.hasStarted = false;
this._bail = options.bail;
this._serial = options.serial;
this._match = options.match || [];
this._addTestResult = this._addTestResult.bind(this);
Expand All @@ -74,10 +74,6 @@ class Runner extends EventEmitter {
throw new TypeError(validationError);
}

if (opts.todo) {
fn = noop;
}

this.tests.add({
metadata: opts,
fn,
Expand Down Expand Up @@ -150,7 +146,7 @@ class Runner extends EventEmitter {

this.hasStarted = true;

return Promise.resolve(this.tests.build(this._bail).run()).then(this._buildStats);
return Promise.resolve(this.tests.build().run()).then(this._buildStats);
}
}

Expand Down
Loading