Skip to content

Commit

Permalink
API change, ability to run serial test suites
Browse files Browse the repository at this point in the history
  • Loading branch information
cloudhead committed May 11, 2010
1 parent 0b891d6 commit b95d282
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 133 deletions.
270 changes: 139 additions & 131 deletions lib/vows.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ assert.AssertionError.prototype.toString = function () {
}

//
// This function gets added to events.Promise.prototype, by default.
// This function gets added to events.EventEmitter.prototype, by default.
// It's essentially a wrapper around `addCallback`, which adds all the specification
// goodness.
//
Expand Down Expand Up @@ -128,9 +128,135 @@ function addVow(/* description & callback */) {
}
};

function Context(vow, ctx, env) {
this.tests = vow.callback;
this.topics = (ctx.topics || []).slice(0);
this.env = env || {};
this.env.context = this;
this.name = (ctx.name ? ctx.name + ' ' : '') +
(vow.description || '');
}

function addVows(tests) {
var promise = new(events.EventEmitter);
vows.promises.push(promise);

this.addListener("success", function () {
// We run the tests asynchronously, for added flexibility
process.nextTick(function () {
var topic, vow, env;

if (typeof(tests) === 'function') {
return tests.call(null);
} else {
// Count the number of vows/promises expected to fire,
// so we know when the tests are over.
// We match the keys against `matcher`, to decide
// whether or not they should be included in the test.
(function count(tests) {
var match = false;
vows.remaining++;
Object.keys(tests).forEach(function (key) {
if (typeof(tests[key]) === "object" && !Array.isArray(tests[key])) {
if (! (match = count(tests[key]) ||
match || vows.options.matcher.test(key))) {
delete tests[key];
vows.remaining--;
}
}
});
return match;
})(tests);

// The test runner, it calls itself recursively, passing the
// previous context to the inner contexts. This is so the `topic`
// functions have access to all the previous context topics in their
// arguments list.
// It is defined and invoked at the same time.
// If it encounters a `topic` function, it waits for the returned
// promise to emit (the topic), at which point it runs the functions under it,
// passing the topic as an argument.
(function run(ctx) {
var ctxAdded = false;

if ('topic' in ctx.tests) {

// Topic isn't a function, wrap it into one.
if (typeof(ctx.tests.topic) !== 'function') {
ctx.tests.topic = (function (topic) {
return function () { return topic };
})(ctx.tests.topic);
}

// Run the topic, passing the previous context topics
topic = ctx.tests.topic.apply(ctx.env, ctx.topics);

// If the topic doesn't return an event emitter (such as a promise),
// we create it ourselves, and emit the value on the next tick.
if (! (topic instanceof vows.options.Emitter)) {
var emitter = new(vows.options.Emitter);

process.nextTick(function (val) {
return function () { emitter.emit("success", val) };
}(topic)); topic = emitter;
}

topic.addListener('success', function (val) {
// Once the topic fires, add the return value
// to the beginning of the topics list, so it
// becomes the first argument for the next topic.
ctx.topics.unshift(val);
});
} else { topic = null }

// Now run the tests, or sub-contexts
Object.keys(ctx.tests).filter(function (k) {
return ctx.tests[k] && k !== 'topic';
}).forEach(function (item) {
// Holds the current test or context
vow = Object.create({
callback: ctx.tests[item],
context: ctx.name,
description: item,
binding: ctx.env
});
env = Object.create(ctx.env);

// If we encounter a function, add it to the callbacks
// of the `topic` function, so it'll get called once the
// topic fires.
// If we encounter an object literal, we recurse, sending it
// our current context.
if (typeof(vow.callback) === 'function') {
topic.addVow(vow);
} else if (typeof(vow.callback) === 'object' && ! Array.isArray(vow.callback)) {
// If there's a setup stage, we have to wait for it to fire,
// before calling the inner context. Else, just run the inner context
// synchronously.
if (topic) {
topic.addListener("success", function (vow, ctx) {
return function (val) {
return run(new(Context)(vow, ctx, env));
};
}(vow, ctx));
} else {
run(new(Context)(vow, ctx, env));
}
}
});
// Check if we're done running the tests
tryFinish(--vows.remaining);
// This is our initial, empty context
})(new(Context)({ callback: tests, context: null, description: null }, {}));
}
});
});
return promise;
}

function puts() {
var args = Array.prototype.slice.call(arguments);
if (vows.promise.listeners('end').length > 0) {
if (vows.promises[vows.promises.length - 1].listeners('end').length > 0) {
buffer.push(args.join('\n'));
} else {
sys.puts.apply(null, args);
Expand All @@ -153,7 +279,7 @@ function tryFinish(remaining) {
puts("\n" + stylize(result, style));
}

vows.promise.emit("end", honored, broken, errored);
vows.promises[vows.promises.length - 1].emit("end", honored, broken, errored);
if (broken || errored) { sys.puts(buffer.join('\n') + '\n') }

process.stdout.addListener('drain', function () {
Expand Down Expand Up @@ -209,146 +335,28 @@ vows.options = {
// Run all vows/tests.
// It can take either a function as `tests`,
// or an object literal.
vows.tell = function (subject, tests) {
vows.tell = function (subject) {
this.options.Emitter.prototype.addVow = addVow;
this.tests = tests;
this.options.Emitter.prototype.addVows = addVows;
this.remaining = 0;
this.promises = [];

// Reset values
total = 0, honored = 0,
broken = 0, errored = 0;
buffer = [];

this.promise = new(events.EventEmitter);

function Context(vow, ctx, env) {
this.tests = vow.callback;
this.topics = (ctx.topics || []).slice(0);
this.env = env || {};
this.env.context = this;
this.name = (ctx.name ? ctx.name + ' ' : '') +
(vow.description || '');
}
var promise = new(events.EventEmitter);

// We run the tests asynchronously, for added flexibility
process.nextTick(function () {
var topic, vow, env;

if (typeof(subject) === 'string' && tests) {
if (!vows.options.brief) {
puts('\n' + stylize(subject, 'underline') + '\n');
}
} else if (subject instanceof Object || subject instanceof Function) {
vows.tests = subject;
} else { throw "tell() takes a topic and an Object" }

start = new(Date);

if (typeof(vows.tests) === 'function') {
return vows.tests.call(null);
} else {
// Count the number of vows/promises expected to fire,
// so we know when the tests are over.
// We match the keys against `matcher`, to decide
// whether or not they should be included in the test.
(function count(tests) {
var match = false;
vows.remaining++;
Object.keys(tests).forEach(function (key) {
if (typeof(tests[key]) === "object" && !Array.isArray(tests[key])) {
if (! (match = count(tests[key]) ||
match || vows.options.matcher.test(key))) {
delete tests[key];
vows.remaining--;
}
}
});
return match;
})(vows.tests);

// The test runner, it calls itself recursively, passing the
// previous context to the inner contexts. This is so the `topic`
// functions have access to all the previous context topics in their
// arguments list.
// It is defined and invoked at the same time.
// If it encounters a `topic` function, it waits for the returned
// promise to emit (the topic), at which point it runs the functions under it,
// passing the topic as an argument.
(function run(ctx) {
var ctxAdded = false;

if ('topic' in ctx.tests) {

// Topic isn't a function, wrap it into one.
if (typeof(ctx.tests.topic) !== 'function') {
ctx.tests.topic = (function (topic) {
return function () { return topic };
})(ctx.tests.topic);
}

// Run the topic, passing the previous context topics
topic = ctx.tests.topic.apply(ctx.env, ctx.topics);

// If the topic doesn't return an event emitter (such as a promise),
// we create it ourselves, and emit the value on the next tick.
if (! (topic instanceof vows.options.Emitter)) {
var emitter = new(vows.options.Emitter);

process.nextTick(function (val) {
return function () { emitter.emit("success", val) };
}(topic)); topic = emitter;
}

topic.addListener('success', function (val) {
// Once the topic fires, add the return value
// to the beginning of the topics list, so it
// becomes the first argument for the next topic.
ctx.topics.unshift(val);
});
} else { topic = null }

// Now run the tests, or sub-contexts
Object.keys(ctx.tests).filter(function (k) {
return ctx.tests[k] && k !== 'topic';
}).forEach(function (item) {
// Holds the current test or context
vow = Object.create({
callback: ctx.tests[item],
context: ctx.name,
description: item,
binding: ctx.env
});
env = Object.create(ctx.env);

// If we encounter a function, add it to the callbacks
// of the `topic` function, so it'll get called once the
// topic fires.
// If we encounter an object literal, we recurse, sending it
// our current context.
if (typeof(vow.callback) === 'function') {
topic.addVow(vow);
} else if (typeof(vow.callback) === 'object' && ! Array.isArray(vow.callback)) {
// If there's a setup stage, we have to wait for it to fire,
// before calling the inner context. Else, just run the inner context
// synchronously.
if (topic) {
topic.addListener("success", function (vow, ctx) {
return function (val) {
return run(new(Context)(vow, ctx, env));
};
}(vow, ctx));
} else {
run(new(Context)(vow, ctx, env));
}
}
});
// Check if we're done running the tests
tryFinish(--vows.remaining);
// This is our initial, empty context
})(new(Context)({ callback: vows.tests, context: null, description: null }, {}));
if (!vows.options.brief) {
puts('\n' + stylize(subject, 'underline') + '\n');
}
promise.emit("success");
});
return this.promise;
start = new(Date);

return promise;
};

vows.describe = vows.tell;
Expand Down
2 changes: 1 addition & 1 deletion test/addvow-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var promiser = function (val) {
}
};

vows.tell("Vows:unit", function () {
vows.tell("Vows:unit").addVows(function () {
promiser("hello world")().addVow(function (val) {
assert.equal(val, "hello world");
}, "addVow()");
Expand Down
2 changes: 1 addition & 1 deletion test/vows-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ var promiser = function (val) {
}
};

vows.describe("Vows", {
vows.describe("Vows").addVows({
"A context": {
topic: promiser("hello world"),

Expand Down

0 comments on commit b95d282

Please sign in to comment.