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

Performance Improvements for files with thousands of tests #461

Merged
merged 9 commits into from
Jan 21, 2016
3 changes: 2 additions & 1 deletion lib/enhance-assert.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ function enhanceAssert(opts) {
onError: opts.onError,
onSuccess: opts.onSuccess,
patterns: module.exports.PATTERNS,
wrapOnlyPatterns: module.exports.NON_ENHANCED_PATTERNS
wrapOnlyPatterns: module.exports.NON_ENHANCED_PATTERNS,
bindReceiver: false
}
);

Expand Down
57 changes: 31 additions & 26 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ function Runner(opts) {
this.options = opts || {};
this.results = [];
this.tests = [];
this.testsByType = {};
this.hasExclusive = false;
}

util.inherits(Runner, EventEmitter);
Expand All @@ -58,9 +60,19 @@ 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.tests.push(test);
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);
Expand All @@ -70,9 +82,9 @@ Runner.prototype._runTestWithHooks = function (test) {
return hook.test(test.title);
}

var tests = this.select({type: 'beforeEach'}).map(hookToTest);
var tests = (this.testsByType.beforeEach || []).map(hookToTest);
tests.push(test);
tests.push.apply(tests, this.select({type: 'afterEach'}).map(hookToTest));
tests.push.apply(tests, (this.testsByType.afterEach || []).map(hookToTest));

var context = {};

Expand Down Expand Up @@ -136,33 +148,24 @@ Runner.prototype._addTestResult = function (test) {
Runner.prototype.run = function () {
var self = this;

var hasExclusive = this.select({
exclusive: true,
skipped: false,
type: 'test'
}).length > 0;

var serial = this.select({
exclusive: hasExclusive,
serial: true,
type: 'test'
});

var concurrent = this.select({
exclusive: hasExclusive,
serial: false,
type: 'test'
});
var serial = [];
var concurrent = [];
var skipCount = 0;

var skipped = this.select({
type: 'test',
skipped: true
});
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 - skipped.length
testCount: serial.length + concurrent.length - skipCount
};

return eachSeries(this.select({type: 'before'}), this._runTest, this)
Expand All @@ -188,7 +191,9 @@ Runner.prototype.run = function () {
};

Runner.prototype.select = function (filter) {
return this.tests.filter(function (test) {
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];
});
Expand Down
182 changes: 91 additions & 91 deletions lib/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,6 @@ function Test(title, fn) {
this.duration = null;
this.assertError = undefined;

Object.defineProperty(this, 'assertCount', {
enumerable: true,
get: function () {
return this.assertions.length;
}
});

// TODO(jamestalmage): make this an optional constructor arg instead of having Runner set it after the fact.
// metadata should just always exist, otherwise it requires a bunch of ugly checks all over the place.
this.metadata = {};
Expand All @@ -53,6 +46,13 @@ function Test(title, fn) {

module.exports = Test;

Object.defineProperty(Test.prototype, 'assertCount', {
enumerable: true,
get: function () {
return this.assertions.length;
}
});

Test.prototype._assert = function (promise) {
this.assertions.push(promise);
};
Expand Down Expand Up @@ -81,6 +81,17 @@ Test.prototype.plan = function (count) {
this.planStack = new Error().stack;
};

Test.prototype._run = function () {
var ret;
try {
ret = this.fn(this._publicApi());
} catch (err) {
this._setAssertError(err);
this.exit();
}
return ret;
};

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

Expand All @@ -90,25 +101,12 @@ Test.prototype.run = function () {
self.promise.reject = reject;
});

// TODO(vdemedes): refactor this to avoid storing the promise
if (!this.fn) {
this.exit();
return undefined;
}

this._timeStart = globals.now();

// wait until all assertions are complete
this._timeout = globals.setTimeout(function () {}, maxTimeout);

var ret;

try {
ret = this.fn(this._publicApi());
} catch (err) {
this._setAssertError(err);
this.exit();
}
var ret = this._run();

var asyncType = 'promises';

Expand Down Expand Up @@ -207,87 +205,89 @@ Test.prototype.exit = function () {

self._checkPlanCount();

if (!self.ended) {
self.ended = true;

globals.setImmediate(function () {
if (self.assertError !== undefined) {
self.promise.reject(self.assertError);
return;
}

self.promise.resolve(self);
});
if (self.assertError !== undefined) {
self.promise.reject(self.assertError);
return;
}

self.promise.resolve(self);
});
};

Test.prototype._publicApi = function () {
var self = this;
var api = {skip: {}};

// Getters
[
'assertCount',
'title',
'end'
]
.forEach(function (name) {
Object.defineProperty(api, name, {
enumerable: name === 'end' ? self.metadata.callback : true,
get: function () {
return self[name];
}
});
});
return new PublicApi(this);
};

function PublicApi(test) {
this._test = test;
this.plan = test.plan.bind(test);
this.skip = new SkipApi(test);
}

// Get / Set
Object.defineProperty(api, 'context', {
enumerable: true,
get: function () {
return self.context;
},
set: function (context) {
self.context = context;
function onAssertionEvent(event) {
var promise;
if (event.assertionThrew) {
event.error.powerAssertContext = event.powerAssertContext;
event.error.originalMessage = event.originalMessage;
promise = Promise.reject(event.error);
} else {
var ret = event.returnValue;
if (isObservable(ret)) {
ret = observableToPromise(ret);
}
});
if (isPromise(ret)) {
ret = ret
.then(null, function (err) {
err.originalMessage = event.originalMessage;
throw err;
});
}
promise = ret;
}
this._test._assert(promise);
return promise;
}

// Bound Functions
api.plan = this.plan.bind(this);
PublicApi.prototype = enhanceAssert({
assert: assert,
onSuccess: onAssertionEvent,
onError: onAssertionEvent
});

function skipFn() {
self._assert(Promise.resolve());
}
// Getters
[
'assertCount',
'title',
'end'
]
.forEach(function (name) {
Object.defineProperty(PublicApi.prototype, name, {
enumerable: false,
get: function () {
return this._test[name];
}
});
});

function onAssertionEvent(event) {
var promise;
if (event.assertionThrew) {
event.error.powerAssertContext = event.powerAssertContext;
promise = Promise.reject(event.error);
} else {
var ret = event.returnValue;
promise = isObservable(ret) ? observableToPromise(ret) : Promise.resolve(ret);
}
promise = promise
.catch(function (err) {
err.originalMessage = event.originalMessage;
return Promise.reject(err);
});
self._assert(promise);
return promise;
// Get / Set
Object.defineProperty(PublicApi.prototype, 'context', {
enumerable: true,
get: function () {
return this._test.context;
},
set: function (context) {
this._test.context = context;
}
});

var enhanced = enhanceAssert({
assert: assert,
onSuccess: onAssertionEvent,
onError: onAssertionEvent
});
function skipFn() {
return this._test._assert(Promise.resolve());
}

// Patched assert methods: increase assert count and store errors.
Object.keys(assert).forEach(function (el) {
api.skip[el] = skipFn;
api[el] = enhanced[el].bind(enhanced);
});
function SkipApi(test) {
this._test = test;
}

return api;
};
Object.keys(assert).forEach(function (el) {
SkipApi.prototype[el] = skipFn;
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"core-assert": "^0.1.0",
"debug": "^2.2.0",
"deeper": "^2.1.0",
"empower-core": "^0.4.0",
"empower-core": "^0.5.0",
"figures": "^1.4.0",
"find-cache-dir": "^0.1.1",
"fn-name": "^2.0.0",
Expand Down
2 changes: 2 additions & 0 deletions test/fork.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ test('fake timers do not break duration', function (t) {
});
});

/*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this change, destructuring would no longer work. This is not allowed anymore:

test({is, same} => {
  is(1 + 1, 2);
  same([1,2].slice(1), [2]);
});

You have to preserve the this reference:

test(t => {
  t.is(1 + 1, 2);
  t.same([1,2].slice(1), [2]);
});

test('destructuring of `t` is allowed', function (t) {
fork(fixture('destructuring-public-api.js'))
.run()
Expand All @@ -81,6 +82,7 @@ test('destructuring of `t` is allowed', function (t) {
t.end();
});
});
*/

test('babelrc is ignored', function (t) {
fork(fixture('babelrc/test.js'))
Expand Down
7 changes: 1 addition & 6 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -472,15 +472,10 @@ test('number of assertions doesn\'t match plan when the test exits, but before a
});

test('assertions return promises', function (t) {
t.plan(4);
ava(function (a) {
a.plan(4);
a.plan(2);
t.ok(isPromise(a.throws(Promise.reject(new Error('foo')))));
t.ok(isPromise(a.throws(function () {
throw new Error('bar');
})));
t.ok(isPromise(a.doesNotThrow(Promise.resolve(true))));
t.ok(isPromise(a.true(true)));
}).run().then(function () {
t.end();
});
Expand Down