# Expresso Insanely fast TDD framework for node featuring code coverage reporting. |
## expresso | bin/expresso
# !/usr/bin/env node |
    !
     * Expresso
     * Copyright(c) TJ Holowaychuk &lt;tj@vision-media.ca&gt;
     * (MIT Licensed)
Module dependencies. |
    var assert = require('assert'),
        childProcess = require('child_process'),
        http = require('http'),
        path = require('path'),
        sys = require('sys'),
        cwd = process.cwd(),
        fs = require('fs'),
        defer;
Expresso version. |
    var version = '0.6.1';
Failure count. |
    var failures = 0;
Number of tests executed. |
    var testcount = 0;
Whitelist of tests to run. |
    var only = [];
Boring output. |
    var boring = false;
Growl notifications. |
    var growl = false;
Server port. |
    var port = 5555;
Watch mode. |
    var watch = false;
Execute serially. |
    var serial = false;
Usage documentation. |
    var usage = ''
        + '[bold]{Usage}: expresso [options] <file ...>'
        + '\n'
        + '\n[bold]{Options}:'
        + '\n  -w, --watch          Watch for modifications and re-execute tests'
        + '\n  -g, --growl          Enable growl notifications'
        + '\n  -c, --coverage       Generate and report test coverage'
        + '\n  -r, --require PATH   Require the given module path'
        + '\n  -o, --only TESTS     Execute only the comma sperated TESTS (can be set several times)'
        + '\n  -I, --include PATH   Unshift the given path to require.paths'
        + '\n  -p, --port NUM       Port number for test servers, starts at 5555'
        + '\n  -s, --serial         Execute tests serially'
        + '\n  -b, --boring         Suppress ansi-escape colors'
        + '\n  -v, --version        Output version number'
        + '\n  -h, --help           Display help information'
        + '\n';

    // Parse arguments

    var files = [],
        args = process.argv.slice(2);

    while (args.length) {
        var arg = args.shift();
        switch (arg) {
            case '-h':
            case '--help':
                print(usage + '\n');
                process.exit(1);
                break;
            case '-v':
            case '--version':
                sys.puts(version);
                process.exit(1);
                break;
            case '-i':
            case '-I':
            case '--include':
                if (arg = args.shift()) {
                    require.paths.unshift(arg);
                } else {
                    throw new Error('--include requires a path');
                }
                break;
            case '-o':
            case '--only':
                if (arg = args.shift()) {
                    only = only.concat(arg.split(/ *, */));
                } else {
                    throw new Error('--only requires comma-separated test names');
                }
                break;
            case '-p':
            case '--port':
                if (arg = args.shift()) {
                    port = parseInt(arg, 10);
                } else {
                    throw new Error('--port requires a number');
                }
                break;
            case '-r':
            case '--require':
                if (arg = args.shift()) {
                    require(arg);
                } else {
                    throw new Error('--require requires a path');
                }
                break;
            case '-c':
            case '--cov':
            case '--coverage':
                defer = true;
                childProcess.exec('rm -fr lib-cov && node-jscoverage lib lib-cov', function(err){
                    if (err) throw err;
                    require.paths.unshift('lib-cov');
                    run(files);
                })
                break;
            case '-b':
            case '--boring':
            	boring = true;
            	break;
            case '-w':
            case '--watch':
                watch = true;
                break;
            case '-g':
            case '--growl':
                growl = true;
                break;
            case '-s':
            case '--serial':
                serial = true;
                break;
            default:
                if (/\.js$/.test(arg)) {
                    files.push(arg);
                }
                break;
        }
    }
Colorized sys.error(). ## param: String str |
    function print(str){
        sys.error(colorize(str));
    }
Colorize the given string using ansi-escape sequences. Disabled when --boring is set. ## param: String str return: String |
    function colorize(str){
        var colors = { bold: 1, red: 31, green: 32, yellow: 33 };
        return str.replace(/\[(\w+)\]\{([^]*?)\}/g, function(_, color, str){
            return boring
                ? str
                : '\x1B[' + colors[color] + 'm' + str + '\x1B[0m';
        });
    }

    // Alias deepEqual as eql for complex equality

    assert.eql = assert.deepEqual;
Assert that `val` is null. ## param: Mixed val param: String msg |
    assert.isNull = function(val, msg) {
        assert.strictEqual(null, val, msg);
    };
Assert that `val` is not null. ## param: Mixed val param: String msg |
    assert.isNotNull = function(val, msg) {
        assert.notStrictEqual(null, val, msg);
    };
Assert that `val` is undefined. ## param: Mixed val param: String msg |
    assert.isUndefined = function(val, msg) {
        assert.strictEqual(undefined, val, msg);
    };
Assert that `val` is not undefined. ## param: Mixed val param: String msg |
    assert.isDefined = function(val, msg) {
        assert.notStrictEqual(undefined, val, msg);
    };
Assert that `obj` is `type`. ## param: Mixed obj param: String type api: public |
    assert.type = function(obj, type, msg){
        var real = typeof obj;
        msg = msg || 'typeof ' + sys.inspect(obj) + ' is ' + real + ', expected ' + type;
        assert.ok(type === real, msg);
    };
Assert that `str` matches `regexp`. ## param: String str param: RegExp regexp param: String msg |
    assert.match = function(str, regexp, msg) {
        msg = msg || sys.inspect(str) + ' does not match ' + sys.inspect(regexp);
        assert.ok(regexp.test(str), msg);
    };
Assert that `val` is within `obj`. ## Examples assert.includes('foobar', 'bar'); assert.includes(['foo', 'bar'], 'foo'); ## param: String | Array obj param: Mixed val param: String msg |
    assert.includes = function(obj, val, msg) {
        msg = msg || sys.inspect(obj) + ' does not include ' + sys.inspect(val);
        assert.ok(obj.indexOf(val) &gt;= 0, msg);
    };
Assert length of `val` is `n`. ## param: Mixed val param: Number n param: String msg |
    assert.length = function(val, n, msg) {
        msg = msg || sys.inspect(val) + ' has length of ' + val.length + ', expected ' + n;
        assert.equal(n, val.length, msg);
    };
Assert response from `server` with the given `req` object and `res` assertions object. ## param: Server server param: Object req param: Object | Function res param: String msg |
    assert.response = function(server, req, res, msg){
        // Callback as third or fourth arg
        var callback = typeof res === 'function'
            ? res
            : typeof msg === 'function'
                ? msg
                : function(){};

        // Default messate to test title
        if (typeof msg === 'function') msg = null;
        msg = msg || assert.testTitle;
        msg += '. ';

        // Pending responses
        server.__pending = server.__pending || 0;
        server.__pending++;

        // Create client
        if (!server.fd) {
            server.listen(server.__port = port++, '127.0.0.1');
            server.client = http.createClient(server.__port);
        }

        // Issue request
        var timer,
            client = server.client,
            method = req.method || 'GET',
            status = res.status || res.statusCode,
            data = req.data || req.body,
            timeout = req.timeout || 0;

        var request = client.request(method, req.url, req.headers);

        // Timeout
        if (timeout) {
            timer = setTimeout(function(){
                --server.__pending || server.close();
                delete req.timeout;
                assert.fail(msg + 'Request timed out after ' + timeout + 'ms.');
            }, timeout);
        }

        if (data) request.write(data);
        request.addListener('response', function(response){
            response.body = '';
            response.setEncoding('utf8');
            response.addListener('data', function(chunk){ response.body += chunk; });
            response.addListener('end', function(){
                --server.__pending || server.close();
                if (timer) clearTimeout(timer);

                // Assert response body
                if (res.body !== undefined) {
                    assert.equal(
                        response.body,
                        res.body,
                        msg + 'Invalid response body.\n'
                            + '    Expected: ' + sys.inspect(res.body) + '\n'
                            + '    Got: ' + sys.inspect(response.body)
                    );
                }

                // Assert response status
                if (typeof status === 'number') {
                    assert.equal(
                        response.statusCode,
                        status,
                        msg + colorize('Invalid response status code.\n'
                            + '    Expected: [green]{' + status + '}\n'
                            + '    Got: [red]{' + response.statusCode + '}')
                    );
                }

                // Assert response headers
                if (res.headers) {
                    var keys = Object.keys(res.headers);
                    for (var i = 0, len = keys.length; i &lt; len; ++i) {
                        var name = keys[i],
                            actual = response.headers[name.toLowerCase()],
                            expected = res.headers[name];
                        assert.equal(
                            actual,
                            expected,
                            msg + colorize('Invalid response header [bold]{' + name + '}.\n'
                                + '    Expected: [green]{' + expected + '}\n'
                                + '    Got: [red]{' + actual + '}')
                        );
                    }
                }

                // Callback
                callback(response);
            });
        });
        request.end();
    };
Pad the given string to the maximum width provided. ## param: String str param: Number width return: String |
    function lpad(str, width) {
        str = String(str);
        var n = width - str.length;
        if (n &lt; 1) return str;
        while (n--) str = ' ' + str;
        return str;
    }
Pad the given string to the maximum width provided. ## param: String str param: Number width return: String |
    function rpad(str, width) {
        str = String(str);
        var n = width - str.length;
        if (n &lt; 1) return str;
        while (n--) str = str + ' ';
        return str;
    }
Report test coverage. ## param: Object cov |
    function reportCoverage(cov) {
        populateCoverage(cov);
        // Stats
        print('\n   [bold]{Test Coverage}\n');
        var sep = '   +------------------------------------------+----------+------+------+--------+',
            lastSep = '                                              +----------+------+------+--------+';
        sys.puts(sep);
        sys.puts('   | filename                                 | coverage | LOC  | SLOC | missed |');
        sys.puts(sep);
        for (var name in cov) {
            var file = cov[name];
            if (Array.isArray(file)) {
                sys.print('   | ' + rpad(name, 40));
                sys.print(' | ' + lpad(file.coverage.toFixed(2), 8));
                sys.print(' | ' + lpad(file.LOC, 4));
                sys.print(' | ' + lpad(file.SLOC, 4));
                sys.print(' | ' + lpad(file.totalMisses, 6));
                sys.print(' |\n');
            }
        }
        sys.puts(sep);
        sys.print('     ' + rpad('', 40));
        sys.print(' | ' + lpad(cov.coverage.toFixed(2), 8));
        sys.print(' | ' + lpad(cov.LOC, 4));
        sys.print(' | ' + lpad(cov.SLOC, 4));
        sys.print(' | ' + lpad(cov.totalMisses, 6));
        sys.print(' |\n');
        sys.puts(lastSep);
        // Source
        for (var name in cov) {
            if (name.match(/\.js$/)) {
                var file = cov[name];
                print('\n   [bold]{' + name + '}:');
                print(file.source);
                sys.print('\n');
            }
        }
    }
Populate code coverage data. ## param: Object cov |
    function populateCoverage(cov) {
        cov.LOC =
        cov.SLOC =
        cov.totalFiles =
        cov.totalHits =
        cov.totalMisses =
        cov.coverage = 0;
        for (var name in cov) {
            var file = cov[name];
            if (Array.isArray(file)) {
                // Stats
                ++cov.totalFiles;
                cov.totalHits += file.totalHits = coverage(file, true);
                cov.totalMisses += file.totalMisses = coverage(file, false);
                file.totalLines = file.totalHits + file.totalMisses;
                cov.SLOC += file.SLOC = file.totalLines;
                if (!file.source) file.source = [];
                cov.LOC += file.LOC = file.source.length;
                file.coverage = (file.totalHits / file.totalLines) * 100;
                // Source
                var width = file.source.length.toString().length;
                file.source = file.source.map(function(line, i){
                    ++i;
                    var hits = file[i] === 0 ? 0 : (file[i] || ' ');
                    if (!boring) {
                        if (hits === 0) {
                            hits = '\x1b[31m' + hits + '\x1b[0m';
                            line = '\x1b[41m' + line + '\x1b[0m';
                        } else {
                            hits = '\x1b[32m' + hits + '\x1b[0m';
                        }
                    }
                    return '\n     ' + lpad(i, width) + ' | ' + hits + ' | ' + line;
                }).join('');
            }
        }
        cov.coverage = (cov.totalHits / cov.SLOC) * 100;
    }
Total coverage for the given file data. ## param: Array data return: Type |
    function coverage(data, val) {
        var n = 0;
        for (var i = 0, len = data.length; i &lt; len; ++i) {
            if (data[i] !== undefined &amp;&amp; data[i] == val) ++n;
        }
        return n;
    }
Run the given test `files`, or try test/*. ## param: Array files |
    function run(files) {
        if (!files.length) {
            try {
                files = fs.readdirSync('test').map(function(file){
                    return 'test/' + file;
                });
            } catch (err) {
                print('\n  failed to load tests in [bold]{./test}\n');
                ++failures;
                process.exit(1);
            }
        }
        if (watch) watchFiles(files);
        runFiles(files);
    }
Show the cursor when `show` is true, otherwise hide it. ## param: Boolean show |
    function cursor(show) {
        if (show) {
            sys.print('\x1b[?25h');
        } else {
            sys.print('\x1b[?25l');
        }
    }
Run the given test `files`. ## param: Array files |
    function runFiles(files) {
        files.forEach(runFile);
    }
Run tests for the given `file`. ## param: String file |
    function runFile(file) {
        if (file.match(/\.js$/)) {
            var title = path.basename(file),
                file = path.join(cwd, file),
                mod = require(file.replace(/\.js$/, ''));
            (function check(){
               var len = Object.keys(mod).length;
               if (len) {
                   runSuite(title, mod);
               } else {
                   setTimeout(check, 20);
               }
            })();
        }
    }
Clear the module cache for the given `file`. ## param: String file |
    function clearCache(file) {
        var keys = Object.keys(module.moduleCache);
        for (var i = 0, len = keys.length; i &lt; len; ++i) {
            var key = keys[i];
            if (key.indexOf(file) === key.length - file.length) {
                delete module.moduleCache[key];
            }
        }
    }
Watch the given `files` for changes. ## param: Array files |
    function watchFiles(files) {
        var p = 0,
            c = ['▫   ', '▫▫  ', '▫▫▫ ', ' ▫▫▫',
                 '  ▫▫', '   ▫', '   ▫', '  ▫▫',
                 '▫▫▫ ', '▫▫  ', '▫   '],
            l = c.length;
        cursor(false);
        setInterval(function(){
            sys.print(colorize('  [green]{' + c[p++ % l] + '} watching\r'));
        }, 100);
        files.forEach(function(file){
            fs.watchFile(file, { interval: 100 }, function(curr, prev){
                if (curr.mtime &gt; prev.mtime) {
                    print('  [yellow]{◦} ' + file);
                    clearCache(file);
                    runFile(file);
                }
            });
        });
    }
Report `err` for the given `test` and `suite`. ## param: String suite param: String test param: Error err |
    function error(suite, test, err) {
        ++failures;
        var name = err.name,
            stack = err.stack.replace(err.name, ''),
            label = test === 'uncaught'
                ? test
                : suite + ' ' + test;
        print('\n   [bold]{' + label + '}: [red]{' + name + '}' + stack + '\n');
        if (watch) notify(label + ' failed');
    }
Run the given tests. ## param: String title param: Object tests |
    var dots = 0;
    function runSuite(title, tests) {
        // Keys
        var keys = only.length
            ? only.slice(0)
            : Object.keys(tests);

        // Setup
        var setup = tests.setup || function(fn){ fn(); };

        // Iterate tests
        (function next(){
            if (keys.length) {
                var key,
                    test = tests[key = keys.shift()];
                // Non-tests
                if (key === 'setup') return next();

                // Run test
                if (test) {
                    try {
                        ++testcount;
                        assert.testTitle = key;
                        if (serial) {
                            if (!watch) {
                                sys.print('.');
                                if (++dots % 25 === 0) sys.print('\n');
                            }
                            setup(function(){
                                if (test.length &lt; 2) {
                                    test(assert);
                                    next();
                                } else {
                                    var id = setTimeout(function(){
                                        throw new Error(&quot;'" + key + "' timed out&quot;);
                                    }, 2000);
                                    test(assert, function(){
                                        clearTimeout(id);
                                        next();
                                    });
                                }
                            });
                        } else {
                            test(assert, function(fn){
                                process.addListener('beforeExit', function(){
                                    try {
                                        fn();
                                    } catch (err) {
                                        error(title, key, err);
                                    }
                                });
                            });
                        }
                    } catch (err) {
                        error(title, key, err);
                    }
                }
                if (!serial) next();
            }
        })();
    }
Report exceptions. |
    function report() {
        process.emit('beforeExit');
        if (failures) {
            print('\n   [bold]{Failures}: [red]{' + failures + '}\n\n');
            notify('Failures: ' + failures);
        } else {
            if (serial) print('');
            print('\n   [green]{100%} ' + testcount + ' tests\n');
            notify('100% ok');
        }
        if (typeof _$jscoverage === 'object') {
            reportCoverage(_$jscoverage);
        }
    }
Growl notify the given `msg`. ## param: String msg |
    function notify(msg) {
        if (growl) {
            childProcess.exec('growlnotify -name Expresso -m "' + msg + '"');
        }
    }

    // Report uncaught exceptions

    process.addListener('uncaughtException', function(err){
        error('uncaught', 'uncaught', err);
    });

    // Show cursor

    ['INT', 'TERM', 'QUIT'].forEach(function(sig){
        process.addListener('SIG' + sig, function(){
            cursor(true);
            process.exit(1);
        });
    });

    // Report test coverage when available
    // and emit "beforeExit" event to perform
    // final assertions

    var orig = process.emit;
    process.emit = function(event){
        if (event === 'exit') {
            report();
            process.reallyExit(failures);
        }
        orig.apply(this, arguments);
    };

    // Run test files

    if (!defer) run(files);