UNPKG

vows

Version:

Asynchronous BDD & continuous integration for node.js

402 lines (343 loc) 14.1 kB
var events = require('events'), path = require('path'); var vows = require('../vows'); var Context = require('../vows/context').Context; this.Suite = function (subject) { this.subject = subject; this.matcher = /.*/; this.reporter = require('./reporters/dot-matrix'); this.batches = []; this.options = { error: true }; this.reset(); }; this.Suite.prototype = new(function () { this.reset = function () { this.results = { honored: 0, broken: 0, errored: 0, pending: 0, total: 0, time: null }; this.batches.forEach(function (b) { b.lastContext = null; b.remaining = b._remaining; b.honored = b.broken = b.errored = b.total = b.pending = 0; b.vows.forEach(function (vow) { vow.status = null }); b.teardowns = []; }); }; this.addBatch = function (tests) { this.batches.push({ tests: tests, suite: this, vows: [], remaining: 0, _remaining: 0, honored: 0, broken: 0, errored: 0, pending: 0, total: 0, teardowns: [] }); return this; }; this.addVows = this.addBatch; this.parseBatch = function (batch, matcher) { var tests = batch.tests; if ('topic' in tests) { throw new(Error)("missing top-level context."); } // Count the number of vows/emitters 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. // Any key, including assertion function keys can be matched. // If a child matches, then the n parent topics must not be skipped. (function count(tests, _match) { var match = false; var keys = Object.keys(tests).filter(function (k) { return k !== 'topic' && k !== 'teardown'; }); for (var i = 0, key; i < keys.length; i++) { key = keys[i]; // If the parent node, or this one matches. match = _match || matcher.test(key); if (typeof(tests[key]) === 'object') { match = count(tests[key], match); } else { if (typeof(tests[key]) === 'string') { tests[key] = new(String)(tests[key]); } if (! match) { tests[key]._skip = true; } } } // If any of the children matched, // don't skip this node. for (var i = 0; i < keys.length; i++) { if (! tests[keys[i]]._skip) { match = true } } if (match) { batch.remaining ++ } else { tests._skip = true } return match; })(tests, false); batch._remaining = batch.remaining; }; this.runBatch = function (batch) { var topic, tests = batch.tests, emitter = batch.emitter = new(events.EventEmitter); var that = this; batch.status = 'begin'; // 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 // emitter to emit (the topic), at which point it runs the functions under it, // passing the topic as an argument. (function run(ctx, lastTopic) { var old = false; topic = ctx.tests.topic; if (typeof(topic) === 'function') { if (ctx.isEvent || ctx.name === 'on') { throw new Error('Event context cannot contain a topic'); } // Run the topic, passing the previous context topics try { topic = topic.apply(ctx.env, ctx.topics); } // If an unexpected error occurs in the topic, set the return // value to 'undefined' and call back with the error catch (ex) { ctx.env.callback(ex); topic = undefined; } if (typeof(topic) === 'undefined') { ctx._callback = true } } // If this context has a topic, store it in `lastTopic`, // if not, use the last topic, passed down by a parent // context. if (typeof(topic) !== 'undefined' || ctx._callback) { lastTopic = topic; } else { old = true; topic = lastTopic; } // If the topic doesn't return an event emitter (such as an EventEmitter), // we create it ourselves, and emit the value on the next tick. if (! (topic && topic.constructor === events.EventEmitter)) { // If the context is a traditional vow, then a topic can ONLY // be an EventEmitter. However if the context is a sub-event // then the topic may be an instanceof EventEmitter if (!ctx.isEvent || (ctx.isEvent && !(topic instanceof events.EventEmitter))) { ctx.emitter = new(events.EventEmitter); if (! ctx._callback) { process.nextTick(function (val) { return function () { ctx.emitter.emit("success", val) }; }(topic)); } // if I have a callback, push the new topic back up to // lastTopic if (ctx._callback) { lastTopic = topic = ctx.emitter; } else { topic = ctx.emitter; } } } topic.on(ctx.event, 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. // If we're using the parent topic, no need to // prepend it to the topics list, or we'll get // duplicates. if (!old || ctx.isEvent) { Array.prototype.unshift.apply(ctx.topics, arguments) }; }); if (topic.setMaxListeners) { topic.setMaxListeners(Infinity) } // Now run the tests, or sub-contexts Object.keys(ctx.tests).filter(function (k) { return ctx.tests[k] && k !== 'topic' && k !== 'teardown' && !ctx.tests[k]._skip; }).forEach(function (item) { // Create a new evaluation context, // inheriting from the parent one. var env = Object.create(ctx.env); env.suite = that; // Holds the current test or context var vow = Object.create({ callback: ctx.tests[item], context: ctx.title, description: item, binding: ctx.env, status: null, batch: batch }); // 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') || (vow.callback instanceof String)) { topic.addVow(vow); } else if (typeof(vow.callback) === 'object') { // If there's a setup stage, we have to wait for it to fire, // before calling the inner context. // If the event has already fired, the context is 'on' or // there is no setup stage, just run the inner context // synchronously. if (topic && ctx.name !== 'on' && (!topic._vowsEmitedEvents || !topic._vowsEmitedEvents.hasOwnProperty(ctx.event))) { var runInnerContext = function(ctx){ return function(val){ return run(new (Context)(vow, ctx, env), lastTopic); }; }(ctx); topic.on(ctx.event, runInnerContext); // Run an inner context if the outer context fails, too. topic.on('error', runInnerContext); } else { run(new (Context)(vow, ctx, env), lastTopic); } } }); // Teardown if (ctx.tests.teardown) { batch.teardowns.push(ctx); } if (! ctx.tests._skip) { batch.remaining --; } // Check if we're done running the tests exports.tryEnd(batch); // This is our initial, empty context })(new(Context)({ callback: tests, context: null, description: null }, {})); return emitter; }; this.report = function () { return this.reporter.report.apply(this.reporter, arguments); }; this.run = function (options, callback) { var that = this, start; options = options || {}; Object.keys(options).forEach(function (k) { that.options[k] = options[k]; }); this.matcher = this.options.matcher || this.matcher; if (options.reporter) { try { this.reporter = typeof options.reporter === 'string' ? require('./reporters/' + options.reporter) : options.reporter; } catch (e) { console.log('Reporter was not found, defaulting to dot-matrix.'); } } this.batches.forEach(function (batch) { that.parseBatch(batch, that.matcher); }); this.reset(); start = new(Date); if (this.batches.filter(function (b) { return b.remaining > 0 }).length) { this.report(['subject', this.subject]); } return (function run(batches) { var batch = batches.shift(); if (batch) { // If the batch has no vows to run, // go to the next one. if (batch.remaining === 0) { run(batches); } else { that.runBatch(batch).on('end', function () { run(batches); }); } } else { that.results.time = (new(Date) - start) / 1000; that.report(['finish', that.results]); if (callback) { callback(that.results) } if (that.results.honored + that.results.pending === that.results.total) { return 0; } else { return 1; } } })(this.batches.slice(0)); }; this.runParallel = function () {}; this.export = function (module, options) { var that = this; Object.keys(options || {}).forEach(function (k) { that.options[k] = options[k]; }); if (require.main === module) { return this.run(); } else { return module.exports[this.subject] = this; } }; this.exportTo = function (module, options) { // Alias, for JSLint return this.export(module, options); }; }); // // Checks if all the tests in the batch have been run, // and triggers the next batch (if any), by emitting the 'end' event. // this.tryEnd = function (batch) { var result, style, time; if (batch.honored + batch.broken + batch.errored + batch.pending === batch.total && batch.remaining === 0) { Object.keys(batch).forEach(function (k) { (k in batch.suite.results) && (batch.suite.results[k] += batch[k]); }); if (batch.teardowns) { for (var i = batch.teardowns.length - 1, ctx; i >= 0; i--) { runTeardown(batch.teardowns[i]); } maybeFinish(); } function runTeardown(teardown) { var env = Object.create(teardown.env); Object.defineProperty(env, "callback", { get: function () { teardown.awaitingCallback = true; return function () { teardown.awaitingCallback = false; maybeFinish(); }; } }); teardown.tests.teardown.apply(env, teardown.topics); } function maybeFinish() { var pending = batch.teardowns.filter(function (teardown) { return teardown.awaitingCallback; }); if (pending.length === 0) { finish(); } } function finish() { batch.status = 'end'; batch.suite.report(['end']); batch.emitter.emit('end', batch.honored, batch.broken, batch.errored, batch.pending); } } };