Skip to content
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

Improve runner performance #466

Closed
wants to merge 11 commits into from
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ function exit() {
}

globals.setImmediate(function () {
var numberOfTests = runner.select({type: 'test'}).length;
var numberOfTests = runner.tests.concurrent.length + runner.tests.serial.length;

if (numberOfTests === 0) {
send('no-tests', {avaRequired: true});
Expand Down
89 changes: 89 additions & 0 deletions lib/concurrent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use strict';
var Promise = require('bluebird');
var isPromise = require('is-promise');
var BAIL_ERROR = new Error();

module.exports = Concurrent;

function Concurrent(tests, bail) {
if (!this instanceof Concurrent) {
throw new TypeError('Class constructor Concurrent cannot be invoked without \'new\'');
}
this.tests = tests;
this.bail = bail;
}

Concurrent.prototype.run = function () {
var bail = this.bail;
var tests = this.tests;
var sync = true;
var passed = true;
var reason;

var results = [];

function addAsync(result) {
if (!result.passed) {
if (passed) {
passed = false;
reason = result.reason;
if (bail) {
throw BAIL_ERROR;
}
}
}
return result;
}

for (var i = 0; i < tests.length; i++) {
var result = tests[i].run();
if (isPromise(result)) {
sync = false;
results.push(result.then(addAsync));
} else {
results.push(result);
if (!result.passed) {
if (bail) {
return {
passed: false,
result: results,
reason: result.reason
};
} else if (passed) {
passed = false;
reason = result.reason;
}
}
}
}

if (sync) {
return {
passed: passed,
reason: reason,
result: results
};
}

var ret = Promise.all(results).then(function (results) {
return {
passed: passed,
reason: reason,
result: results
};
});

if (bail) {
ret = ret.catch(function (err) {
if (err !== BAIL_ERROR) {
throw err;
}
return {
passed: false,
reason: reason
};
});
}

return ret;
};
161 changes: 27 additions & 134 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var Promise = require('bluebird');
var objectAssign = require('object-assign');
var Test = require('./test');
var Hook = require('./hook');
var optionChain = require('option-chain');
var TestCollection = require('./test-collection');

var chainableMethods = {
spread: true,
Expand All @@ -29,114 +27,51 @@ var chainableMethods = {
}
};

function noop() {}

function each(items, fn, context) {
return Promise.all(items.map(fn, context));
}

function eachSeries(items, fn, context) {
return Promise.each(items, fn.bind(context));
}

function Runner(opts) {
function Runner() {
if (!(this instanceof Runner)) {
return new Runner(opts);
return new Runner();
}

EventEmitter.call(this);

this.options = opts || {};
this.results = [];
this.tests = [];
this.testsByType = {};
this.hasExclusive = false;
this.tests = new TestCollection();
this._addTestResult = this._addTestResult.bind(this);
}

util.inherits(Runner, EventEmitter);
module.exports = Runner;

optionChain(chainableMethods, function (opts, title, fn) {
var Constructor = (opts && /Each/.test(opts.type)) ? Hook : Test;
var test = new Constructor(title, fn);
test.metadata = objectAssign({}, opts);
this._addTest(test);
}, Runner.prototype);

Runner.prototype._addTest = function (test) {
this.tests.push(test);
var type = test.metadata.type;
var tests = this.testsByType[type] || (this.testsByType[type] = []);
tests.push(test);
if (test.metadata.exclusive) {
this.hasExclusive = true;
}
};

Runner.prototype._runTestWithHooks = function (test) {
if (test.metadata.skipped) {
return this._addTestResult(test);
}

function hookToTest(hook) {
return hook.test(test.title);
}

var tests = (this.testsByType.beforeEach || []).map(hookToTest);
tests.push(test);
tests.push.apply(tests, (this.testsByType.afterEach || []).map(hookToTest));

var context = {};

return eachSeries(tests, function (test) {
Object.defineProperty(test, 'context', {
get: function () {
return context;
},
set: function (val) {
context = val;
}
});

return this._runTest(test);
}, this).catch(noop);
};

Runner.prototype._runTest = function (test) {
var self = this;

// add test result regardless of state
// but on error, don't execute next tests
if (test.metadata.skipped) {
return this._addTestResult(test);
if (typeof title === 'function') {
fn = title;
title = null;
}

return test.run().finally(function () {
self._addTestResult(test);
this.tests.add({
metadata: opts,
fn: fn,
title: title
});
};
}, Runner.prototype);

Runner.prototype._runConcurrent = function (tests) {
if (this.options.serial) {
return this._runSerial(tests);
Runner.prototype._addTestResult = function (result) {
if (result.result.metadata.type === 'test') {
this.stats.testCount++;
if (result.result.metadata.skipped) {
this.stats.skipCount++;
}
}

return each(tests, this._runTestWithHooks, this);
};

Runner.prototype._runSerial = function (tests) {
return eachSeries(tests, this._runTestWithHooks, this);
};

Runner.prototype._addTestResult = function (test) {
if (test.assertError) {
if (!result.passed) {
this.stats.failCount++;
}

var test = result.result;

var props = {
duration: test.duration,
title: test.title,
error: test.assertError,
error: result.reason,
type: test.metadata.type,
skip: test.metadata.skipped
};
Expand All @@ -146,56 +81,14 @@ Runner.prototype._addTestResult = function (test) {
};

Runner.prototype.run = function () {
var self = this;

var serial = [];
var concurrent = [];
var skipCount = 0;

this.testsByType.test.forEach(function (test) {
var metadata = test.metadata;
if (metadata.exclusive === this.hasExclusive) {
(metadata.serial ? serial : concurrent).push(test);
if (metadata.skipped) {
skipCount++;
}
}
}, this);

var stats = this.stats = {
failCount: 0,
passCount: 0,
testCount: serial.length + concurrent.length - skipCount
skipCount: 0,
testCount: 0
};

return eachSeries(this.select({type: 'before'}), this._runTest, this)
.catch(noop)
.then(function () {
if (stats.failCount > 0) {
return Promise.reject();
}
})
.then(function () {
return self._runSerial(serial);
})
.then(function () {
return self._runConcurrent(concurrent);
})
.then(function () {
return eachSeries(self.select({type: 'after'}), self._runTest, self);
})
.catch(noop)
.then(function () {
stats.passCount = stats.testCount - stats.failCount;
});
};

Runner.prototype.select = function (filter) {
var tests = filter.type ? this.testsByType[filter.type] || [] : this.tests;

return tests.filter(function (test) {
return Object.keys(filter).every(function (key) {
return filter[key] === test.metadata[key];
});
return Promise.resolve(this.tests.buildPhases(this._addTestResult).run()).then(function () {
stats.passCount = stats.testCount - stats.failCount - stats.skipCount;
});
};
Loading