blob: d31c2558e67935e0187ba540e2e93b93713aec4a [file] [log] [blame]
var deepEqual = require('deep-equal');
var defined = require('defined');
var path = require('path');
var inherits = require('inherits');
var EventEmitter = require('events').EventEmitter;
var has = require('has');
var trim = require('string.prototype.trim');
var bind = require('function-bind');
var forEach = require('for-each');
var isEnumerable = bind.call(Function.call, Object.prototype.propertyIsEnumerable);
var toLowerCase = bind.call(Function.call, String.prototype.toLowerCase);
module.exports = Test;
var nextTick = typeof setImmediate !== 'undefined'
? setImmediate
: process.nextTick;
var safeSetTimeout = setTimeout;
var safeClearTimeout = clearTimeout;
inherits(Test, EventEmitter);
var getTestArgs = function (name_, opts_, cb_) {
var name = '(anonymous)';
var opts = {};
var cb;
for (var i = 0; i < arguments.length; i++) {
var arg = arguments[i];
var t = typeof arg;
if (t === 'string') {
name = arg;
} else if (t === 'object') {
opts = arg || opts;
} else if (t === 'function') {
cb = arg;
}
}
return { name: name, opts: opts, cb: cb };
};
function Test (name_, opts_, cb_) {
if (! (this instanceof Test)) {
return new Test(name_, opts_, cb_);
}
var args = getTestArgs(name_, opts_, cb_);
this.readable = true;
this.name = args.name || '(anonymous)';
this.assertCount = 0;
this.pendingCount = 0;
this._skip = args.opts.skip || false;
this._timeout = args.opts.timeout;
this._plan = undefined;
this._cb = args.cb;
this._progeny = [];
this._ok = true;
var depthEnvVar = process.env.NODE_TAPE_OBJECT_PRINT_DEPTH;
if (args.opts.objectPrintDepth) {
this._objectPrintDepth = args.opts.objectPrintDepth;
} else if (depthEnvVar) {
if (toLowerCase(depthEnvVar) === 'infinity') {
this._objectPrintDepth = Infinity;
} else {
this._objectPrintDepth = depthEnvVar;
}
} else {
this._objectPrintDepth = 5;
}
for (var prop in this) {
this[prop] = (function bind(self, val) {
if (typeof val === 'function') {
return function bound() {
return val.apply(self, arguments);
};
}
return val;
})(this, this[prop]);
}
}
Test.prototype.run = function () {
if (this._skip) {
this.comment('SKIP ' + this.name);
}
if (!this._cb || this._skip) {
return this._end();
}
if (this._timeout != null) {
this.timeoutAfter(this._timeout);
}
this.emit('prerun');
this._cb(this);
this.emit('run');
};
Test.prototype.test = function (name, opts, cb) {
var self = this;
var t = new Test(name, opts, cb);
this._progeny.push(t);
this.pendingCount++;
this.emit('test', t);
t.on('prerun', function () {
self.assertCount++;
})
if (!self._pendingAsserts()) {
nextTick(function () {
self._end();
});
}
nextTick(function() {
if (!self._plan && self.pendingCount == self._progeny.length) {
self._end();
}
});
};
Test.prototype.comment = function (msg) {
var that = this;
forEach(trim(msg).split('\n'), function (aMsg) {
that.emit('result', trim(aMsg).replace(/^#\s*/, ''));
});
};
Test.prototype.plan = function (n) {
this._plan = n;
this.emit('plan', n);
};
Test.prototype.timeoutAfter = function(ms) {
if (!ms) throw new Error('timeoutAfter requires a timespan');
var self = this;
var timeout = safeSetTimeout(function() {
self.fail('test timed out after ' + ms + 'ms');
self.end();
}, ms);
this.once('end', function() {
safeClearTimeout(timeout);
});
}
Test.prototype.end = function (err) {
var self = this;
if (arguments.length >= 1 && !!err) {
this.ifError(err);
}
if (this.calledEnd) {
this.fail('.end() called twice');
}
this.calledEnd = true;
this._end();
};
Test.prototype._end = function (err) {
var self = this;
if (this._progeny.length) {
var t = this._progeny.shift();
t.on('end', function () { self._end() });
t.run();
return;
}
if (!this.ended) this.emit('end');
var pendingAsserts = this._pendingAsserts();
if (!this._planError && this._plan !== undefined && pendingAsserts) {
this._planError = true;
this.fail('plan != count', {
expected : this._plan,
actual : this.assertCount
});
}
this.ended = true;
};
Test.prototype._exit = function () {
if (this._plan !== undefined &&
!this._planError && this.assertCount !== this._plan) {
this._planError = true;
this.fail('plan != count', {
expected : this._plan,
actual : this.assertCount,
exiting : true
});
} else if (!this.ended) {
this.fail('test exited without ending', {
exiting: true
});
}
};
Test.prototype._pendingAsserts = function () {
if (this._plan === undefined) {
return 1;
}
return this._plan - (this._progeny.length + this.assertCount);
};
Test.prototype._assert = function assert (ok, opts) {
var self = this;
var extra = opts.extra || {};
var res = {
id : self.assertCount ++,
ok : Boolean(ok),
skip : defined(extra.skip, opts.skip),
name : defined(extra.message, opts.message, '(unnamed assert)'),
operator : defined(extra.operator, opts.operator),
objectPrintDepth : self._objectPrintDepth
};
if (has(opts, 'actual') || has(extra, 'actual')) {
res.actual = defined(extra.actual, opts.actual);
}
if (has(opts, 'expected') || has(extra, 'expected')) {
res.expected = defined(extra.expected, opts.expected);
}
this._ok = Boolean(this._ok && ok);
if (!ok) {
res.error = defined(extra.error, opts.error, new Error(res.name));
}
if (!ok) {
var e = new Error('exception');
var err = (e.stack || '').split('\n');
var dir = __dirname + path.sep;
for (var i = 0; i < err.length; i++) {
/*
Stack trace lines may resemble one of the following. We need
to should correctly extract a function name (if any) and
path / line no. for each line.
at myFunction (/path/to/file.js:123:45)
at myFunction (/path/to/file.other-ext:123:45)
at myFunction (/path to/file.js:123:45)
at myFunction (C:\path\to\file.js:123:45)
at myFunction (/path/to/file.js:123)
at Test.<anonymous> (/path/to/file.js:123:45)
at Test.bound [as run] (/path/to/file.js:123:45)
at /path/to/file.js:123:45
Regex has three parts. First is non-capturing group for 'at '
(plus anything preceding it).
/^(?:[^\s]*\s*\bat\s+)/
Second captures function call description (optional). This is
not necessarily a valid JS function name, but just what the
stack trace is using to represent a function call. It may look
like `<anonymous>` or 'Test.bound [as run]'.
For our purposes, we assume that, if there is a function
name, it's everything leading up to the first open
parentheses (trimmed) before our pathname.
/(?:(.*)\s+\()?/
Last part captures file path plus line no (and optional
column no).
/((?:\/|[A-Z]:\\)[^:\)]+:(\d+)(?::(\d+))?)/
*/
var re = /^(?:[^\s]*\s*\bat\s+)(?:(.*)\s+\()?((?:\/|[A-Z]:\\)[^:\)]+:(\d+)(?::(\d+))?)/
var m = re.exec(err[i]);
if (!m) {
continue;
}
var callDescription = m[1] || '<anonymous>';
var filePath = m[2];
if (filePath.slice(0, dir.length) === dir) {
continue;
}
// Function call description may not (just) be a function name.
// Try to extract function name by looking at first "word" only.
res.functionName = callDescription.split(/\s+/)[0]
res.file = filePath;
res.line = Number(m[3]);
if (m[4]) res.column = Number(m[4]);
res.at = callDescription + ' (' + filePath + ')';
break;
}
}
self.emit('result', res);
var pendingAsserts = self._pendingAsserts();
if (!pendingAsserts) {
if (extra.exiting) {
self._end();
} else {
nextTick(function () {
self._end();
});
}
}
if (!self._planError && pendingAsserts < 0) {
self._planError = true;
self.fail('plan != count', {
expected : self._plan,
actual : self._plan - pendingAsserts
});
}
};
Test.prototype.fail = function (msg, extra) {
this._assert(false, {
message : msg,
operator : 'fail',
extra : extra
});
};
Test.prototype.pass = function (msg, extra) {
this._assert(true, {
message : msg,
operator : 'pass',
extra : extra
});
};
Test.prototype.skip = function (msg, extra) {
this._assert(true, {
message : msg,
operator : 'skip',
skip : true,
extra : extra
});
};
function assert(value, msg, extra) {
this._assert(value, {
message : defined(msg, 'should be truthy'),
operator : 'ok',
expected : true,
actual : value,
extra : extra
});
}
Test.prototype.ok
= Test.prototype['true']
= Test.prototype.assert
= assert;
function notOK(value, msg, extra) {
this._assert(!value, {
message : defined(msg, 'should be falsy'),
operator : 'notOk',
expected : false,
actual : value,
extra : extra
});
}
Test.prototype.notOk
= Test.prototype['false']
= Test.prototype.notok
= notOK;
function error(err, msg, extra) {
this._assert(!err, {
message : defined(msg, String(err)),
operator : 'error',
actual : err,
extra : extra
});
}
Test.prototype.error
= Test.prototype.ifError
= Test.prototype.ifErr
= Test.prototype.iferror
= error;
function equal(a, b, msg, extra) {
this._assert(a === b, {
message : defined(msg, 'should be equal'),
operator : 'equal',
actual : a,
expected : b,
extra : extra
});
}
Test.prototype.equal
= Test.prototype.equals
= Test.prototype.isEqual
= Test.prototype.is
= Test.prototype.strictEqual
= Test.prototype.strictEquals
= equal;
function notEqual(a, b, msg, extra) {
this._assert(a !== b, {
message : defined(msg, 'should not be equal'),
operator : 'notEqual',
actual : a,
notExpected : b,
extra : extra
});
}
Test.prototype.notEqual
= Test.prototype.notEquals
= Test.prototype.notStrictEqual
= Test.prototype.notStrictEquals
= Test.prototype.isNotEqual
= Test.prototype.isNot
= Test.prototype.not
= Test.prototype.doesNotEqual
= Test.prototype.isInequal
= notEqual;
function tapeDeepEqual(a, b, msg, extra) {
this._assert(deepEqual(a, b, { strict: true }), {
message : defined(msg, 'should be equivalent'),
operator : 'deepEqual',
actual : a,
expected : b,
extra : extra
});
}
Test.prototype.deepEqual
= Test.prototype.deepEquals
= Test.prototype.isEquivalent
= Test.prototype.same
= tapeDeepEqual;
function deepLooseEqual(a, b, msg, extra) {
this._assert(deepEqual(a, b), {
message : defined(msg, 'should be equivalent'),
operator : 'deepLooseEqual',
actual : a,
expected : b,
extra : extra
});
}
Test.prototype.deepLooseEqual
= Test.prototype.looseEqual
= Test.prototype.looseEquals
= deepLooseEqual;
function notDeepEqual(a, b, msg, extra) {
this._assert(!deepEqual(a, b, { strict: true }), {
message : defined(msg, 'should not be equivalent'),
operator : 'notDeepEqual',
actual : a,
notExpected : b,
extra : extra
});
}
Test.prototype.notDeepEqual
= Test.prototype.notEquivalent
= Test.prototype.notDeeply
= Test.prototype.notSame
= Test.prototype.isNotDeepEqual
= Test.prototype.isNotDeeply
= Test.prototype.isNotEquivalent
= Test.prototype.isInequivalent
= notDeepEqual;
function notDeepLooseEqual(a, b, msg, extra) {
this._assert(!deepEqual(a, b), {
message : defined(msg, 'should be equivalent'),
operator : 'notDeepLooseEqual',
actual : a,
expected : b,
extra : extra
});
}
Test.prototype.notDeepLooseEqual
= Test.prototype.notLooseEqual
= Test.prototype.notLooseEquals
= notDeepLooseEqual;
Test.prototype['throws'] = function (fn, expected, msg, extra) {
if (typeof expected === 'string') {
msg = expected;
expected = undefined;
}
var caught = undefined;
try {
fn();
} catch (err) {
caught = { error : err };
if ((err != null) && (!isEnumerable(err, 'message') || !has(err, 'message'))) {
var message = err.message;
delete err.message;
err.message = message;
}
}
var passed = caught;
if (expected instanceof RegExp) {
passed = expected.test(caught && caught.error);
expected = String(expected);
}
if (typeof expected === 'function' && caught) {
passed = caught.error instanceof expected;
caught.error = caught.error.constructor;
}
this._assert(typeof fn === 'function' && passed, {
message : defined(msg, 'should throw'),
operator : 'throws',
actual : caught && caught.error,
expected : expected,
error: !passed && caught && caught.error,
extra : extra
});
};
Test.prototype.doesNotThrow = function (fn, expected, msg, extra) {
if (typeof expected === 'string') {
msg = expected;
expected = undefined;
}
var caught = undefined;
try {
fn();
}
catch (err) {
caught = { error : err };
}
this._assert(!caught, {
message : defined(msg, 'should not throw'),
operator : 'throws',
actual : caught && caught.error,
expected : expected,
error : caught && caught.error,
extra : extra
});
};
Test.skip = function (name_, _opts, _cb) {
var args = getTestArgs.apply(null, arguments);
args.opts.skip = true;
return Test(args.name, args.opts, args.cb);
};
// vim: set softtabstop=4 shiftwidth=4: