From 27b50594d7c1f729fec9d30d3af108f28040f082 Mon Sep 17 00:00:00 2001 From: James Talmage Date: Tue, 19 Jan 2016 00:42:33 -0500 Subject: [PATCH 1/7] WIP - add a test-collection class --- lib/test-collection.js | 90 +++++++++++++++++++++++++++++++++++++++++ test/test-collection.js | 83 +++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 lib/test-collection.js create mode 100644 test/test-collection.js diff --git a/lib/test-collection.js b/lib/test-collection.js new file mode 100644 index 000000000..bf2944f07 --- /dev/null +++ b/lib/test-collection.js @@ -0,0 +1,90 @@ +'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); + + 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) { + return { + metadata: testEntry.metadata, + title: testEntry.title, + id: testEntry.id + }; + }); +}; + +TestCollection.prototype.getEntry = function (entryOrId) { + if (typeof entryOrId === 'number') { + entryOrId = this.tests[entryOrId]; + } + return entryOrId; +}; + +TestCollection.prototype.testsFor = function (testEntry) { + testEntry = this.getEntry(testEntry); + var type = testEntry.metadata.type; + if (type === 'before' || type === 'after') { + return [makeTest(testEntry.title, testEntry)]; + } + assert.is(type, 'test', 'not a valid testEntry'); + + function hookToTest(hookEntry) { + return makeTest(hookEntry.title + ' for "' + testEntry.title + '"', hookEntry); + } + + var tests = this.select({type: 'beforeEach'}).map(hookToTest); + tests.push(makeTest(testEntry.title, testEntry)); + tests.push.apply(tests, this.select({type: 'afterEach'}).map(hookToTest)); + return tests; +}; + +TestCollection.prototype.makeTest = function (testEntry) { + testEntry = this.getEntry(testEntry); + return makeTest(testEntry.title, testEntry); +}; + +function makeTest(title, testEntry) { + var test = new Test(title, testEntry.fn); + test.metadata = testEntry.metadata; + test.id = testEntry.id; + return test; +} + +TestCollection.prototype.select = function (filter) { + return this.tests.filter(function (test) { + return Object.keys(filter).every(function (key) { + return filter[key] === test.metadata[key]; + }); + }); +}; diff --git a/test/test-collection.js b/test/test-collection.js new file mode 100644 index 000000000..ef1df6140 --- /dev/null +++ b/test/test-collection.js @@ -0,0 +1,83 @@ +var test = require('tap').test; +var TestCollection = require('../lib/test-collection'); +var Test = require('../lib/test'); + +test('requires new', function (t) { + var withoutNew = TestCollection; + t.throws(function () { + withoutNew(); + }); + t.end(); +}); + +test('add throws if no callback is supplied', function (t) { + var collection = new TestCollection(); + t.throws(function () { + collection.add({type: 'test'}, 'someTitle'); + }, {message: 'you must provide a callback'}); + t.end(); +}); + +test('add will set the title', function (t) { + var collection = new TestCollection(); + collection.add({type: 'test'}, 'foo', function () {}); + t.deepEqual(collection.serialize(), [ + {id: 0, title: 'foo', metadata: {type: 'test'}} + ]); + t.end(); +}); + +test('add will infer the title from the function name', function (t) { + var collection = new TestCollection(); + collection.add({type: 'test'}, function bar() {}); + t.deepEqual(collection.serialize(), [ + {id: 0, title: 'bar', metadata: {type: 'test'}} + ]); + t.end(); +}); + +test('add will set title to "[anonymous]" if it is type:test', function (t) { + var collection = new TestCollection(); + collection.add({type: 'test'}, function () {}); + t.deepEqual(collection.serialize(), [ + {id: 0, title: '[anonymous]', metadata: {type: 'test'}} + ]); + t.end(); +}); + +test('add will set the title===type for other types', function (t) { + var collection = new TestCollection(); + collection.add({type: 'after'}, function () {}); + collection.add({type: 'before'}, function () {}); + t.deepEqual(collection.serialize(), [ + {id: 0, title: 'after', metadata: {type: 'after'}}, + {id: 1, title: 'before', metadata: {type: 'before'}} + ]); + t.end(); +}); + +test('testFor creates a list of tests', function (t) { + var collection = new TestCollection(); + collection.add({type: 'beforeEach'}, function () {}); + collection.add({type: 'afterEach'}, function () {}); + collection.add({type: 'test'}, function foo() {}); + collection.add({type: 'test'}, function bar() {}); + + t.deepEqual(collection.serialize(collection.testsFor(2)), [ + {id: 0, title: 'beforeEach for "foo"', metadata: {type: 'beforeEach'}}, + {id: 2, title: 'foo', metadata: {type: 'test'}}, + {id: 1, title: 'afterEach for "foo"', metadata: {type: 'afterEach'}} + ]); + + t.deepEqual(collection.serialize(collection.testsFor(3)), [ + {id: 0, title: 'beforeEach for "bar"', metadata: {type: 'beforeEach'}}, + {id: 3, title: 'bar', metadata: {type: 'test'}}, + {id: 1, title: 'afterEach for "bar"', metadata: {type: 'afterEach'}} + ]); + + collection.testsFor(2).concat(collection.testsFor(3)).forEach(function (test) { + t.ok(test instanceof Test); + }); + + t.end(); +}); From 5a686eab45ae736c194684630461bb3efaca281f Mon Sep 17 00:00:00 2001 From: James Talmage Date: Tue, 19 Jan 2016 01:35:26 -0500 Subject: [PATCH 2/7] incorporate test-collection into runner --- lib/runner.js | 34 +++-------- lib/test-collection.js | 19 +++--- test/fixture/hooks-failing.js | 10 +-- test/runner.js | 112 ++++++++++++++++------------------ 4 files changed, 73 insertions(+), 102 deletions(-) diff --git a/lib/runner.js b/lib/runner.js index 39599a8d7..4628ec65d 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -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, @@ -48,17 +46,14 @@ 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) { @@ -66,17 +61,9 @@ Runner.prototype._runTestWithHooks = function (test) { 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) { + return eachSeries(this.tests.testsFor(test), function (test) { Object.defineProperty(test, 'context', { get: function () { return context; @@ -165,7 +152,7 @@ Runner.prototype.run = function () { testCount: serial.length + concurrent.length - skipped.length }; - return eachSeries(this.select({type: 'before'}), this._runTest, this) + return eachSeries(this.select({type: 'before'}, true), this._runTest, this) .catch(noop) .then(function () { if (stats.failCount > 0) { @@ -179,7 +166,7 @@ Runner.prototype.run = function () { return self._runConcurrent(concurrent); }) .then(function () { - return eachSeries(self.select({type: 'after'}), self._runTest, self); + return eachSeries(self.select({type: 'after'}, true), self._runTest, self); }) .catch(noop) .then(function () { @@ -187,10 +174,7 @@ Runner.prototype.run = function () { }); }; -Runner.prototype.select = function (filter) { - return this.tests.filter(function (test) { - return Object.keys(filter).every(function (key) { - return filter[key] === test.metadata[key]; - }); - }); +Runner.prototype.select = function (filter, create) { + var entries = this.tests.select(filter); + return create ? entries.map(TestCollection.makeTest) : entries; }; diff --git a/lib/test-collection.js b/lib/test-collection.js index bf2944f07..3ea326e40 100644 --- a/lib/test-collection.js +++ b/lib/test-collection.js @@ -54,9 +54,11 @@ TestCollection.prototype.getEntry = function (entryOrId) { TestCollection.prototype.testsFor = function (testEntry) { testEntry = this.getEntry(testEntry); var type = testEntry.metadata.type; + /* if (type === 'before' || type === 'after') { return [makeTest(testEntry.title, testEntry)]; } + */ assert.is(type, 'test', 'not a valid testEntry'); function hookToTest(hookEntry) { @@ -69,9 +71,12 @@ TestCollection.prototype.testsFor = function (testEntry) { return tests; }; -TestCollection.prototype.makeTest = function (testEntry) { - testEntry = this.getEntry(testEntry); - return makeTest(testEntry.title, testEntry); +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(title, testEntry) { @@ -81,10 +86,6 @@ function makeTest(title, testEntry) { return test; } -TestCollection.prototype.select = function (filter) { - return this.tests.filter(function (test) { - return Object.keys(filter).every(function (key) { - return filter[key] === test.metadata[key]; - }); - }); +TestCollection.makeTest = function (entry) { + return makeTest(entry.title, entry); }; diff --git a/test/fixture/hooks-failing.js b/test/fixture/hooks-failing.js index ece3b0f01..0452820f9 100644 --- a/test/fixture/hooks-failing.js +++ b/test/fixture/hooks-failing.js @@ -1,10 +1,4 @@ import test from '../../'; -test.beforeEach(fail); -test(pass); - -function pass() {} - -function fail(t) { - t.fail(); -} +test.beforeEach(t => t.fail()); +test('pass', t => t.pass()); diff --git a/test/runner.js b/test/runner.js index cd9f6aebb..31d9e10b8 100644 --- a/test/runner.js +++ b/test/runner.js @@ -1,7 +1,6 @@ 'use strict'; var test = require('tap').test; var runner = require('../lib/runner'); -var Test = require('../lib/test'); var Runner = runner; var mockTitle = 'mock title'; var noop = function () {}; @@ -14,121 +13,114 @@ test('returns new instance of runner without "new"', function (t) { test('runner.test adds a new test', function (t) { var runner = new Runner(); runner.test(mockTitle, noop); - t.is(runner.tests.length, 1); - t.true(runner.tests[0] instanceof Test); - t.false(runner.tests[0].metadata.serial); + t.is(runner.tests.tests.length, 1); + t.false(runner.tests.tests[0].metadata.serial); t.end(); }); test('runner.serial adds a new serial test', function (t) { var runner = new Runner(); runner.serial(mockTitle, noop); - t.is(runner.tests.length, 1); - t.true(runner.tests[0] instanceof Test); - t.true(runner.tests[0].metadata.serial); + t.is(runner.tests.tests.length, 1); + t.true(runner.tests.tests[0].metadata.serial); t.end(); }); test('runner.before adds a new before hook', function (t) { var runner = new Runner(); runner.before(mockTitle, noop); - t.is(runner.tests.length, 1); - t.true(runner.tests[0] instanceof Test); - t.is(runner.tests[0].metadata.type, 'before'); + t.is(runner.tests.tests.length, 1); + t.is(runner.tests.tests[0].metadata.type, 'before'); t.end(); }); test('runner.after adds a new after hook', function (t) { var runner = new Runner(); runner.after(mockTitle, noop); - t.is(runner.tests.length, 1); - t.true(runner.tests[0] instanceof Test); - t.is(runner.tests[0].metadata.type, 'after'); + t.is(runner.tests.tests.length, 1); + t.is(runner.tests.tests[0].metadata.type, 'after'); t.end(); }); test('runner.beforeEach adds a new beforeEach hook', function (t) { var runner = new Runner(); runner.beforeEach(mockTitle, noop); - t.is(runner.tests.length, 1); - t.is(runner.tests[0].title, mockTitle); - t.is(runner.tests[0].fn, noop); - t.is(runner.tests[0].metadata.type, 'beforeEach'); + t.is(runner.tests.tests.length, 1); + t.is(runner.tests.tests[0].title, mockTitle); + t.is(runner.tests.tests[0].fn, noop); + t.is(runner.tests.tests[0].metadata.type, 'beforeEach'); t.end(); }); test('runner.beforeEach title is optional', function (t) { - function doThisFirst() {} + var doThisFirst = function () {}; var runner = new Runner(); runner.beforeEach(doThisFirst); - t.is(runner.tests.length, 1); - // TODO(jamestalmage): Make `title` logic common between Hook and Test - t.is(runner.tests[0].title, null); - t.is(runner.tests[0].fn, doThisFirst); - t.is(runner.tests[0].metadata.type, 'beforeEach'); + t.is(runner.tests.tests.length, 1); + t.is(runner.tests.tests[0].title, 'beforeEach'); + t.is(runner.tests.tests[0].fn, doThisFirst); + t.is(runner.tests.tests[0].metadata.type, 'beforeEach'); t.end(); }); test('runner.afterEach adds a new afterEach hook', function (t) { var runner = new Runner(); runner.afterEach(mockTitle, noop); - t.is(runner.tests.length, 1); - t.is(runner.tests[0].title, mockTitle); - t.is(runner.tests[0].fn, noop); - t.is(runner.tests[0].metadata.type, 'afterEach'); + t.is(runner.tests.tests.length, 1); + t.is(runner.tests.tests[0].title, mockTitle); + t.is(runner.tests.tests[0].fn, noop); + t.is(runner.tests.tests[0].metadata.type, 'afterEach'); t.end(); }); test('runner.skip adds a new skipped test', function (t) { var runner = new Runner(); runner.skip(mockTitle, noop); - t.is(runner.tests.length, 1); - t.true(runner.tests[0] instanceof Test); - t.is(runner.tests[0].title, mockTitle); - t.is(runner.tests[0].metadata.skipped, true); + t.is(runner.tests.tests.length, 1); + t.is(runner.tests.tests[0].title, mockTitle); + t.is(runner.tests.tests[0].metadata.skipped, true); t.end(); }); test('runner.skip - title is optional', function (t) { var runner = new Runner(); runner.skip(noop); - t.is(runner.tests.length, 1); - t.true(runner.tests[0] instanceof Test); - t.is(runner.tests[0].title, '[anonymous]'); - t.is(runner.tests[0].metadata.skipped, true); + t.is(runner.tests.tests.length, 1); + t.is(runner.tests.tests[0].title, '[anonymous]'); + t.is(runner.tests.tests[0].metadata.skipped, true); t.end(); }); test('methods are chainable: serial.skip', function (t) { var runner = new Runner(); runner.serial.skip(noop); - t.is(runner.tests.length, 1); - t.is(runner.tests[0].metadata.type, 'test'); - t.true(runner.tests[0].metadata.serial); - t.false(runner.tests[0].metadata.exclusive); - t.true(runner.tests[0].metadata.skipped); + t.is(runner.tests.tests.length, 1); + t.is(runner.tests.tests[0].metadata.type, 'test'); + t.true(runner.tests.tests[0].metadata.serial); + t.false(runner.tests.tests[0].metadata.exclusive); + t.true(runner.tests.tests[0].metadata.skipped); t.end(); }); test('methods are chainable: beforeEach.skip', function (t) { var runner = new Runner(); runner.beforeEach.skip(noop); - t.is(runner.tests.length, 1); - t.is(runner.tests[0].metadata.type, 'beforeEach'); - t.false(runner.tests[0].metadata.serial); - t.false(runner.tests[0].metadata.exclusive); - t.true(runner.tests[0].metadata.skipped); + t.is(runner.tests.tests.length, 1); + t.is(runner.tests.tests[0].metadata.type, 'beforeEach'); + t.false(runner.tests.tests[0].metadata.serial); + t.false(runner.tests.tests[0].metadata.exclusive); + t.true(runner.tests.tests[0].metadata.skipped); t.end(); }); test('methods are chainable: serial.only', function (t) { var runner = new Runner(); runner.serial.only(noop); - t.is(runner.tests.length, 1); - t.is(runner.tests[0].metadata.type, 'test'); - t.true(runner.tests[0].metadata.serial); - t.true(runner.tests[0].metadata.exclusive); - t.false(runner.tests[0].metadata.skipped); + t.is(runner.tests.tests.length, 1); + t.is(runner.tests.tests[0].metadata.type, 'test'); + t.true(runner.tests.tests[0].metadata.serial); + t.true(runner.tests.tests[0].metadata.exclusive); + t.false(runner.tests.tests[0].metadata.skipped); t.end(); }); @@ -244,11 +236,11 @@ test('include skipped tests in results', function (t) { t.same(titles, [ 'before', 'before.skip', - 'beforeEach', - 'beforeEach.skip', + 'beforeEach for "test"', + 'beforeEach.skip for "test"', 'test', - 'afterEach', - 'afterEach.skip', + 'afterEach for "test"', + 'afterEach.skip for "test"', 'test.skip', 'after', 'after.skip' @@ -261,6 +253,10 @@ test('include skipped tests in results', function (t) { test('test types and titles', function (t) { t.plan(10); + var pass = function (a) { + a.pass(); + }; + var runner = new Runner(); runner.before(pass); runner.beforeEach(pass); @@ -268,16 +264,12 @@ test('test types and titles', function (t) { runner.afterEach(pass); runner.test('test', pass); - function pass(a) { - a.pass(); - } - var tests = [ - {type: 'before', title: 'pass'}, + {type: 'before', title: 'before'}, {type: 'beforeEach', title: 'beforeEach for "test"'}, {type: 'test', title: 'test'}, {type: 'afterEach', title: 'afterEach for "test"'}, - {type: 'after', title: 'pass'} + {type: 'after', title: 'after'} ]; runner.on('test', function (props) { From 39de3246a185f72238bd4505dfa90e09a959ae4b Mon Sep 17 00:00:00 2001 From: James Talmage Date: Tue, 19 Jan 2016 01:37:55 -0500 Subject: [PATCH 3/7] cleanup --- lib/test-collection.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/test-collection.js b/lib/test-collection.js index 3ea326e40..84605a010 100644 --- a/lib/test-collection.js +++ b/lib/test-collection.js @@ -54,11 +54,6 @@ TestCollection.prototype.getEntry = function (entryOrId) { TestCollection.prototype.testsFor = function (testEntry) { testEntry = this.getEntry(testEntry); var type = testEntry.metadata.type; - /* - if (type === 'before' || type === 'after') { - return [makeTest(testEntry.title, testEntry)]; - } - */ assert.is(type, 'test', 'not a valid testEntry'); function hookToTest(hookEntry) { From e5b6ccaad2b88f833bc6635409fc362643de8ab6 Mon Sep 17 00:00:00 2001 From: James Talmage Date: Tue, 19 Jan 2016 02:00:57 -0500 Subject: [PATCH 4/7] remove redundant code / tests --- lib/hook.js | 24 ------------------------ lib/test-collection.js | 5 +++++ lib/test.js | 8 +------- test/test.js | 22 ++-------------------- 4 files changed, 8 insertions(+), 51 deletions(-) delete mode 100644 lib/hook.js diff --git a/lib/hook.js b/lib/hook.js deleted file mode 100644 index ed22a4705..000000000 --- a/lib/hook.js +++ /dev/null @@ -1,24 +0,0 @@ -var Test = require('./test'); - -module.exports = Hook; - -function Hook(title, fn) { - if (!(this instanceof Hook)) { - return new Hook(title, fn); - } - - if (typeof title === 'function') { - fn = title; - title = null; - } - - this.title = title; - this.fn = fn; -} - -Hook.prototype.test = function (testTitle) { - var title = this.title || (this.metadata.type + ' for "' + testTitle + '"'); - var test = new Test(title, this.fn); - test.metadata = this.metadata; - return test; -}; diff --git a/lib/test-collection.js b/lib/test-collection.js index 84605a010..4d8c8972c 100644 --- a/lib/test-collection.js +++ b/lib/test-collection.js @@ -22,6 +22,11 @@ TestCollection.prototype.add = function (metadata, title, fn) { 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, diff --git a/lib/test.js b/lib/test.js index 815891069..987d4ceb8 100644 --- a/lib/test.js +++ b/lib/test.js @@ -2,7 +2,6 @@ var isGeneratorFn = require('is-generator-fn'); var maxTimeout = require('max-timeout'); var Promise = require('bluebird'); -var fnName = require('fn-name'); var co = require('co-with-promise'); var observableToPromise = require('observable-to-promise'); var isPromise = require('is-promise'); @@ -23,7 +22,7 @@ function Test(title, fn) { assert.is(typeof fn, 'function', 'you must provide a callback'); - this.title = title || fnName(fn) || '[anonymous]'; + this.title = title; this.fn = isGeneratorFn(fn) ? co.wrap(fn) : fn; this.assertions = []; this.planCount = null; @@ -44,11 +43,6 @@ function Test(title, fn) { // store the time point before test execution // to calculate the total time spent in test this._timeStart = null; - - // workaround for Babel giving anonymous functions a name - if (this.title === 'callee$0$0') { - this.title = '[anonymous]'; - } } module.exports = Test; diff --git a/test/test.js b/test/test.js index e453997e3..ae6012299 100644 --- a/test/test.js +++ b/test/test.js @@ -27,36 +27,18 @@ test('run test', function (t) { }); }); -test('title is optional', function (t) { - ava(function (a) { - a.pass(); - }).run().then(function (a) { - t.is(a.title, '[anonymous]'); - t.end(); - }); -}); - test('callback is required', function (t) { t.throws(function () { ava(); - }, /you must provide a callback/); + }, {message: /you must provide a callback/}); t.throws(function () { ava('title'); - }, /you must provide a callback/); + }, {message: /you must provide a callback/}); t.end(); }); -test('infer name from function', function (t) { - ava(function foo(a) { - a.pass(); - }).run().then(function (a) { - t.is(a.title, 'foo'); - t.end(); - }); -}); - test('multiple asserts', function (t) { ava(function (a) { a.pass(); From 43f958d34fc793769e088b937def24e881d83dbe Mon Sep 17 00:00:00 2001 From: James Talmage Date: Tue, 19 Jan 2016 03:04:44 -0500 Subject: [PATCH 5/7] remove unnecessary code - why was this ever here? --- lib/runner.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/runner.js b/lib/runner.js index 4628ec65d..912d84580 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -153,12 +153,6 @@ Runner.prototype.run = function () { }; return eachSeries(this.select({type: 'before'}, true), this._runTest, this) - .catch(noop) - .then(function () { - if (stats.failCount > 0) { - return Promise.reject(); - } - }) .then(function () { return self._runSerial(serial); }) From 83d11855983738a6549239d07eb63cf5c97cc7be Mon Sep 17 00:00:00 2001 From: James Talmage Date: Tue, 19 Jan 2016 06:03:38 -0500 Subject: [PATCH 6/7] buildPhases --- lib/test-collection.js | 62 +++++++++++++++++++++++++++++++++++------ test/test-collection.js | 53 +++++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 8 deletions(-) diff --git a/lib/test-collection.js b/lib/test-collection.js index 4d8c8972c..f684ea578 100644 --- a/lib/test-collection.js +++ b/lib/test-collection.js @@ -41,12 +41,15 @@ TestCollection.prototype.add = function (metadata, title, fn) { 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) { @@ -57,20 +60,65 @@ TestCollection.prototype.getEntry = function (entryOrId) { }; TestCollection.prototype.testsFor = function (testEntry) { + return this.testEntriesFor(testEntry).map(makeTest); +}; + +TestCollection.prototype.testEntriesFor = function (testEntry) { testEntry = this.getEntry(testEntry); + var type = testEntry.metadata.type; assert.is(type, 'test', 'not a valid testEntry'); function hookToTest(hookEntry) { - return makeTest(hookEntry.title + ' for "' + testEntry.title + '"', 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(makeTest(testEntry.title, testEntry)); + 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 ret = this.select({type: 'before'}).map(function (testEntry) { + return [[makeTest(testEntry)]]; + }); + ret.push.apply(ret, serial.map(function (testEntry) { + return [this.testsFor(testEntry)]; + }, this)); + ret.push(concurrent.map(function (testEntry) { + return this.testsFor(testEntry); + }, this)); + ret.push.apply(ret, this.select({type: 'after'}).map(function (testEntry) { + return [[makeTest(testEntry)]]; + })); + + return ret; +}; + TestCollection.prototype.select = function (filter) { return this.tests.filter(function (test) { return Object.keys(filter).every(function (key) { @@ -79,13 +127,11 @@ TestCollection.prototype.select = function (filter) { }); }; -function makeTest(title, testEntry) { - var test = new Test(title, testEntry.fn); +function makeTest(testEntry) { + var test = new Test(testEntry.title, testEntry.fn); test.metadata = testEntry.metadata; test.id = testEntry.id; return test; } -TestCollection.makeTest = function (entry) { - return makeTest(entry.title, entry); -}; +TestCollection.makeTest = makeTest; diff --git a/test/test-collection.js b/test/test-collection.js index ef1df6140..477d81261 100644 --- a/test/test-collection.js +++ b/test/test-collection.js @@ -1,6 +1,21 @@ var test = require('tap').test; var TestCollection = require('../lib/test-collection'); var Test = require('../lib/test'); +var objectAssign = require('object-assign'); + +function defaults() { + return { + type: 'test', + serial: false, + exclusive: false, + skipped: false, + callback: false + }; +} + +function metadata(opts) { + return objectAssign(defaults(), opts); +} test('requires new', function (t) { var withoutNew = TestCollection; @@ -81,3 +96,41 @@ test('testFor creates a list of tests', function (t) { t.end(); }); + +test('buildPhases', function (t) { + var collection = new TestCollection(); + + collection.add(metadata({type: 'before'}), function before1() {}); + collection.add(metadata({type: 'before'}), function before2() {}); + collection.add(metadata({type: 'beforeEach'}), function () {}); + collection.add(metadata({type: 'afterEach'}), function () {}); + collection.add(metadata({type: 'after'}), function after1() {}); + collection.add(metadata({type: 'after'}), function after2() {}); + collection.add(metadata({type: 'test'}), function foo() {}); + collection.add(metadata({type: 'test'}), function bar() {}); + + t.deepEqual(serialize(collection.buildPhases()), [ + [['before1']], + [['before2']], + [ + ['foo.beforeEach', 'foo', 'foo.afterEach'], + ['bar.beforeEach', 'bar', 'bar.afterEach'] + ], + [['after1']], + [['after2']] + ]); + t.end(); +}); + +function serialize(phase) { + return phase.map(function (testEntry) { + if (Array.isArray(testEntry)) { + return serialize(testEntry); + } + var match = /^(.+?) for "(.+?)"$/.exec(testEntry.title); + if (match) { + return match[2] + '.' + match[1]; + } + return testEntry.title; + }); +} From cba55e026b721dabdb21bb9a36039536d68fbf57 Mon Sep 17 00:00:00 2001 From: James Talmage Date: Tue, 19 Jan 2016 23:17:19 -0500 Subject: [PATCH 7/7] WIP - closer - a couple failing tests related to bailing out when `before` tests fail --- lib/runner.js | 103 +++++++--------------------------------- lib/sequence.js | 35 ++++++++++++++ lib/test-collection.js | 45 +++++++++++++++--- test/sequence.js | 28 +++++++++++ test/test-collection.js | 8 +++- 5 files changed, 124 insertions(+), 95 deletions(-) create mode 100644 lib/sequence.js create mode 100644 test/sequence.js diff --git a/lib/runner.js b/lib/runner.js index 912d84580..d3d23bd5d 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -4,6 +4,7 @@ var util = require('util'); var Promise = require('bluebird'); var optionChain = require('option-chain'); var TestCollection = require('./test-collection'); +var Sequence = require('./sequence'); var chainableMethods = { spread: true, @@ -56,53 +57,6 @@ optionChain(chainableMethods, function (opts, title, fn) { this.tests.add(opts, title, fn); }, Runner.prototype); -Runner.prototype._runTestWithHooks = function (test) { - if (test.metadata.skipped) { - return this._addTestResult(test); - } - - var context = {}; - - return eachSeries(this.tests.testsFor(test), 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++; @@ -123,52 +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'}, true), this._runTest, this) - .then(function () { - return self._runSerial(serial); - }) - .then(function () { - return self._runConcurrent(concurrent); - }) - .then(function () { - return eachSeries(self.select({type: 'after'}, true), 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, create) { - var entries = this.tests.select(filter); - return create ? entries.map(TestCollection.makeTest) : entries; +Runner.prototype.select = function (filter) { + return this.tests.select(filter); }; diff --git a/lib/sequence.js b/lib/sequence.js new file mode 100644 index 000000000..378bb4c6d --- /dev/null +++ b/lib/sequence.js @@ -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){}); +}; diff --git a/lib/test-collection.js b/lib/test-collection.js index f684ea578..227ca8e00 100644 --- a/lib/test-collection.js +++ b/lib/test-collection.js @@ -66,6 +66,15 @@ TestCollection.prototype.testsFor = function (testEntry) { 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'); @@ -84,7 +93,7 @@ TestCollection.prototype.testEntriesFor = function (testEntry) { return tests; }; -TestCollection.prototype.buildPhases = function () { +TestCollection.prototype._buildPhases = function () { var hasExclusive = this.select({ exclusive: true, skipped: false, @@ -103,20 +112,37 @@ TestCollection.prototype.buildPhases = function () { type: 'test' }); - var ret = this.select({type: 'before'}).map(function (testEntry) { + var skipped = this.select({ + type: 'test', + skipped: true + }); + + var phases = this.select({type: 'before'}).map(function (testEntry) { return [[makeTest(testEntry)]]; }); - ret.push.apply(ret, serial.map(function (testEntry) { + phases.push.apply(phases, serial.map(function (testEntry) { return [this.testsFor(testEntry)]; }, this)); - ret.push(concurrent.map(function (testEntry) { + phases.push(concurrent.map(function (testEntry) { return this.testsFor(testEntry); }, this)); - ret.push.apply(ret, this.select({type: 'after'}).map(function (testEntry) { + phases.push.apply(phases, this.select({type: 'after'}).map(function (testEntry) { return [[makeTest(testEntry)]]; })); - return ret; + 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) { @@ -128,10 +154,15 @@ TestCollection.prototype.select = function (filter) { }; function makeTest(testEntry) { - var test = new Test(testEntry.title, testEntry.fn); + 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; diff --git a/test/sequence.js b/test/sequence.js new file mode 100644 index 000000000..fce4d9f0d --- /dev/null +++ b/test/sequence.js @@ -0,0 +1,28 @@ +var test = require('tap').test; +var Sequence = require('../lib/sequence'); +var _Test = require('../lib/test'); +var delay = require('delay'); + +function Test(title, fn) { + var test = new _Test(title, fn); + test.metadata = {callback: false}; + return test; +} + + +test('set of tests', function (t) { + t.plan(2); + new Sequence([ + new Test('foo', function (a) { + return delay(30).then(function () { + a.context = 'foo'; + }); + }), + new Test('bar', function (a) { + t.is(a.context, 'foo'); + }) + ]).run().then(function () { + t.pass(); + t.end(); + }); +}); diff --git a/test/test-collection.js b/test/test-collection.js index 477d81261..b6ae0c61e 100644 --- a/test/test-collection.js +++ b/test/test-collection.js @@ -108,13 +108,19 @@ test('buildPhases', function (t) { collection.add(metadata({type: 'after'}), function after2() {}); collection.add(metadata({type: 'test'}), function foo() {}); collection.add(metadata({type: 'test'}), function bar() {}); + collection.add(metadata({type: 'test', serial:true}), function serial1() {}); + collection.add(metadata({type: 'test', serial:true}), function serial2() {}); + collection.add(metadata({type: 'test', skipped:true}), function skipped() {}); t.deepEqual(serialize(collection.buildPhases()), [ [['before1']], [['before2']], + [['serial1.beforeEach', 'serial1', 'serial1.afterEach']], + [['serial2.beforeEach', 'serial2', 'serial2.afterEach']], [ ['foo.beforeEach', 'foo', 'foo.afterEach'], - ['bar.beforeEach', 'bar', 'bar.afterEach'] + ['bar.beforeEach', 'bar', 'bar.afterEach'], + ['skipped'] ], [['after1']], [['after2']]