/*****************************************************************************
 *
 * Copyright (c) 2003-2004 EcmaUnit Contributors. All rights reserved.
 *
 * This software is distributed under the terms of the EcmaUnit
 * License. See LICENSE.txt for license text. For a list of EcmaUnit
 * Contributors see CREDITS.txt.
 *
 *****************************************************************************/

// $Id: ecmaunit.js 14611 2005-07-13 09:14:38Z guido $

/*
   Object-oriented prototype-based unit test suite
*/

function TestCase() {
    /* a single test case */
    this.name = 'TestCase';

    this.initialize = function(reporter) {
        // this array's contents will be displayed when done (if it
        // contains anything)
        this._exceptions = new Array();
        this._reporter = reporter;
    };

    this.setUp = function() {
        /* this will be called on before each test method that is ran */
    };

    this.tearDown = function() {
        /* this will be called after each test method that has been ran */
    };

    this.assertEquals = function(var1, var2, message) {
        /* assert whether 2 vars have the same value */
        if (!message)  {
            message = '';
        } else {
            message = "'" + message + "' ";
        }
        if (var1 && var1.toSource && var2 && var2.toSource) {
            if (var1.toSource() != var2.toSource()) {
                this._throwException('Assertion ' + message + 'failed: ' + 
                                        var1 + ' != ' + var2);
            };
        } else {
            if (var1 != var2) {
                this._throwException('Assertion ' + message + 'failed: ' + 
                                        var1 + ' != ' + var2);
            };
        };
    };

    this.assertNotEquals = function(var1, var2, message) {
        /* assert whether 2 vars have different values */
        if (!message)  {
            message = '';
        } else {
            message = "'" + message + "' ";
        }
        if (var1 && var1.toSource && var2 && var2.toSource) {
            if (var1.toSource() == var2.toSource()) {
                this._throwException('Assertion ' + message + 'failed: ' + 
                                        var1 + ' == ' + var2);
            };
        } else {
            if (var1 == var2) {
                this._throwException('Assertion ' + message + 'failed: ' + 
                                        var1 + ' == ' + var2);
            };
        };
    };

    this.debug = function(msg) {
        this._reporter.debug(msg);
    }
    this.assert = function(statement, message) {
        /* assert whether a variable resolves to true */
        if (!statement) {
            if (!message) message = (statement && statement.toString) ? 
                                        statement.toString() : statement;
            this._throwException('Assertion \'' + message + '\' failed');
        };
    };

    this.assertTrue = this.assert;

    this.assertFalse = function(statement, message) {
        /* assert whether a variable resolves to false */
        if (statement) {
            if (!message) message = statement.toString ? 
                    statement.toString() : statement;
            this._throwException('AssertFalse \'' + message + '\' failed');
        };
    };

    this.assertThrows = function(func, exception, context) {
        /* assert whether a certain exception is raised */
        if (!context) {
            context = null;
        };
        var exception_thrown = false;
        // remove the first three args, they're the function's normal args
        var args = [];
        for (var i=3; i < arguments.length; i++) {
            args.push(arguments[i]);
        };
        try {
            func.apply(context, args);
        } catch(e) {
            // allow catching undefined exceptions too
            if (exception === undefined) {
            } else if (exception) {
                var isinstance = false;
                try {
                    if (e instanceof exception) {
                        isinstance = true;
                    };
                } catch(f) {
                };
                if (!isinstance) {
                    if (exception.toSource && e.toSource) {
                        exception = exception.toSource();
                        e = e.toSource();
                    };
                    if (exception.toString && e.toString) {
                        exception = exception.toString();
                        e = e.toString();
                    };
                    if (e != exception) {
                        this._throwException('Function threw the wrong ' +
                                'exception ' + e.toString() + 
                                ', while expecting ' + exception.toString());
                    };
                };
            };
            exception_thrown = true;
        };
        if (!exception_thrown) {
            if (exception) {
                this._throwException("function didn\'t raise exception \'" + 
                                        exception.toString() + "'");
            } else {
                this._throwException('function didn\'t raise exception');
            };
        };
    };

    this.runTests = function() {
        /* find all methods of which the name starts with 'test'
            and call them */
        var ret = this._runHelper();
	this._reporter.summarize(ret[0], ret[1], this._exceptions);
    };

    this._runHelper = function() {
        /* this actually runs the tests
            return value is an array [total tests ran, total time spent (ms)]
        */
        var now = new Date();
        var starttime = now.getTime();
        var numtests = 0;
        for (var attr in this) {
            if (attr.substr(0, 4) == 'test') {
                this.setUp();
                try {
                    this[attr]();
                    this._reporter.reportSuccess(this.name, attr);
                } catch(e) {
                    var raw = e;
                    if (e.name && e.message) { // Microsoft
                        e = e.name + ': ' + e.message;
                    }
                    this._reporter.reportError(this.name, attr, e, raw);
                    this._exceptions.push(new Array(this.name, attr, e, raw));
                };
                this.tearDown();
                numtests++;
            };
        };
        var now = new Date();
        var totaltime = now.getTime() - starttime;
        return new Array(numtests, totaltime);
    };

    this._throwException = function(message) {
        var lineno = this._getLineNo();
        if (lineno) {
            message = 'line ' + lineno + ' - ' + message;
        };
        throw(message);
    };

    this._getLineNo = function() {
        /* tries to get the line no in Moz */
        var stack = undefined;
        try {notdefined()} catch(e) {stack = e.stack};
        if (stack) {
            stack = stack.toString().split('\n');
            for (var i=0; i < stack.length; i++) {
                var line = stack[i].split('@')[1];
                if (line.indexOf('ecmaunit') == -1) {
                    // return the first line after we get out of ecmaunit
                    var chunks = line.split(':');
                    var lineno = chunks[chunks.length - 1];
                    if (lineno != '0') {
                        return lineno;
                    };
                };
            };
        } else {
            return false;
        };
    };
};

function TestSuite(reporter) {
    /* run a suite of tests */
    this._reporter = reporter;
    this._tests = new Array();
    this._exceptions = new Array();
    
    this.registerTest = function(test) {
        /* register a test */
        if (!test) {
            throw('TestSuite.registerTest() requires a testcase as argument');
        };
        this._tests.push(test);
    };

    this.runSuite = function() {
        /* run the suite */
        var now = new Date();
        var starttime = now.getTime();
        var testsran = 0;
        for (var i=0; i < this._tests.length; i++) {
            var test = new this._tests[i]();
            test.initialize(this._reporter);
            testsran += test._runHelper()[0];
            // the TestCase class handles output of dots and Fs, but we
            // should take care of the exceptions
            if (test._exceptions.length) {
                for (var j=0; j < test._exceptions.length; j++) {
                    // attr, exc in the org array, so here it becomes
                    // name, attr, exc
                    var excinfo = test._exceptions[j];
                    this._exceptions.push(excinfo);
                };
            };
        };
        var now = new Date();
        var totaltime = now.getTime() - starttime;
        this._reporter.summarize(testsran, totaltime, this._exceptions);
    };
};

function StdoutReporter(verbose) {
    this.verbose = verbose;
    this.debug = function(text) {
        print(text+"\n");
    }

    this.reportSuccess = function(testcase, attr) {
        /* report a test success */
        if (this.verbose) {
            print(testcase + '.' + attr + '(): OK');
        } else {
            print('.');
        };
    };

    this.reportError = function(testcase, attr, exception, raw) {
        /* report a test failure */
        if (this.verbose) {
            print(testcase + '.' + attr + '(): FAILED!');
        } else {
            print('F');
        };
    };

    this.summarize = function(numtests, time, exceptions) {
        print('\n' + numtests + ' tests ran in ' + time / 1000.0 + 
                ' seconds\n');
        if (exceptions.length) {
            for (var i=0; i < exceptions.length; i++) {
                var testcase = exceptions[i][0];
                var attr = exceptions[i][1];
                var exception = exceptions[i][2];
                var raw = exceptions[i][3];
                print(testcase + '.' + attr + ', exception: ' + exception);
                if (verbose) {
                    this._printStackTrace(raw);
                };
            };
            print('NOT OK!');
        } else {
            print('OK!');
        };
    };

    this._printStackTrace = function(exc) {
        if (!exc.stack) {
            print('no stacktrace available');
            return;
        };
        var lines = exc.stack.toString().split('\n');
        var toprint = [];
        for (var i=0; i < lines.length; i++) {
            var line = lines[i];
            if (line.indexOf('ecmaunit.js') > -1) {
                // remove useless bit of traceback
                break;
            };
            if (line.charAt(0) == '(') {
                line = 'function' + line;
            };
            var chunks = line.split('@');
            toprint.push(chunks);
        };
        toprint.reverse();
        for (var i=0; i < toprint.length; i++) {
            print('  ' + toprint[i][1]);
            print('    ' + toprint[i][0]);
        };
        print();
    };
};

function HTMLReporter(outputelement, verbose) {
    this.outputelement = outputelement;
    this.document = outputelement.ownerDocument;
    this.verbose = verbose;

    this.debug = function(text) {
        var msg = this.document.createTextNode(text);
        var div = this.document.createElement('div');
        div.appendChild(msg);
        this.outputelement.appendChild(div);
    }
    this.reportSuccess = function(testcase, attr) {
        /* report a test success */
        // a single dot looks rather small
        var dot = this.document.createTextNode('+');
        this.outputelement.appendChild(dot);
    };

    this.reportError = function(testcase, attr, exception, raw) {
        /* report a test failure */
        var f = this.document.createTextNode('F');
        this.outputelement.appendChild(f);
    };

    this.summarize = function(numtests, time, exceptions) {
        /* write the result output to the html node */
        var p = this.document.createElement('p');
        var text = this.document.createTextNode(numtests + ' tests ran in ' + 
                                                time / 1000.0 + ' seconds');
        p.appendChild(text);
        this.outputelement.appendChild(p);
        if (exceptions.length) {
            for (var i=0; i < exceptions.length; i++) {
                var testcase = exceptions[i][0];
                var attr = exceptions[i][1];
                var exception = exceptions[i][2].toString();
                var raw = exceptions[i][3];
                var div = this.document.createElement('div');
                var lines = exception.toString().split('\n');
                var text = this.document.createTextNode(
                    testcase + '.' + attr + ', exception ');
                div.appendChild(text);
                // add some formatting for Opera: this browser displays nice
                // tracebacks...
                for (var j=0; j < lines.length; j++) {
                    var text = lines[j];
                    if (j > 0) {
                        text = '\xa0\xa0\xa0\xa0' + text;
                    };
                    div.appendChild(this.document.createTextNode(text));
                    div.appendChild(this.document.createElement('br'));
                };
                div.style.color = 'red';
                this.outputelement.appendChild(div);
                if (this.verbose) {
                    // display stack trace on Moz
                    this._displayStackTrace(raw);
                };
            };
            var div = this.document.createElement('div');
            var text = this.document.createTextNode('NOT OK!');
            div.appendChild(text);
            div.style.backgroundColor = 'red';
            div.style.color = 'black';
            div.style.fontWeight = 'bold';
            div.style.textAlign = 'center';
            div.style.marginTop = '1em';
            this.outputelement.appendChild(div);
        } else {
            var div = this.document.createElement('div');
            var text = this.document.createTextNode('OK!');
            div.appendChild(text);
            div.style.backgroundColor = 'lightgreen';
            div.style.color = 'black';
            div.style.fontWeight = 'bold';
            div.style.textAlign = 'center';
            div.style.marginTop = '1em';
            this.outputelement.appendChild(div);
        };
    };

    this._displayStackTrace = function(exc) {
        /*
        if (arguments.caller) {
            // IE
            var caller = arguments;
            toprint = [];
            while (caller) {
                var callee = caller.callee.toString();
                callee = callee.replace('\n', '').replace(/\s+/g, ' ');
                var funcsig = /(.*?)\s*\{/.exec(callee)[1];
                var args = caller.callee.arguments;
                var displayargs = [];
                for (var i=0; i < args.length; i++) {
                    displayargs.push(args[i].toString());
                };
                toprint.push((funcsig + ' - (' + displayargs + ')'));
                caller = caller.caller;
            };
            toprint.reverse();
            var pre = this.document.createElement('pre');
            for (var i=0; i < toprint.length; i++) {
                pre.appendChild(document.createTextNode(toprint[i]));
                pre.appendChild(document.createElement('br'));
            };
            this.outputelement.appendChild(pre);
        };
        */
        if (exc.stack) {
            // Moz (sometimes)
            var lines = exc.stack.toString().split('\n');
            var toprint = []; // need to reverse this before outputting
            for (var i=0; i < lines.length; i++) {
                var line = lines[i];
                if (line.indexOf('ecmaunit.js') > -1) {
                    // remove useless bit of traceback
                    break;
                };
                if (line[0] == '(') {
                    line = 'function' + line;
                };
                line = line.split('@');
                toprint.push(line);
            };
            toprint.reverse();
            var pre = this.document.createElement('pre');
            for (var i=0; i < toprint.length; i++) {
                pre.appendChild(
                    this.document.createTextNode(
                        '  ' + toprint[i][1] + '\n    ' + toprint[i][0] + '\n'
                    )
                );
            };
            pre.appendChild(document.createTextNode('\n'));
            this.outputelement.appendChild(pre);
        };
    };
};
