tests/testharness.js
author Art Barstow <art.barstow@nokia.com>
Sat, 08 Dec 2012 17:00:36 -0500
changeset 164 7f5c482f6f02
parent 148 22c2e3d43b31
permissions -rw-r--r--
Record of the overall status of the test suite
/*
Distributed under both the W3C Test Suite License [1] and the W3C
3-clause BSD License [2]. To contribute to a W3C Test Suite, see the
policies and contribution forms [3].

[1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license
[2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license
[3] http://www.w3.org/2004/10/27-testcases
*/

/*
 * == Introduction ==
 *
 * This file provides a framework for writing testcases. It is intended to
 * provide a convenient API for making common assertions, and to work both
 * for testing synchronous and asynchronous DOM features in a way that
 * promotes clear, robust, tests.
 *
 * == Basic Usage ==
 *
 * To use this file, import the script into the test document:
 * <script src="http://w3c-test.org/resources/testharness.js"></script>
 *
 * Within each file one may define one or more tests. Each test is atomic
 * in the sense that a single test has a single result (pass/fail/timeout).
 * Within each test one may have a number of asserts. The test fails at the
 * first failing assert, and the remainder of the test is (typically) not run.
 *
 * If the file containing the tests is a HTML file with an element of id "log"
 * this will be populated with a table containing the test results after all
 * the tests have run.
 *
 * == Synchronous Tests ==
 *
 * To create a synchronous test use the test() function:
 *
 * test(test_function, name, properties)
 *
 * test_function is a function that contains the code to test. For example a
 * trivial passing test would be:
 *
 * test(function() {assert_true(true)}, "assert_true with true")
 *
 * The function passed in is run in the test() call.
 *
 * properties is an object that overrides default test properties. The recognised properties
 * are:
 *    timeout - the test timeout in ms
 *
 * e.g.
 * test(test_function, "Sample test", {timeout:1000})
 *
 * would run test_function with a timeout of 1s.
 *
 * == Asynchronous Tests ==
 *
 * Testing asynchronous features is somewhat more complex since the result of
 * a test may depend on one or more events or other callbacks. The API provided
 * for testing these features is indended to be rather low-level but hopefully
 * applicable to many situations.
 *
 * To create a test, one starts by getting a Test object using async_test:
 *
 * async_test(name, properties)
 *
 * e.g.
 * var t = async_test("Simple async test")
 *
 * Assertions can be added to the test by calling the step method of the test
 * object with a function containing the test assertions:
 *
 * t.step(function() {assert_true(true)});
 *
 * When all the steps are complete, the done() method must be called:
 *
 * t.done();
 *
 * The properties argument is identical to that for test().
 *
 * == Making assertions ==
 *
 * Functions for making assertions start assert_
 * The best way to get a list is to look in this file for functions names
 * matching that pattern. The general signature is
 *
 * assert_something(actual, expected, description)
 *
 * although not all assertions precisely match this pattern e.g. assert_true
 * only takes actual and description as arguments.
 *
 * The description parameter is used to present more useful error messages when
 * a test fails
 *
 * == Setup ==
 *
 * Sometimes tests require non-trivial setup that may fail. For this purpose
 * there is a setup() function, that may be called with one or two arguments.
 * The two argument version is:
 *
 * setup(func, properties)
 *
 * The one argument versions may omit either argument.
 * func is a function to be run synchronously. setup() becomes a no-op once
 * any tests have returned results. Properties are global properties of the test
 * harness. Currently recognised properties are:
 *
 * timeout - The time in ms after which the harness should stop waiting for
 *           tests to complete (this is different to the per-test timeout
 *           because async tests do not start their timer until .step is called)
 *
 * explicit_done - Wait for an explicit call to done() before declaring all tests
 *                 complete (see below)
 *
 * == Determining when all tests are complete ==
 *
 * By default the test harness will assume there are no more results to come
 * when:
 * 1) There are no Test objects that have been created but not completed
 * 2) The load event on the document has fired
 *
 * This behaviour can be overridden by setting the explicit_done property to true
 * in a call to setup(). If explicit_done is true, the test harness will not assume
 * it is done until the global done() function is called. Once done() is called, the
 * two conditions above apply like normal.
 *
 * == Generating tests ==
 *
 * NOTE: this functionality may be removed
 *
 * There are scenarios in which is is desirable to create a large number of
 * (synchronous) tests that are internally similar but vary in the parameters
 * used. To make this easier, the generate_tests function allows a single
 * function to be called with each set of parameters in a list:
 *
 * generate_tests(test_function, parameter_lists)
 *
 * For example:
 *
 * generate_tests(assert_equals, [
 *     ["Sum one and one", 1+1, 2],
 *     ["Sum one and zero", 1+0, 1]
 *     ])
 *
 * Is equivalent to:
 *
 * test(function() {assert_equals(1+1, 2)}, "Sum one and one")
 * test(function() {assert_equals(1+0, 1)}, "Sum one and zero")
 *
 * Note that the first item in each parameter list corresponds to the name of
 * the test.
 *
 * == Callback API ==
 *
 * The framework provides callbacks corresponding to 3 events:
 *
 * start - happens when the first Test is created
 * result - happens when a test result is recieved
 * complete - happens when all results are recieved
 *
 * The page defining the tests may add callbacks for these events by calling
 * the following methods:
 *
 *   add_start_callback(callback) - callback called with no arguments
 *   add_result_callback(callback) - callback called with a test argument
 *   add_completion_callback(callback) - callback called with an array of tests
 *                                       and an status object
 *
 * tests have the following properties:
 *   status: A status code. This can be compared to the PASS, FAIL, TIMEOUT and
 *           NOTRUN properties on the test object
 *   message: A message indicating the reason for failure. In the future this
 *            will always be a string
 *
 *  The status object gives the overall status of the harness. It has the
 *  following properties:
 *    status: Can be compared to the OK, ERROR and TIMEOUT properties
 *    message: An error message set when the status is ERROR
 *
 * == External API ==
 *
 * In order to collect the results of multiple pages containing tests, the test
 * harness will, when loaded in a nested browsing context, attempt to call
 * certain functions in the top level browsing context:
 *
 * start - top.start_callback
 * result - top.result_callback
 * complete - top.completion_callback
 *
 * These are given the same arguments as the corresponding internal callbacks
 * described above.
 *
 * == List of assertions ==
 *
 * assert_true(actual, description)
 *   asserts that /actual/ is strictly true
 *
 * assert_false(actual, description)
 *   asserts that /actual/ is strictly false
 *
 * assert_equals(actual, expected, description)
 *   asserts that /actual/ is strictly equal to /expected/
 *
 * assert_array_equals(actual, expected, description)
 *   asserts that /actual/ and /expected/ have the same length and the value of
 *   each indexed property in /actual/ is the strictly equal to the corresponding
 *   property value in /expected/
 *
 * assert_regexp_match(actual, expected, description)
 *   asserts that /actual/ matches the regexp /expected/
 *
 * assert_exists(object, property_name, description)
 *   asserts that object has an own property property_name
 *
 * assert_not_exists(object, property_name, description)
 *   assert that object does not have own property property_name
 *
 * assert_inherits(object, property_name, description)
 *   assert that object does not have an own property named property_name
 *   but that property_name is present in the prototype chain for object
 *
 * assert_readonly(object, property_name, description)
 *   assert that property property_name on object is readonly
 *
 * assert_throws(code_or_object, func, description)
 *   code_or_object - either a DOM error code as a string e.g. "NAMESPACE_ERR"
 *                    or an object that should be thrown
 *   func - a function that should throw
 *
 *   assert that func throws either a DOMException with error code
 *   code_or_object (if code_or_object is a string) or that it throws an object
 *   code_or_object
 *
 * assert_unreached(description)
 *   asserts if called. Used to ensure that some codepath is *not* taken e.g.
 *   an event does not fire.
 *
 */

(function ()
{
    var debug = false;
    // default timeout is 5 seconds, test can override if needed
    var default_timeout = 5000;
    var default_test_timeout = 2000;

    /*
    * API functions
    */

    var name_counter = 0;
    function next_default_name()
    {
        //Don't use document.title to work around an Opera bug in XHTML documents
        var prefix = document.getElementsByTagName("title").length > 0 ?
                         document.getElementsByTagName("title")[0].firstChild.data :
                         "Untitled";
        var suffix = name_counter > 0 ? " " + name_counter : "";
        name_counter++;
        return prefix + suffix;
    }

    function test(func, name, properties)
    {
        var test_name = name ? name : next_default_name();
        properties = properties ? properties : {};
        var test_obj = new Test(test_name, properties);
        test_obj.step(func);
        if (test_obj.status === test_obj.NOTRUN) {
            test_obj.done();
        }
    }

    function async_test(name, properties)
    {
        var test_name = name ? name : next_default_name();
        properties = properties ? properties : {};
        var test_obj = new Test(test_name, properties);
        return test_obj;
    }

    function setup(func_or_properties, properties_or_func)
    {
        var func = null;
        var properties = {};
        if (arguments.length === 2) {
            func = func_or_properties;
            properties = properties_or_func;
        } else if (func_or_properties instanceof Function){
            func = func_or_properties;
        } else {
            properties = func_or_properties;

        }
        tests.setup(func, properties);
    }

    function done() {
        tests.end_wait();
    }

    function generate_tests(func, args) {
        forEach(args, function(x)
                {
                    var name = x[0];
                    test(function()
                         {
                             func.apply(this, x.slice(1));
                         }, name);
                });
    }

    function on_event(object, event, callback)
    {
      object.addEventListener(event, callback, false);
    }

    expose(test, 'test');
    expose(async_test, 'async_test');
    expose(generate_tests, 'generate_tests');
    expose(setup, 'setup');
    expose(done, 'done');
    expose(on_event, 'on_event');

    /*
    * Convert a value to a nice, human-readable string
    */
    function format_value(val)
    {
        if (val === null)
        {
            // typeof is object, so the switch isn't useful
            return "null";
        }
        // In JavaScript, -0 === 0 and String(-0) == "0", so we have to
        // special-case.
        if (val === -0 && 1/val === -Infinity)
        {
            return "-0";
        }
        // Special-case Node objects, since those come up a lot in my tests.  I
        // ignore namespaces.
        if (typeof val == "object" && val instanceof Node)
        {
            switch (val.nodeType)
            {
            case Node.ELEMENT_NODE:
                var ret = "Element node <";
                if (val.namespaceURI == "http://www.w3.org/1999/xhtml" || val.namespaceURI === null)
                {
                    ret += val.tagName.toLowerCase();
                }
                else
                {
                    ret += val.tagName;
                }
                for (var i = 0; i < val.attributes.length; i++)
                {
                    ret += " " + val.attributes[i].name + "=" + format_value(val.attributes[i].value);
                }
                ret += ">";
                return ret;
            case Node.TEXT_NODE:
                return "Text node with data " + format_value(val.data) + " and parent " + format_value(val.parentNode);
            case Node.PROCESSING_INSTRUCTION_NODE:
                return "ProcessingInstruction node with target " + format_value(val.target) + " and data " + format_value(val.data);
            case Node.COMMENT_NODE:
                return "Comment node with data " + format_value(val.data);
            case Node.DOCUMENT_NODE:
                return "Document node";
            case Node.DOCUMENT_TYPE_NODE:
                return "DocumentType node";
            case Node.DOCUMENT_FRAGMENT_NODE:
                return "DocumentFragment node";
            default:
                return "Node object of unknown type";
            }
        }
        switch (typeof val)
        {
        case "string":
            for (var i = 0; i < 32; i++)
            {
                var replace = "\\";
                switch (i) {
                case 0: replace += "0"; break;
                case 1: replace += "x01"; break;
                case 2: replace += "x02"; break;
                case 3: replace += "x03"; break;
                case 4: replace += "x04"; break;
                case 5: replace += "x05"; break;
                case 6: replace += "x06"; break;
                case 7: replace += "x07"; break;
                case 8: replace += "b"; break;
                case 9: replace += "t"; break;
                case 10: replace += "n"; break;
                case 11: replace += "v"; break;
                case 12: replace += "f"; break;
                case 13: replace += "r"; break;
                case 14: replace += "x0e"; break;
                case 15: replace += "x0f"; break;
                case 16: replace += "x10"; break;
                case 17: replace += "x11"; break;
                case 18: replace += "x12"; break;
                case 19: replace += "x13"; break;
                case 20: replace += "x14"; break;
                case 21: replace += "x15"; break;
                case 22: replace += "x16"; break;
                case 23: replace += "x17"; break;
                case 24: replace += "x18"; break;
                case 25: replace += "x19"; break;
                case 26: replace += "x1a"; break;
                case 27: replace += "x1b"; break;
                case 28: replace += "x1c"; break;
                case 29: replace += "x1d"; break;
                case 30: replace += "x1e"; break;
                case 31: replace += "x1f"; break;
                }
                val = val.replace(String.fromCharCode(i), replace);
            }
            return '"' + val.replace('"', '\\"') + '"';
        case "boolean":
        case "undefined":
        case "number":
            return String(val);
        default:
            return typeof val + ' "' + val + '"';
        }
    }

    /*
    * Assertions
    */

    function assert_true(actual, description)
    {
        var message = make_message("assert_true", description,
                                   "expected true got ${actual}", {actual:String(actual)});
        assert(actual === true, message);
    };
    expose(assert_true, "assert_true");

    function assert_false(actual, description)
    {
        var message = make_message("assert_false", description,
                                   "expected false got ${actual}", {actual:String(actual)});
        assert(actual === false, message);
    };
    expose(assert_false, "assert_false");

    function assert_equals(actual, expected, description)
    {
         /*
          * Test if two primitives are equal or two objects
          * are the same object
          */
         var message = make_message("assert_equals", description,
                                    "expected ${expected} but got ${actual}",
                                    {expected:format_value(expected), actual:format_value(actual)});
         if (expected !== expected)
         {
             //NaN case
             assert(actual !== actual, message);
         }
         else
         {
             //typical case
             assert(actual === expected, message);
         }
    };
    expose(assert_equals, "assert_equals");

    function assert_object_equals(actual, expected, description)
    {
         //This needs to be improved a great deal
         function check_equal(expected, actual, stack)
         {
             stack.push(actual);

             for (p in actual)
             {
                 var message = make_message(
                     "assert_object_equals", description,
                     "unexpected property ${p}", {p:p});

                 assert(expected.hasOwnProperty(p), message);

                 if (typeof actual[p] === "object" && actual[p] !== null)
                 {
                     if (stack.indexOf(actual[p]) === -1)
                     {
                         check_equal(actual[p], expected[p], stack);
                     }
                 }
                 else
                 {
                     message = make_message(
                         "assert_object_equals", description,
                         "property ${p} expected ${expected} got ${actual}",
                         {p:p, expected:expected, actual:actual});

                     assert(actual[p] === expected[p], message);
                 }
             }
             for (p in expected)
             {
                 var message = make_message(
                     "assert_object_equals", description,
                     "expected property ${p} missing", {p:p});

                 assert(actual.hasOwnProperty(p), message);
             }
             stack.pop();
         }
         check_equal(actual, expected, []);
    };
    expose(assert_object_equals, "assert_object_equals");

    function assert_array_equals(actual, expected, description)
    {
        var message = make_message(
            "assert_array_equals", description,
            "lengths differ, expected ${expected} got ${actual}",
            {expected:expected.length, actual:actual.length});

        assert(actual.length === expected.length, message);

        for (var i=0; i < actual.length; i++)
        {
            message = make_message(
                "assert_array_equals", description,
                "property ${i}, property expected to be $expected but was $actual",
                {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
                 actual:actual.hasOwnProperty(i) ? "present" : "missing"});
            assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), message);
            message = make_message(
                          "assert_array_equals", description,
                          "property ${i}, expected ${expected} but got ${actual}",
                          {i:i, expected:expected[i], actual:actual[i]});
            assert(expected[i] === actual[i], message);
        }
    }
    expose(assert_array_equals, "assert_array_equals");

    function assert_regexp_match(actual, expected, description) {
        /*
         * Test if a string (actual) matches a regexp (expected)
         */
        var message = make_message("assert_regexp_match", description,
                                   "expected ${expected} but got ${actual}",
                                   {expected:String(expected), actual:String(actual)});
        assert(expected.test(actual), message);
    }
    expose(assert_regexp_match, "assert_regexp_match");

    function assert_exists(object, property_name, description)
    {
         var message = make_message(
             "assert_exists", description,
             "expected property ${p} missing", {p:property_name});

         assert(object.hasOwnProperty(property_name), message);
    };
    expose(assert_exists, "assert_exists");

    function assert_not_exists(object, property_name, description)
    {
         var message = make_message(
             "assert_not_exists", description,
             "unexpected property ${p} found", {p:property_name});

         assert(!object.hasOwnProperty(property_name), message);
    };
    expose(assert_not_exists, "assert_not_exists");

  function assert_inherits(object, property_name, description)
    {
         var message = make_message(
             "assert_inherits", description,
             "property ${p} found on object expected in prototype chain",
           {p:property_name});
         assert(!object.hasOwnProperty(property_name), message);

         message = make_message(
             "assert_inherits", description,
             "property ${p} not found in prototype chain",
           {p:property_name});
         assert(property_name in object, message);
    };
    expose(assert_inherits, "assert_inherits");

    function assert_readonly(object, property_name, description)
    {
         var initial_value = object[property_name];
         try {
             var message = make_message(
                 "assert_readonly", description,
                 "deleting property ${p} succeeded", {p:property_name});
             assert(delete object[property_name] === false, message);
             assert(object[property_name] === initial_value, message);
             //Note that this can have side effects in the case where
             //the property has PutForwards
             object[property_name] = initial_value + "a"; //XXX use some other value here?
             message = make_message("assert_readonly", description,
                                    "changing property ${p} succeeded",
                                    {p:property_name});
             assert(object[property_name] === initial_value, message);
         }
         finally
         {
             object[property_name] = initial_value;
         }
    };
    expose(assert_readonly, "assert_readonly");

    function assert_throws(code_or_object, func, description)
    {
        try
        {
            func.call(this);
            assert(false, make_message("assert_throws", description,
                                      "${func} did not throw", {func:String(func)}));
        }
        catch(e)
        {
            if (e instanceof AssertionError) {
                throw(e);
            }
            if (typeof code_or_object === "string")
            {
                assert(e[code_or_object] !== undefined &&
                       e.code === e[code_or_object],
                       make_message("assert_throws", description,
                           [["{text}", "${func} threw with"] ,
                            function()
                            {
                                var actual_name;
                                for (var p in DOMException)
                                {
                                    if (e.code === DOMException[p])
                                    {
                                        actual_name = p;
                                        break;
                                    }
                                }
                                if (actual_name)
                                {
                                    return ["{text}", " code " + actual_name + " (${actual_number})"];
                                }
                                else
                                {
                                    return ["{text}", " error number ${actual_number}"];
                                }
                            },
                            ["{text}"," expected ${expected}"],
                            function()
                            {
                                return e[code_or_object] ?
                                    ["{text}", " (${expected_number})"] : null;
                            }
                           ],
                                    {func:String(func), actual_number:e.code,
                                     expected:String(code_or_object),
                                     expected_number:e[code_or_object]}));
                assert(e instanceof DOMException,
                      make_message("assert_throws", description,
                                   "thrown exception ${exception} was not a DOMException",
                                  {exception:String(e)}));
            }
            else
            {
                assert(e instanceof Object && "name" in e && e.name == code_or_object.name,
                       make_message("assert_throws", description,
                           "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})",
                                    {func:String(func), actual:String(e), actual_name:e.name,
                                     expected:String(code_or_object),
                                     expected_name:code_or_object.name}));
            }
        }
    }
    expose(assert_throws, "assert_throws");

    function assert_unreached(description) {
         var message = make_message("assert_unreached", description,
                                    "Reached unreachable code");

         assert(false, message);
    }
    expose(assert_unreached, "assert_unreached");

    function Test(name, properties)
    {
        this.name = name;
        this.status = this.NOTRUN;
        this.timeout_id = null;
        this.is_done = false;

        this.timeout_length = properties.timeout ? properties.timeout : default_test_timeout;

        this.message = null;

        var this_obj = this;
        this.steps = [];

        tests.push(this);
    }

    Test.prototype = {
        PASS:0,
        FAIL:1,
        TIMEOUT:2,
        NOTRUN:3
    };


    Test.prototype.step = function(func, this_obj)
    {
        //In case the test has already failed
        if (this.status !== this.NOTRUN)
        {
          return;
        }

        tests.started = true;

        if (this.timeout_id === null) {
            this.set_timeout();
        }

        this.steps.push(func);

        try
        {
            func.apply(this_obj);
        }
        catch(e)
        {
            //This can happen if something called synchronously invoked another
            //step
            if (this.status !== this.NOTRUN)
            {
                return;
            }
            this.status = this.FAIL;
            this.message = e.message;
            this.done();
            if (debug && e.constructor !== AssertionError) {
                throw e;
            }
        }
    };

    Test.prototype.set_timeout = function()
    {
        var this_obj = this;
        this.timeout_id = setTimeout(function()
                                     {
                                         this_obj.timeout();
                                     }, this.timeout_length);
    };

    Test.prototype.timeout = function()
    {
        this.status = this.TIMEOUT;
        this.timeout_id = null;
        this.message = "Test timed out";
        this.done();
    };

    Test.prototype.done = function()
    {
        if (this.is_done) {
            return;
        }
        clearTimeout(this.timeout_id);
        if (this.status === this.NOTRUN)
        {
            this.status = this.PASS;
        }
        this.is_done = true;
        tests.result(this);
    };


   /*
    * Harness
    */

    function TestsStatus()
    {
        this.status = null;
        this.message = null;
    }
    TestsStatus.prototype = {
        OK:0,
        ERROR:1,
        TIMEOUT:2
    };

    function Tests()
    {
        this.tests = [];
        this.num_pending = 0;

        this.phases = {
            INITIAL:0,
            SETUP:1,
            HAVE_TESTS:2,
            HAVE_RESULTS:3,
            COMPLETE:4
        };
        this.phase = this.phases.INITIAL;

        //All tests can't be done until the load event fires
        this.all_loaded = false;
        this.wait_for_finish = false;
        this.processing_callbacks = false;

        this.timeout_length = default_timeout;
        this.timeout_id = null;
        this.set_timeout();

        this.start_callbacks = [];
        this.test_done_callbacks = [];
        this.all_done_callbacks = [];

        this.status = new TestsStatus();

        var this_obj = this;

        on_event(window, "load",
                 function()
                 {
                     this_obj.all_loaded = true;
                     if (this_obj.all_done())
                     {
                         this_obj.complete();
                     }
                 });
    }

    Tests.prototype.setup = function(func, properties)
    {
        if (this.phase >= this.phases.HAVE_RESULTS) {
            return;
        }
        if (this.phase < this.phases.SETUP) {
            this.phase = this.phases.SETUP;
        }

        if (properties.timeout)
        {
            this.timeout_length = properties.timeout;
            this.set_timeout();
        }
        if (properties.explicit_done)
        {
            this.wait_for_finish = true;
        }

        if (func)
        {
            try
            {
                func();
            } catch(e)
            {
                this.status.status = this.status.ERROR;
                this.status.message = e;
            };
        }
    };

    Tests.prototype.set_timeout = function()
    {
        var this_obj = this;
        clearTimeout(this.timeout_id);
        this.timeout_id = setTimeout(function() {
                                         this_obj.timeout();
                                     }, this.timeout_length);
    };

    Tests.prototype.timeout = function() {
        this.status.status = this.status.TIMEOUT;
        this.complete();
    };

    Tests.prototype.end_wait = function()
    {
        this.wait_for_finish = false;
        if (this.all_done()) {
            this.complete();
        }
    };

    Tests.prototype.push = function(test)
    {
        if (this.phase < this.phases.HAVE_TESTS) {
            this.notify_start();
        }
        this.num_pending++;
        this.tests.push(test);
    };

    Tests.prototype.all_done = function() {
        return (this.all_loaded && this.num_pending === 0 &&
                !this.wait_for_finish && !this.processing_callbacks);
    };

    Tests.prototype.start = function() {
        this.phase = this.phases.HAVE_TESTS;
        this.notify_start();
    };

    Tests.prototype.notify_start = function() {
        var this_obj = this;
        forEach (this.start_callbacks,
                 function(callback)
                 {
                     callback(this_obj);
                 });
        if(top !== window && top.start_callback)
        {
            try
            {
                top.start_callback.call(this_obj);
            }
            catch(e)
            {
                if (debug)
                {
                    throw(e);
                }
            }
        }
    };

    Tests.prototype.result = function(test)
    {
        if (this.phase > this.phases.HAVE_RESULTS)
        {
            return;
        }
        this.phase = this.phases.HAVE_RESULTS;
        this.num_pending--;
        this.notify_result(test);
    };

    Tests.prototype.notify_result = function(test) {
        var this_obj = this;
        this.processing_callbacks = true;
        forEach(this.test_done_callbacks,
                function(callback)
                {
                    callback(test, this_obj);
                });

        if(top !== window && top.result_callback)
        {
            try
            {
                top.result_callback.call(this_obj, test);
            }
            catch(e)
            {
                if(debug) {
                    throw e;
                }
            }
        }
        this.processing_callbacks = false;
        if (this.all_done())
        {
            this.complete();
        }

    };

    Tests.prototype.complete = function() {
        if (this.phase === this.phases.COMPLETE) {
            return;
        }
        this.phase = this.phases.COMPLETE;
        this.notify_complete();
    };

    Tests.prototype.notify_complete = function()
    {
        clearTimeout(this.timeout_id);
        var this_obj = this;
        if (this.status.status === null)
        {
            this.status.status = this.status.OK;
        }

        forEach (this.all_done_callbacks,
                 function(callback)
                 {
                     callback(this_obj.tests, this_obj.status);
                 });
        if(top !== window && top.completion_callback)
        {
            try
            {
                top.completion_callback(this_obj.tests, this.status);
            }
            catch(e)
            {
                if (debug)
                {
                    throw e;
                }
            }

        }
    };

    var tests = new Tests();
    add_completion_callback(output_results);


    function add_start_callback(callback) {
        tests.start_callbacks.push(callback);
    }

    function add_result_callback(callback)
    {
        tests.test_done_callbacks.push(callback);
    }

    function add_completion_callback(callback)
    {
       tests.all_done_callbacks.push(callback);
    }

    expose(add_start_callback, 'add_start_callback');
    expose(add_result_callback, 'add_result_callback');
    expose(add_completion_callback, 'add_completion_callback');

    /*
     * Output listener
    */

    (function show_status() {
        var done_count = 0;
         function on_done(test, tests) {
             var log = document.getElementById("log");
             done_count++;
             if (log)
             {
                 if (log.lastChild) {
                     log.removeChild(log.lastChild);
                 }
                 var nodes = render([["{text}", "Running, ${done} complete"],
                                 function() {
                                     if (tests.all_done) {
                                         return ["{text}", " ${pending} remain"];
                                     } else {
                                         return null;
                                     }
                                 }
                                    ], {done:done_count,
                                        pending:tests.num_pending});
                 forEach(nodes, function(node) {
                             log.appendChild(node);
                         });
                 log.normalize();
             }
         }
         if (document.getElementById("log"))
         {
             add_result_callback(on_done);
         }
     })();

    function output_results(tests, harness_status)
    {
        var log = document.getElementById("log");
        if (!log)
        {
            return;
        }
        while (log.lastChild)
        {
            log.removeChild(log.lastChild);
        }
        var prefix = null;
        var scripts = document.getElementsByTagName("script");
        for (var i=0; i<scripts.length; i++)
        {
            var src = scripts[i].src;
            if (src.slice(src.length - "testharness.js".length) === "testharness.js")
            {
                prefix = src.slice(0, src.length - "testharness.js".length);
                break;
            }
        }
        if (prefix != null) {
            var stylesheet = document.createElement("link");
            stylesheet.setAttribute("rel", "stylesheet");
            stylesheet.setAttribute("href", prefix + "testharness.css");
            var heads = document.getElementsByTagName("head");
            if (heads) {
                heads[0].appendChild(stylesheet);
            }
        }

        var status_text = {};
        status_text[Test.prototype.PASS] = "Pass";
        status_text[Test.prototype.FAIL] = "Fail";
        status_text[Test.prototype.TIMEOUT] = "Timeout";
        status_text[Test.prototype.NOTRUN] = "Not Run";

        var status_number = {};
        forEach(tests, function(test) {
                    var status = status_text[test.status];
                    if (status_number.hasOwnProperty(status))
                    {
                        status_number[status] += 1;
                    } else {
                        status_number[status] = 1;
                    }
                });

        function status_class(status)
        {
            return status.replace(/\s/g, '').toLowerCase();
        }

        var summary_template = ["section", {"id":"summary"},
                                ["h2", {}, "Summary"],
                                ["p", {}, "Found ${num_tests} tests"],
                                function(vars) {
                                    var rv = [["div", {}]];
                                    var i=0;
                                    while (status_text.hasOwnProperty(i)) {
                                        if (status_number.hasOwnProperty(status_text[i])) {
                                            var status = status_text[i];
                                            rv[0].push(["div", {"class":status_class(status)},
                                                        ["input", {type:"checkbox", checked:"checked"}],
                                                       status_number[status] + " " + status]);
                                        }
                                        i++;
                                    }
                                    return rv;
                                }];

        log.appendChild(render(summary_template, {num_tests:tests.length}));

        forEach(document.querySelectorAll("section#summary input"),
                function(element)
                {
                    on_event(element, "click",
                             function(e)
                             {
                                 if (document.getElementById("results") === null)
                                 {
                                     e.preventDefault();
                                     return;
                                 }
                                 var result_class = element.parentNode.getAttribute("class");
                                 var checked = element.checked;
                                 forEach(document.querySelectorAll("table#results > tbody > tr > td."+result_class),
                                         function(cell)
                                         {
                                             cell.parentNode.style.display = checked ? "" : "None";
                                         });
                             });
                });

        var template = ["section", {},
                        ["h2", {}, "Details"],
                        ["table", {"id":"results"},
                        ["tr", {},
                         ["th", {}, "Result"],
                         ["th", {}, "Test Name"],
                         ["th", {}, "Message"]
                        ],
                        ["tbody", {},
                        function(vars) {
                            var rv = map(vars.tests, function(test) {
                                             var status = status_text[test.status];
                                             return  ["tr", {},
                                                      ["td", {"class":status_class(status)}, status],
                                                      ["td", {}, test.name],
                                                      ["td", {}, test.message ? test.message : " "]
                                                     ];
                                         });
                            return rv;
                        }]
                       ]];

        log.appendChild(render(template, {tests:tests}));

    }


    /*
     * Template code
     *
     * A template is just a javascript structure. An element is represented as:
     *
     * [tag_name, {attr_name:attr_value}, child1, child2]
     *
     * the children can either be strings (which act like text nodes), other templates or
     * functions (see below)
     *
     * A text node is represented as
     *
     * ["{text}", value]
     *
     * String values have a simple substitution syntax; ${foo} represents a variable foo.
     *
     * It is possible to embed logic in templates by using a function in a place where a
     * node would usually go. The function must either return part of a template or null.
     *
     * In cases where a set of nodes are required as output rather than a single node
     * with children it is possible to just use a list
     * [node1, node2, node3]
     *
     * Usage:
     *
     * render(template, substitutions) - take a template and an object mapping
     * variable names to parameters and return either a DOM node or a list of DOM nodes
     *
     * substitute(template, substitutions) - take a template and variable mapping object,
     * make the variable substitutions and return the substituted template
     *
     */

    function is_single_node(template)
    {
        return typeof template[0] === "string";
    }

    function substitute(template, substitutions)
    {
        if (typeof template === "function") {
            var replacement = template(substitutions);
            if (replacement)
            {
                var rv = substitute(replacement, substitutions);
                return rv;
            }
            else
            {
                return null;
            }
        }
        else if (is_single_node(template))
        {
            return substitute_single(template, substitutions);
        }
        else
        {
            return filter(map(template, function(x) {
                                  return substitute(x, substitutions);
                              }), function(x) {return x !== null;});
        }
    }
    expose(substitute, "template.substitute");

    function substitute_single(template, substitutions)
    {
        var substitution_re = /\${([^ }]*)}/g;

        function do_substitution(input) {
            var components = input.split(substitution_re);
            var rv = [];
            for (var i=0; i<components.length; i+=2)
            {
                rv.push(components[i]);
                if (components[i+1])
                {
                    rv.push(String(substitutions[components[i+1]]));
                }
            }
            return rv;
        }

        var rv = [];
        rv.push(do_substitution(String(template[0])).join(""));

        if (template[0] === "{text}") {
            substitute_children(template.slice(1), rv);
        } else {
            substitute_attrs(template[1], rv);
            substitute_children(template.slice(2), rv);
        }

        function substitute_attrs(attrs, rv)
        {
            rv[1] = {};
            for (var name in template[1])
            {
                if (attrs.hasOwnProperty(name))
                {
                    var new_name = do_substitution(name).join("");
                    var new_value = do_substitution(attrs[name]).join("");
                    rv[1][new_name] = new_value;
                };
            }
        }

        function substitute_children(children, rv)
        {
            for (var i=0; i<children.length; i++)
            {
                if (children[i] instanceof Object) {
                    var replacement = substitute(children[i], substitutions);
                    if (replacement !== null)
                    {
                        if (is_single_node(replacement))
                        {
                            rv.push(replacement);
                        }
                        else
                        {
                            extend(rv, replacement);
                        }
                    }
                }
                else
                {
                    extend(rv, do_substitution(String(children[i])));
                }
            }
            return rv;
        }

        return rv;
    }

    function make_dom_single(template)
    {
        if (template[0] === "{text}")
        {
            var element = document.createTextNode("");
            for (var i=1; i<template.length; i++)
            {
                element.data += template[i];
            }
        }
        else
        {
            var element = document.createElement(template[0]);
            for (var name in template[1]) {
                if (template[1].hasOwnProperty(name))
                {
                    element.setAttribute(name, template[1][name]);
                }
            }
            for (var i=2; i<template.length; i++)
            {
                if (template[i] instanceof Object)
                {
                    var sub_element = make_dom(template[i]);
                    element.appendChild(sub_element);
                }
                else
                {
                    var text_node = document.createTextNode(template[i]);
                    element.appendChild(text_node);
                }
            }
        }

        return element;
    }



    function make_dom(template, substitutions)
    {
        if (is_single_node(template))
        {
            return make_dom_single(template);
        }
        else
        {
            return map(template, function(x) {
                           return make_dom_single(x);
                       });
        }
    }

    function render(template, substitutions)
    {
        return make_dom(substitute(template, substitutions));
    }
    expose(render, "template.render");

    /*
     * Utility funcions
     */
    function assert(expected_true, message)
    {
        if (expected_true !== true)
        {
            throw new AssertionError(message);
        }
    }

    function AssertionError(message)
    {
        this.message = message;
    }

    function make_message(function_name, description, error, substitutions)
    {
        var message = substitute([["span", {"class":"assert"}, "${function_name}:"],
                                  function()
                                  {
                                      if (description) {
                                          return ["span", {"class":"description"}, description];
                                      } else {
                                          return null;
                                      }
                                  },
                                  ["div", {"class":"error"}, error]
                                 ], merge({function_name:function_name},
                                         substitutions));

        return message;
    }

    function filter(array, callable, thisObj) {
        var rv = [];
        for (var i=0; i<array.length; i++)
        {
            if (array.hasOwnProperty(i))
            {
                var pass = callable.call(thisObj, array[i], i, array);
                if (pass) {
                    rv.push(array[i]);
                }
            }
        }
        return rv;
    }

    function map(array, callable, thisObj)
    {
        var rv = [];
        rv.length = array.length;
        for (var i=0; i<array.length; i++)
        {
            if (array.hasOwnProperty(i))
            {
                rv[i] = callable.call(thisObj, array[i], i, array);
            }
        }
        return rv;
    }

    function extend(array, items)
    {
        Array.prototype.push.apply(array, items);
    }

    function forEach (array, callback, thisObj)
    {
        for (var i=0; i<array.length; i++)
        {
            if (array.hasOwnProperty(i))
            {
                callback.call(thisObj, array[i], i, array);
            }
        }
    }

    function merge(a,b)
    {
        var rv = {};
        var p;
        for (p in a)
        {
            rv[p] = a[p];
        }
        for (p in b) {
            rv[p] = b[p];
        }
        return rv;
    }

    function expose(object, name)
    {
        var components = name.split(".");
        var target = window;
        for (var i=0; i<components.length - 1; i++)
        {
            if (!(components[i] in target))
            {
                target[components[i]] = {};
            }
            target = target[components[i]];
        }
        target[components[components.length - 1]] = object;
    }

})();