5 * Copyright(c) TJ Holowaychuk <tj@vision-media.ca>
10 * Module dependencies.
13 var assert = require('assert'),
14 childProcess = require('child_process'),
15 http = require('http'),
16 path = require('path'),
26 var version = '0.7.2';
36 * Number of tests executed.
42 * Whitelist of tests to run.
54 * Growl notifications.
84 * Usage documentation.
88 + '[bold]{Usage}: expresso [options] <file ...>'
90 + '\n[bold]{Options}:'
91 + '\n -g, --growl Enable growl notifications'
92 + '\n -c, --coverage Generate and report test coverage'
93 + '\n -q, --quiet Suppress coverage report if 100%'
94 + '\n -t, --timeout MS Timeout in milliseconds, defaults to 2000'
95 + '\n -r, --require PATH Require the given module path'
96 + '\n -o, --only TESTS Execute only the comma sperated TESTS (can be set several times)'
97 + '\n -I, --include PATH Unshift the given path to require.paths'
98 + '\n -p, --port NUM Port number for test servers, starts at 5555'
99 + '\n -s, --serial Execute tests serially'
100 + '\n -b, --boring Suppress ansi-escape colors'
101 + '\n -v, --version Output version number'
102 + '\n -h, --help Display help information'
108 args = process.argv.slice(2);
110 while (args.length) {
111 var arg = args.shift();
126 if (arg = args.shift()) {
127 require.paths.unshift(arg);
129 throw new Error('--include requires a path');
134 if (arg = args.shift()) {
135 only = only.concat(arg.split(/ *, */));
137 throw new Error('--only requires comma-separated test names');
142 if (arg = args.shift()) {
143 port = parseInt(arg, 10);
145 throw new Error('--port requires a number');
150 if (arg = args.shift()) {
153 throw new Error('--require requires a path');
158 if (arg = args.shift()) {
159 timeout = parseInt(arg, 10);
161 throw new Error('--timeout requires an argument');
168 childProcess.exec('rm -fr lib-cov && node-jscoverage lib lib-cov', function(err){
170 require.paths.unshift('lib-cov');
191 if (/\.js$/.test(arg)) {
199 * Colorized sys.error().
201 * @param {String} str
205 sys.error(colorize(str));
209 * Colorize the given string using ansi-escape sequences.
210 * Disabled when --boring is set.
212 * @param {String} str
216 function colorize(str){
217 var colors = { bold: 1, red: 31, green: 32, yellow: 33 };
218 return str.replace(/\[(\w+)\]\{([^]*?)\}/g, function(_, color, str){
221 : '\x1B[' + colors[color] + 'm' + str + '\x1B[0m';
225 // Alias deepEqual as eql for complex equality
227 assert.eql = assert.deepEqual;
230 * Assert that `val` is null.
233 * @param {String} msg
236 assert.isNull = function(val, msg) {
237 assert.strictEqual(null, val, msg);
241 * Assert that `val` is not null.
244 * @param {String} msg
247 assert.isNotNull = function(val, msg) {
248 assert.notStrictEqual(null, val, msg);
252 * Assert that `val` is undefined.
255 * @param {String} msg
258 assert.isUndefined = function(val, msg) {
259 assert.strictEqual(undefined, val, msg);
263 * Assert that `val` is not undefined.
266 * @param {String} msg
269 assert.isDefined = function(val, msg) {
270 assert.notStrictEqual(undefined, val, msg);
274 * Assert that `obj` is `type`.
277 * @param {String} type
281 assert.type = function(obj, type, msg){
282 var real = typeof obj;
283 msg = msg || 'typeof ' + sys.inspect(obj) + ' is ' + real + ', expected ' + type;
284 assert.ok(type === real, msg);
288 * Assert that `str` matches `regexp`.
290 * @param {String} str
291 * @param {RegExp} regexp
292 * @param {String} msg
295 assert.match = function(str, regexp, msg) {
296 msg = msg || sys.inspect(str) + ' does not match ' + sys.inspect(regexp);
297 assert.ok(regexp.test(str), msg);
301 * Assert that `val` is within `obj`.
305 * assert.includes('foobar', 'bar');
306 * assert.includes(['foo', 'bar'], 'foo');
308 * @param {String|Array} obj
310 * @param {String} msg
313 assert.includes = function(obj, val, msg) {
314 msg = msg || sys.inspect(obj) + ' does not include ' + sys.inspect(val);
315 assert.ok(obj.indexOf(val) >= 0, msg);
319 * Assert length of `val` is `n`.
323 * @param {String} msg
326 assert.length = function(val, n, msg) {
327 msg = msg || sys.inspect(val) + ' has length of ' + val.length + ', expected ' + n;
328 assert.equal(n, val.length, msg);
332 * Assert response from `server` with
333 * the given `req` object and `res` assertions object.
335 * @param {Server} server
336 * @param {Object} req
337 * @param {Object|Function} res
338 * @param {String} msg
341 assert.response = function(server, req, res, msg){
342 // Check that the server is ready or defer
344 if (!('__deferred' in server)) {
345 server.__deferred = [];
347 server.__deferred.push(arguments);
348 if (!server.__started) {
349 server.listen(server.__port = port++, '127.0.0.1', function(){
350 if (server.__deferred) {
351 process.nextTick(function(){
352 server.__deferred.forEach(function(args){
353 assert.response.apply(assert, args);
358 server.__started = true;
363 // Callback as third or fourth arg
364 var callback = typeof res === 'function'
366 : typeof msg === 'function'
370 // Default messate to test title
371 if (typeof msg === 'function') msg = null;
372 msg = msg || assert.testTitle;
376 server.__pending = server.__pending || 0;
381 server.listen(server.__port = port++, '127.0.0.1', issue);
388 server.client = http.createClient(server.__port);
392 client = server.client,
393 method = req.method || 'GET',
394 status = res.status || res.statusCode,
395 data = req.data || req.body,
396 requestTimeout = req.timeout || 0;
398 var request = client.request(method, req.url, req.headers);
401 if (requestTimeout) {
402 timer = setTimeout(function(){
403 --server.__pending || server.close();
405 assert.fail(msg + 'Request timed out after ' + requestTimeout + 'ms.');
409 if (data) request.write(data);
410 request.addListener('response', function(response){
412 response.setEncoding('utf8');
413 response.addListener('data', function(chunk){ response.body += chunk; });
414 response.addListener('end', function(){
415 --server.__pending || server.close();
416 if (timer) clearTimeout(timer);
418 // Assert response body
419 if (res.body !== undefined) {
420 var eql = res.body instanceof RegExp
421 ? res.body.test(response.body)
422 : res.body === response.body;
425 msg + 'Invalid response body.\n'
426 + ' Expected: ' + sys.inspect(res.body) + '\n'
427 + ' Got: ' + sys.inspect(response.body)
431 // Assert response status
432 if (typeof status === 'number') {
436 msg + colorize('Invalid response status code.\n'
437 + ' Expected: [green]{' + status + '}\n'
438 + ' Got: [red]{' + response.statusCode + '}')
442 // Assert response headers
444 var keys = Object.keys(res.headers);
445 for (var i = 0, len = keys.length; i < len; ++i) {
447 actual = response.headers[name.toLowerCase()],
448 expected = res.headers[name],
449 eql = expected instanceof RegExp
450 ? expected.test(actual)
451 : expected == actual;
454 msg + colorize('Invalid response header [bold]{' + name + '}.\n'
455 + ' Expected: [green]{' + expected + '}\n'
456 + ' Got: [red]{' + actual + '}')
470 * Pad the given string to the maximum width provided.
472 * @param {String} str
473 * @param {Number} width
477 function lpad(str, width) {
479 var n = width - str.length;
480 if (n < 1) return str;
481 while (n--) str = ' ' + str;
486 * Pad the given string to the maximum width provided.
488 * @param {String} str
489 * @param {Number} width
493 function rpad(str, width) {
495 var n = width - str.length;
496 if (n < 1) return str;
497 while (n--) str = str + ' ';
502 * Report test coverage.
504 * @param {Object} cov
507 function reportCoverage(cov) {
509 print('\n [bold]{Test Coverage}\n');
510 var sep = ' +------------------------------------------+----------+------+------+--------+',
511 lastSep = ' +----------+------+------+--------+';
513 sys.puts(' | filename | coverage | LOC | SLOC | missed |');
515 for (var name in cov) {
516 var file = cov[name];
517 if (Array.isArray(file)) {
518 sys.print(' | ' + rpad(name, 40));
519 sys.print(' | ' + lpad(file.coverage.toFixed(2), 8));
520 sys.print(' | ' + lpad(file.LOC, 4));
521 sys.print(' | ' + lpad(file.SLOC, 4));
522 sys.print(' | ' + lpad(file.totalMisses, 6));
527 sys.print(' ' + rpad('', 40));
528 sys.print(' | ' + lpad(cov.coverage.toFixed(2), 8));
529 sys.print(' | ' + lpad(cov.LOC, 4));
530 sys.print(' | ' + lpad(cov.SLOC, 4));
531 sys.print(' | ' + lpad(cov.totalMisses, 6));
535 for (var name in cov) {
536 if (name.match(/\.js$/)) {
537 var file = cov[name];
538 if ((file.coverage < 100) || !quiet) {
539 print('\n [bold]{' + name + '}:');
548 * Populate code coverage data.
550 * @param {Object} cov
553 function populateCoverage(cov) {
560 for (var name in cov) {
561 var file = cov[name];
562 if (Array.isArray(file)) {
565 cov.totalHits += file.totalHits = coverage(file, true);
566 cov.totalMisses += file.totalMisses = coverage(file, false);
567 file.totalLines = file.totalHits + file.totalMisses;
568 cov.SLOC += file.SLOC = file.totalLines;
569 if (!file.source) file.source = [];
570 cov.LOC += file.LOC = file.source.length;
571 file.coverage = (file.totalHits / file.totalLines) * 100;
573 var width = file.source.length.toString().length;
574 file.source = file.source.map(function(line, i){
576 var hits = file[i] === 0 ? 0 : (file[i] || ' ');
579 hits = '\x1b[31m' + hits + '\x1b[0m';
580 line = '\x1b[41m' + line + '\x1b[0m';
582 hits = '\x1b[32m' + hits + '\x1b[0m';
585 return '\n ' + lpad(i, width) + ' | ' + hits + ' | ' + line;
589 cov.coverage = (cov.totalHits / cov.SLOC) * 100;
593 * Total coverage for the given file data.
595 * @param {Array} data
599 function coverage(data, val) {
601 for (var i = 0, len = data.length; i < len; ++i) {
602 if (data[i] !== undefined && data[i] == val) ++n;
608 * Test if all files have 100% coverage
610 * @param {Object} cov
614 function hasFullCoverage(cov) {
615 for (var name in cov) {
616 var file = cov[name];
617 if (file instanceof Array) {
618 if (file.coverage !== 100) {
627 * Run the given test `files`, or try _test/*_.
629 * @param {Array} files
632 function run(files) {
636 files = fs.readdirSync('test').map(function(file){
637 return 'test/' + file;
640 print('\n failed to load tests in [bold]{./test}\n');
649 * Show the cursor when `show` is true, otherwise hide it.
651 * @param {Boolean} show
654 function cursor(show) {
656 sys.print('\x1b[?25h');
658 sys.print('\x1b[?25l');
663 * Run the given test `files`.
665 * @param {Array} files
668 function runFiles(files) {
672 runFile(files.shift(), next);
676 files.forEach(runFile);
681 * Run tests for the given `file`, callback `fn()` when finished.
683 * @param {String} file
684 * @param {Function} fn
687 function runFile(file, fn) {
688 if (file.match(/\.js$/)) {
689 var title = path.basename(file),
690 file = path.join(cwd, file),
691 mod = require(file.replace(/\.js$/, ''));
693 var len = Object.keys(mod).length;
695 runSuite(title, mod, fn);
697 setTimeout(check, 20);
704 * Report `err` for the given `test` and `suite`.
706 * @param {String} suite
707 * @param {String} test
711 function error(suite, test, err) {
714 stack = err.stack.replace(err.name, ''),
715 label = test === 'uncaught'
717 : suite + ' ' + test;
718 print('\n [bold]{' + label + '}: [red]{' + name + '}' + stack + '\n');
722 * Run the given tests, callback `fn()` when finished.
724 * @param {String} title
725 * @param {Object} tests
726 * @param {Function} fn
730 function runSuite(title, tests, fn) {
732 var keys = only.length
734 : Object.keys(tests);
737 var setup = tests.setup || function(fn){ fn(); };
743 test = tests[key = keys.shift()];
745 if (key === 'setup') return next();
751 assert.testTitle = key;
754 if (++dots % 25 === 0) sys.print('\n');
756 if (test.length < 1) {
760 var id = setTimeout(function(){
761 throw new Error("'" + key + "' timed out");
771 addBeforeExit(function(){
775 error(title, key, err);
781 error(title, key, err);
792 * Adds before exit handlers with only one event handler
794 * @param {Function} callback
798 var beforeExitHandlers = [];
800 function addBeforeExit (fn) {
801 if (!beforeExitHandlers.length)
802 process.on('beforeExit', function () {
803 for (var i = 0, l = beforeExitHandlers.length; i < l; i++)
804 beforeExitHandlers[i]();
806 beforeExitHandlers.push(fn);
815 process.emit('beforeExit');
817 print('\n [bold]{Failures}: [red]{' + failures + '}\n\n');
818 notify('Failures: ' + failures);
820 if (serial) print('');
821 print('\n [green]{100%} ' + testcount + ' tests\n');
824 if (typeof _$jscoverage === 'object') {
825 populateCoverage(_$jscoverage);
826 if (!hasFullCoverage(_$jscoverage) || !quiet) {
827 reportCoverage(_$jscoverage);
833 * Growl notify the given `msg`.
835 * @param {String} msg
838 function notify(msg) {
840 childProcess.exec('growlnotify -name Expresso -m "' + msg + '"');
844 // Report uncaught exceptions
846 process.addListener('uncaughtException', function(err){
847 error('uncaught', 'uncaught', err);
852 ['INT', 'TERM', 'QUIT'].forEach(function(sig){
853 process.addListener('SIG' + sig, function(){
859 // Report test coverage when available
860 // and emit "beforeExit" event to perform
863 var orig = process.emit;
864 process.emit = function(event){
865 if (event === 'exit') {
867 process.reallyExit(failures);
869 orig.apply(this, arguments);
874 if (!defer) run(files);