Skip to content

Commit

Permalink
Beginning support for built-in setup/teardown functions
Browse files Browse the repository at this point in the history
Basically, you can't do them properly without building them in.  The API
will probably change.
  • Loading branch information
Benjamin Thomas committed Oct 9, 2010
1 parent 8bbb288 commit 1221b9e
Show file tree
Hide file tree
Showing 5 changed files with 486 additions and 79 deletions.
4 changes: 2 additions & 2 deletions lib/console-runner.js
Expand Up @@ -132,10 +132,10 @@ exports.run = function(list, options, callback) {

for(var i = 0; i < tests.length; i++) {
var r = tests[i];
if (r.numAssertions) {
if (typeof r.numAssertions != 'undefined') {
totalAssertions += r.numAssertions;
}
else if (r.failure.constructor == Array) {
else if (Array.isArray(r.failure)) {
multiErrors.push(r);
}
else if (r.failure instanceof assert.AssertionError) {
Expand Down
184 changes: 111 additions & 73 deletions lib/testing.js
Expand Up @@ -29,62 +29,50 @@ exports.runSuite = function(obj, options) {

// keep track of internal state
var suite =
{ started: []
, results: []
{ startTime: new Date()
, todo: exports.getTestsFromObject(obj, options.testName)
, queue: []
, errored: []
, results: []
}

suite.todo = exports.getTestsFromObject(obj, options.testName);
if (suite.todo.length < 1) { return suiteFinished(); }

if (suite.todo.length < 1) { // No tests to run
return suiteFinished();
}

if (options.onSuiteStart) {
// TODO also pass the number of tests?
options.onSuiteStart(options.name);
}
// TODO also pass the number of tests?
if (options.onSuiteStart) { options.onSuiteStart(options.name); }

process.on('uncaughtException', errorHandler);
process.on('exit', exitHandler);

suite.startTime = new Date();

// start the test chain
startNextTest();

/******** functions ********/

function startNextTest() {
// grab the next test
var curTest = suite.todo.shift();

if (!curTest) {
return;
}

// move our test to the list of started tests
suite.started.push(curTest);

curTest.startTime = new Date();
// keep track of the number of assertions made
curTest.numAssertions = 0;
suite.queue.push(curTest);

// this is the object that the tests get for manipulating how the tests work
var testObj =
curTest.startTime = new Date(); // for calculating how long it takes
curTest.func = Array.isArray(curTest.func) ? curTest.func : [curTest.func];
curTest.progress = 0;
curTest.numAssertions = 0; // assertions made
curTest.obj = // object that is passed to the tests
{ get uncaughtExceptionHandler() { return curTest.UEHandler; }
, set uncaughtExceptionHandler(h) {
if (options.parallel) {
throw new Error("Cannot set an 'uncaughtExceptionHandler' when running tests in parallel");
}
curTest.UEHandler = h;
}
, finish: function() { testFinished(curTest); }
, finish: function() { testProgressed(curTest); }
};

// store the testObj in the test
curTest.obj = testObj;

addAssertionFunctions(curTest);

if (options.onTestStart) {
Expand All @@ -95,10 +83,10 @@ exports.runSuite = function(obj, options) {
try {
// actually run the test
// TODO pass finish function?
curTest.func(curTest.obj);
curTest.func[curTest.progress](curTest.obj);
}
catch(err) {
errorHandler(err);
errorHandler(err, curTest);
}

// if we are supposed to run the tests in parallel, start the next test
Expand All @@ -109,25 +97,10 @@ exports.runSuite = function(obj, options) {
}
}

// Called when a test finishes, either successfully or from an assertion error
function testFinished(test, problem) {
// calculate the time it took
test.duration = test.duration || (new Date() - test.startTime);

if (problem) {
function testProgressed(test, problem) {
if (!test.failure && problem) {
test.failure = problem;
}
else {
// if they specified the number of assertions, make sure they match up
if (test.obj.numAssertions && test.obj.numAssertions != test.numAssertions) {
test.failure = new assert.AssertionError(
{ message: 'Wrong number of assertions: ' + test.numAssertions +
' != ' + test.obj.numAssertions
, actual: test.numAssertions
, expected: test.obj.numAssertions
});
}
}

if (test.errors) {
test.errors.forEach(function(e) {
Expand All @@ -136,28 +109,79 @@ exports.runSuite = function(obj, options) {
delete test.errors;
}

// TODO check progress of test
test.progress++;

if (test.func.length == 1) {
testFinished(test);
}
else if (test.func.length == 3) {
if (test.progress == 1) {
if (test.failure) {
// if there was a failure skip the test function and just run teardown
test.progress++;
}

try {
test.func[test.progress](test.obj);
}
catch(err) {
errorHandler(err, test);
}
}
else if (test.progress == 2) {
try {
test.func[test.progress](test.obj);
}
catch(err) {
errorHandler(err, test);
}
}
else {
testFinished(test);
}
}
else {
console.log('Oops, tests need to be 3-tuples [setup, function teardown] or 1-tuples [function]');
}
}

function testFinished(test) {
// if they specified the number of assertions, make sure they match up
if (test.obj.numAssertions && test.obj.numAssertions != test.numAssertions) {
test.failure = new assert.AssertionError(
{ message: 'Wrong number of assertions: ' + test.numAssertions +
' != ' + test.obj.numAssertions
, actual: test.numAssertions
, expected: test.obj.numAssertions
});
}


// calculate the time it took
test.duration = test.duration || (new Date() - test.startTime);

// remove it from the list of tests that have been started
suite.started.splice(suite.started.indexOf(test), 1);
suite.queue.splice(suite.queue.indexOf(test), 1);

// clean up properties that are no longer needed
delete test.obj;
delete test.progress;
delete test.func;
delete test.startTime;
if (test.failure) { delete test.numAssertions; }

suite.results.push(test);
if (options.onTestDone) {
// notify listener
options.onTestDone(test);
}

// if we have no more tests to start and none still running, we're done
if (suite.todo.length == 0 && suite.started.length == 0) {
suiteFinished();
}

process.nextTick(function() {
// if we have no more tests to start and none still running, we're done
if (suite.todo.length == 0 && suite.queue.length == 0) {
suiteFinished();
}

// check to see if we can isolate any errors
checkErrors();

Expand All @@ -166,43 +190,47 @@ exports.runSuite = function(obj, options) {
}

// listens for uncaught errors and keeps track of which tests they could be from
function errorHandler(err) {
function errorHandler(err, test) {
// assertions throw an error, but we can't just catch those errors, because
// then the rest of the test will run. So, we don't catch it and it ends up
// here. When that happens just finish the test.
if (err instanceof assert.AssertionError && err.TEST) {
var t = err.TEST;
delete err.TEST;
testFinished(t, err);
return;
return testProgressed(t, err);
}

// We want to allow tests to supply a function for handling uncaught errors,
// and since all uncaught errors come here, this is where we have to handle
// them.
// (you can only handle uncaught errors when not in parallel mode)
if (!options.parallel && suite.started[0].UEHandler) {
if (!options.parallel && suite.queue[0].UEHandler) {
// an error could possibly be thrown in the UncaughtExceptionHandler, in
// this case we do not want to call the handler again, so we move it
suite.started[0].UEHandlerUsed = suite.started[0].UEHandler;
delete suite.started[0].UEHandler;
suite.queue[0].UEHandlerUsed = suite.queue[0].UEHandler;
delete suite.queue[0].UEHandler;

try {
// run the UncaughtExceptionHandler
suite.started[0].UEHandlerUsed(err);
suite.queue[0].UEHandlerUsed(err);
return;
}
catch(e) {
// we had an error, just run our error handler function on this error
// again. We don't have to worry about it triggering the uncaught
// exception handler again because we moved it just a second ago
return errorHandler(e);
return errorHandler(e, test);
}
}

if (test) {
// shortcut if we know the test that caused this error
return testProgressed(test, err);
}

var summary =
{ error: err
, candidates: suite.started.slice()
, candidates: suite.queue.slice()
, endTime: new Date()
};
summary.candidates.forEach(function(t) {
Expand All @@ -227,42 +255,52 @@ exports.runSuite = function(obj, options) {
for(var i = 0; i < suite.errored.length; i++) {
var err = suite.errored[i];

if (err.error.message == 'E teardown') {
testAsyncE = err;
}
// if there is only one candidate then we can finish that test
if (suite.errored[i].candidates.length == 1) {
if (err.candidates.length == 1) {
suite.errored.splice(i,1);
i = 0;
i = -1;

var test = err.candidates[0];

test.duration = err.endTime - test.startTime;
delete err.endTime;

testFinished(test, err.error);
testProgressed(test, err.error);
}
}

// if the number of errors we've found equals the number of tests still
// running then we know that the errors must match up with the tests, so
// finish each of the tests.
if (suite.errored.length && suite.errored.length == suite.started.length) {
suite.started.slice().forEach(function(t) {
testFinished(t, t.errors.map(function(d) { return d.error; }));
});

if (suite.errored.length && suite.errored.length == suite.queue.length) {
var errs = suite.errored.map(function(d) { return d.error; });
suite.errored = [];
suite.queue.slice().forEach(function(t) {
testProgressed(t, errs);
});
}
}

function exitHandler() {
if (suite.started.length > 0) {
if (suite.queue.length > 0) {
if (options.onPrematureExit) {
options.onPrematureExit(suite.started.map(function(t) { return t.name; }));
options.onPrematureExit(suite.queue.map(function(t) { return t.name; }));
}
}
}

// clean up method which notifies all listeners of what happened
function suiteFinished() {
if (suite.finished) {
return;
}

suite.finished = true;

process.removeListener('uncaughtException', errorHandler);
process.removeListener('exit', exitHandler);

Expand Down Expand Up @@ -384,10 +422,10 @@ exports.expandFiles = function(list, suiteNames, cb) {
suiteNames = null;
}

if (list.constructor != Array) {
if (!Array.isArray(list)) {
list = [list];
}
if (suiteNames && suiteNames.constructor != Array) {
if (suiteNames && !Array.isArray(suiteNames)) {
suiteNames = [suiteNames];
}
if (suiteNames && suiteNames.length === 0) {
Expand Down Expand Up @@ -520,7 +558,7 @@ exports.getTestsFromObject = function(o, filter, namespace) {
var tests = [];
for(var key in o) {
var displayName = (namespace ? namespace+' \u2192 ' : '') + key;
if (typeof o[key] == 'function') {
if (typeof o[key] == 'function' || Array.isArray(o[key])) {
// if the testName option is set, then only add the test to the todo
// list if the name matches
if (!filter || filter.indexOf(key) >= 0) {
Expand Down

0 comments on commit 1221b9e

Please sign in to comment.