Skip to content

[WIP] - Refactor Runner #455

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

Closed
wants to merge 7 commits into from
Closed
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
24 changes: 0 additions & 24 deletions lib/hook.js

This file was deleted.

129 changes: 18 additions & 111 deletions lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
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 Sequence = require('./sequence');

var chainableMethods = {
spread: true,
Expand Down Expand Up @@ -48,74 +47,16 @@ function Runner(opts) {

this.options = opts || {};
this.results = [];
this.tests = [];
this.tests = new TestCollection();
}

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.tests.push(test);
this.tests.add(opts, title, fn);
}, Runner.prototype);

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

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

var tests = this.select({type: 'beforeEach'}).map(hookToTest);
tests.push(test);
tests.push.apply(tests, this.select({type: '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);
}

return test.run().finally(function () {
self._addTestResult(test);
});
};

Runner.prototype._runConcurrent = function (tests) {
if (this.options.serial) {
return this._runSerial(tests);
}

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) {
this.stats.failCount++;
Expand All @@ -136,61 +77,27 @@ 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 skipped = this.select({
type: 'test',
skipped: true
});

var phaseData = this.tests._buildPhases();
var phases = phaseData.phases;
var stats = this.stats = {
failCount: 0,
passCount: 0,
testCount: serial.length + concurrent.length - skipped.length
testCount: phaseData.stats.testCount
};

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;
return eachSeries(phases, function (phase) {
return each(phase, function (tests) {
return new Sequence(tests)
.on('test', function test(test) {
self._addTestResult(test);
})
.run();
});
}).then(function () {
stats.passCount = stats.testCount - stats.failCount;
});
};

Runner.prototype.select = function (filter) {
return this.tests.filter(function (test) {
return Object.keys(filter).every(function (key) {
return filter[key] === test.metadata[key];
});
});
return this.tests.select(filter);
};
35 changes: 35 additions & 0 deletions lib/sequence.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var Promise = require('bluebird');

function Sequence(tests) {
if (!this instanceof Sequence) {
throw new Error('Sequence must be called with new');
}
EventEmitter.call(this);
this.tests = tests;
this.context = {};
}

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

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

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

return test.run().finally(function () {
self.emit('test', test);
});
}).catch(function (e){});
};
168 changes: 168 additions & 0 deletions lib/test-collection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
'use strict';
var assert = require('./assert');
var fnName = require('fn-name');
var Test = require('./test');

module.exports = TestCollection;

function TestCollection() {
if (!(this instanceof TestCollection)) {
throw new Error('TestCollection must be called with new');
}
this.tests = [];
}

TestCollection.prototype.add = function (metadata, title, fn) {
if (typeof title === 'function') {
fn = title;
title = null;
}

assert.is(typeof fn, 'function', 'you must provide a callback');

title = title || fnName(fn) || (metadata.type === 'test' ? '[anonymous]' : metadata.type);

// workaround for Babel giving anonymous functions a name
if (title === 'callee$0$0') {
title = '[anonymous]';
}

var testEntry = {
metadata: metadata,
title: title,
fn: fn,
id: this.tests.length
};

this.tests.push(testEntry);

return testEntry;
};

TestCollection.prototype.serialize = function (tests) {
return (tests || this.tests).map(function (testEntry) {
if (Array.isArray(testEntry)) {
return this.serialize(testEntry);
}
return {
metadata: testEntry.metadata,
title: testEntry.title,
id: testEntry.id
};
}, this);
};

TestCollection.prototype.getEntry = function (entryOrId) {
if (typeof entryOrId === 'number') {
entryOrId = this.tests[entryOrId];
}
return entryOrId;
};

TestCollection.prototype.testsFor = function (testEntry) {
return this.testEntriesFor(testEntry).map(makeTest);
};

TestCollection.prototype.testEntriesFor = function (testEntry) {
testEntry = this.getEntry(testEntry);

if (testEntry.metadata.skipped) {
return [{
id: testEntry.id,
metadata: testEntry.metadata,
title: testEntry.title,
fn: noop
}];
}

var type = testEntry.metadata.type;
assert.is(type, 'test', 'not a valid testEntry');

function hookToTest(hookEntry) {
return {
id: hookEntry.id,
metadata: hookEntry.metadata,
title: hookEntry.title + ' for "' + testEntry.title + '"',
fn: hookEntry.fn
};
}

var tests = this.select({type: 'beforeEach'}).map(hookToTest);
tests.push(testEntry);
tests.push.apply(tests, this.select({type: 'afterEach'}).map(hookToTest));
return tests;
};

TestCollection.prototype._buildPhases = function () {
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 skipped = this.select({
type: 'test',
skipped: true
});

var phases = this.select({type: 'before'}).map(function (testEntry) {
return [[makeTest(testEntry)]];
});
phases.push.apply(phases, serial.map(function (testEntry) {
return [this.testsFor(testEntry)];
}, this));
phases.push(concurrent.map(function (testEntry) {
return this.testsFor(testEntry);
}, this));
phases.push.apply(phases, this.select({type: 'after'}).map(function (testEntry) {
return [[makeTest(testEntry)]];
}));

return {
stats: {
serial: serial.length,
concurrent: concurrent.length,
skipped: skipped.length,
testCount: serial.length + concurrent.length - skipped.length
},
phases: phases
};
};

TestCollection.prototype.buildPhases = function () {
return this._buildPhases().phases;
};

TestCollection.prototype.select = function (filter) {
return this.tests.filter(function (test) {
return Object.keys(filter).every(function (key) {
return filter[key] === test.metadata[key];
});
});
};

function makeTest(testEntry) {
var test = new Test(
testEntry.title,
testEntry.metadata.skipped ? noop : testEntry.fn
);
test.metadata = testEntry.metadata;
test.id = testEntry.id;
return test;
}

function noop() {}

TestCollection.makeTest = makeTest;
Loading