Bug 885878 - Update testharness.js; r=jgraham
authorMs2ger <ms2ger@gmail.com>
Thu, 28 Nov 2013 15:07:56 +0100
changeset 157992 dc71a9c74cabb9a4833159ba0fd25e1de9e9664d
parent 157991 4acc9992a56c7feeb43e38140ad2229dcf4b5a3f
child 157993 4779e661432a87143bdf4659d8012127d34c8419
push id25730
push userMs2ger@gmail.com
push dateFri, 29 Nov 2013 07:40:20 +0000
treeherdermozilla-central@733f30f7093b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjgraham
bugs885878
milestone28.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 885878 - Update testharness.js; r=jgraham
dom/imptests/failures/editing/selecttest/test_interfaces.html.json
dom/imptests/idlharness.js
dom/imptests/testharness.js
--- a/dom/imptests/failures/editing/selecttest/test_interfaces.html.json
+++ b/dom/imptests/failures/editing/selecttest/test_interfaces.html.json
@@ -1,11 +1,12 @@
 {
   "Selection interface: existence and properties of interface object":true,
   "Selection interface: existence and properties of interface prototype object":true,
+  "Selection interface object length":true,
   "Selection interface: existence and properties of interface prototype object's \"constructor\" property":true,
   "Selection interface: attribute anchorNode":true,
   "Selection interface: attribute anchorOffset":true,
   "Selection interface: attribute focusNode":true,
   "Selection interface: attribute focusOffset":true,
   "Selection interface: attribute isCollapsed":true,
   "Selection interface: attribute rangeCount":true,
   "Selection interface: calling collapse(Node,unsigned long) on getSelection() with too few arguments must throw TypeError":true,
--- a/dom/imptests/idlharness.js
+++ b/dom/imptests/idlharness.js
@@ -9,17 +9,17 @@ policies and contribution forms [3].
 */
 
 /*
  * This file automatically generates browser tests for WebIDL interfaces, using
  * the testharness.js framework.  To use, first include the following:
  *
  *   <script src=/resources/testharness.js></script>
  *   <script src=/resources/testharnessreport.js></script>
- *   <script src=/resources/webidl2.js></script>
+ *   <script src=/resources/WebIDLParser.js></script>
  *   <script src=/resources/idlharness.js></script>
  *
  * Then you'll need some type of IDLs.  Here's some script that can be run on a
  * spec written in HTML, which will grab all the elements with class="idl",
  * concatenate them, and replace the body so you can copy-paste:
  *
      var s = "";
      [].forEach.call(document.getElementsByClassName("idl"), function(idl) {
@@ -299,31 +299,31 @@ IdlArray.prototype.internal_add_idls = f
 
         case "dictionary":
             // Nothing to test, but we need the dictionary info around for type
             // checks
             this.members[parsed_idl.name] = new IdlDictionary(parsed_idl);
             break;
 
         case "typedef":
-            // TODO
-            console.log("typedef not yet supported");
+            this.members[parsed_idl.name] = new IdlTypedef(parsed_idl);
             break;
 
         case "callback":
             // TODO
             console.log("callback not yet supported");
             break;
 
         case "enum":
             this.members[parsed_idl.name] = new IdlEnum(parsed_idl);
             break;
 
-        case "callback":
+        case "callback interface":
             // TODO
+            console.log("callback interface not yet supported");
             break;
 
         default:
             throw parsed_idl.name + ": " + parsed_idl.type + " not yet supported";
         }
     }.bind(this));
 };
 
@@ -538,16 +538,18 @@ IdlArray.prototype.assert_type_is = func
 
         case "unsigned long long":
             assert_equals(typeof value, "number");
             assert_true(0 <= value, "unsigned long long is negative");
             return;
 
         case "float":
         case "double":
+        case "unrestricted float":
+        case "unrestricted double":
             // TODO: distinguish these cases
             assert_equals(typeof value, "number");
             return;
 
         case "DOMString":
             assert_equals(typeof value, "string");
             return;
 
@@ -579,16 +581,20 @@ IdlArray.prototype.assert_type_is = func
     else if (this.members[type] instanceof IdlEnum)
     {
         assert_equals(typeof value, "string");
     }
     else if (this.members[type] instanceof IdlDictionary)
     {
         // TODO: Test when we actually have something to test this on
     }
+    else if (this.members[type] instanceof IdlTypedef)
+    {
+        // TODO: Test when we actually have something to test this on
+    }
     else
     {
         throw "Type " + type + " isn't an interface or dictionary";
     }
 };
 //@}
 
 /// IdlObject ///
@@ -1137,51 +1143,56 @@ IdlInterface.prototype.test_self = funct
                 window[this.name]();
             }.bind(this), "interface object didn't throw TypeError when called as a function");
             assert_throws(new TypeError(), function() {
                 new window[this.name]();
             }.bind(this), "interface object didn't throw TypeError when called as a constructor");
         }
     }.bind(this), this.name + " interface: existence and properties of interface object");
 
-    if (this.has_extended_attribute("Constructor"))
-    {
-        test(function()
-        {
+    if (!this.is_callback()) {
+        test(function() {
+            // This function tests WebIDL as of 2013-08-25.
+            // http://dev.w3.org/2006/webapi/WebIDL/#es-interface-call
+
             assert_own_property(window, this.name,
                                 "window does not have own property " + format_value(this.name));
 
-            // "Interface objects for interfaces declared with a [Constructor]
-            // extended attribute must have a property named “length” with
-            // attributes { [[Writable]]: false, [[Enumerable]]: false,
-            // [[Configurable]]: false } whose value is a Number determined as
-            // follows: . . .
-            // "Return the length of the shortest argument list of the entries
-            // in S."
-            // TODO: Variadic constructors.  Should generalize this so that it
-            // works for testing operation length too (currently we just don't
-            // support multiple operations with the same identifier).
-            var expected_length = this.extAttrs
-                .filter(function(attr) { return attr.name == "Constructor"; })
-                .map(function(attr) {
-                    return attr.arguments ? attr.arguments.filter(
-                        function(arg) {
-                            return !arg.optional;
-                        }).length : 0;
-                })
-                .reduce(function(m, n) { return Math.min(m, n); });
+            // "Interface objects for non-callback interfaces MUST have a
+            // property named “length” with attributes { [[Writable]]: false,
+            // [[Enumerable]]: false, [[Configurable]]: false } whose value is
+            // a Number."
             assert_own_property(window[this.name], "length");
-            assert_equals(window[this.name].length, expected_length, "wrong value for " + this.name + ".length");
             var desc = Object.getOwnPropertyDescriptor(window[this.name], "length");
             assert_false("get" in desc, this.name + ".length has getter");
             assert_false("set" in desc, this.name + ".length has setter");
             assert_false(desc.writable, this.name + ".length is writable");
             assert_false(desc.enumerable, this.name + ".length is enumerable");
             assert_false(desc.configurable, this.name + ".length is configurable");
-        }.bind(this), this.name + " interface constructor");
+
+            var constructors = this.extAttrs
+                .filter(function(attr) { return attr.name == "Constructor"; });
+            var expected_length;
+            if (!constructors.length) {
+                // "If the [Constructor] extended attribute, does not appear on
+                // the interface definition, then the value is 0."
+                expected_length = 0;
+            } else {
+                // "Otherwise, the value is determined as follows: . . .
+                // "Return the length of the shortest argument list of the
+                // entries in S."
+                expected_length = constructors.map(function(attr) {
+                    return attr.arguments ? attr.arguments.filter(function(arg) {
+                        return !arg.optional;
+                    }).length : 0;
+                })
+                .reduce(function(m, n) { return Math.min(m, n); });
+            }
+            assert_equals(window[this.name].length, expected_length, "wrong value for " + this.name + ".length");
+        }.bind(this), this.name + " interface object length");
     }
 
     // TODO: Test named constructors if I find any interfaces that have them.
 
     test(function()
     {
         assert_own_property(window, this.name,
                             "window does not have own property " + format_value(this.name));
@@ -1372,26 +1383,34 @@ IdlInterface.prototype.test_members = fu
                 continue;
             }
             test(function()
             {
                 assert_own_property(window, this.name,
                                     "window does not have own property " + format_value(this.name));
                 assert_own_property(window[this.name], "prototype",
                                     'interface "' + this.name + '" does not have own property "prototype"');
-                assert_true(member.name in window[this.name].prototype,
-                            "The prototype object must have a property " +
-                            format_value(member.name));
 
-                // TODO: Needs to test for LenientThis.
-                assert_throws(new TypeError(), function() {
-                    window[this.name].prototype[member.name];
-                }.bind(this), "getting property on prototype object must throw TypeError");
+                if (member["static"]) {
+                    assert_own_property(window[this.name], member.name,
+                        "The interface object must have a property " +
+                        format_value(member.name));
+                }
+                else
+                {
+                    assert_true(member.name in window[this.name].prototype,
+                        "The prototype object must have a property " +
+                        format_value(member.name));
 
-                do_interface_attribute_asserts(window[this.name].prototype, member);
+                    // TODO: Needs to test for LenientThis.
+                    assert_throws(new TypeError(), function() {
+                        window[this.name].prototype[member.name];
+                    }.bind(this), "getting property on prototype object must throw TypeError");
+                    do_interface_attribute_asserts(window[this.name].prototype, member);
+                }
             }.bind(this), this.name + " interface: attribute " + member.name);
         }
         else if (member.type == "operation")
         {
             // TODO: Need to correctly handle multiple operations with the same
             // identifier.
             if (!member.name)
             {
@@ -1415,61 +1434,71 @@ IdlInterface.prototype.test_members = fu
                 // "For each unique identifier of an operation defined on the
                 // interface, there must be a corresponding property on the
                 // interface prototype object (if it is a regular operation) or
                 // the interface object (if it is a static operation), unless
                 // the effective overload set for that identifier and operation
                 // and with an argument count of 0 (for the ECMAScript language
                 // binding) has no entries."
                 //
-                // TODO: The library doesn't seem to support static operations.
-                assert_own_property(window[this.name].prototype, member.name,
-                    "interface prototype object missing non-static operation");
+                var prototypeOrInterfaceObject;
+                if (member["static"]) {
+                    assert_own_property(window[this.name], member.name,
+                            "interface prototype object missing static operation");
+                    prototypeOrInterfaceObject = window[this.name];
+                }
+                else
+                {
+                    assert_own_property(window[this.name].prototype, member.name,
+                            "interface prototype object missing non-static operation");
+                    prototypeOrInterfaceObject = window[this.name].prototype;
+                }
 
-                var desc = Object.getOwnPropertyDescriptor(window[this.name].prototype, member.name);
+                var desc = Object.getOwnPropertyDescriptor(prototypeOrInterfaceObject, member.name);
                 // "The property has attributes { [[Writable]]: true,
                 // [[Enumerable]]: true, [[Configurable]]: true }."
                 assert_false("get" in desc, "property has getter");
                 assert_false("set" in desc, "property has setter");
                 assert_true(desc.writable, "property is not writable");
                 assert_true(desc.enumerable, "property is not enumerable");
                 assert_true(desc.configurable, "property is not configurable");
                 // "The value of the property is a Function object whose
                 // behavior is as follows . . ."
-                assert_equals(typeof window[this.name].prototype[member.name], "function",
+                assert_equals(typeof prototypeOrInterfaceObject[member.name], "function",
                               "property must be a function");
                 // "The value of the Function object’s “length” property is
                 // a Number determined as follows:
                 // ". . .
                 // "Return the length of the shortest argument list of the
                 // entries in S."
                 //
                 // TODO: Doesn't handle overloading or variadic arguments.
-                assert_equals(window[this.name].prototype[member.name].length,
+                assert_equals(prototypeOrInterfaceObject[member.name].length,
                     member.arguments.filter(function(arg) {
                         return !arg.optional;
                     }).length,
                     "property has wrong .length");
 
                 // Make some suitable arguments
                 var args = member.arguments.map(function(arg) {
                     return create_suitable_object(arg.idlType);
                 });
 
                 // "Let O be a value determined as follows:
                 // ". . .
                 // "Otherwise, throw a TypeError."
                 // This should be hit if the operation is not static, there is
                 // no [ImplicitThis] attribute, and the this value is null.
                 //
-                // TODO: We currently ignore the static and [ImplicitThis]
-                // cases.
-                assert_throws(new TypeError(), function() {
-                    window[this.name].prototype[member.name].apply(null, args);
-                }, "calling operation with this = null didn't throw TypeError");
+                // TODO: We currently ignore the [ImplicitThis] case.
+                if (!member["static"]) {
+                    assert_throws(new TypeError(), function() {
+                        window[this.name].prototype[member.name].apply(null, args);
+                    }, "calling operation with this = null didn't throw TypeError");
+                }
 
                 // ". . . If O is not null and is also not a platform object
                 // that implements interface I, throw a TypeError."
                 //
                 // TODO: Test a platform object that implements some other
                 // interface.  (Have to be sure to get inheritance right.)
                 assert_throws(new TypeError(), function() {
                     window[this.name].prototype[member.name].apply({}, args);
@@ -1591,56 +1620,64 @@ IdlInterface.prototype.test_interface_of
         || member.type == "attribute"
         || member.type == "operation")
         && member.name)
         {
             test(function()
             {
                 assert_equals(exception, null, "Unexpected exception when evaluating object");
                 assert_equals(typeof obj, expected_typeof, "wrong typeof object");
-                assert_inherits(obj, member.name);
-                if (member.type == "const")
-                {
-                    assert_equals(obj[member.name], constValue(member.value));
-                }
-                if (member.type == "attribute")
-                {
-                    // Attributes are accessor properties, so they might
-                    // legitimately throw an exception rather than returning
-                    // anything.
-                    var property, thrown = false;
-                    try
+                if (!member["static"]) {
+                    assert_inherits(obj, member.name);
+                    if (member.type == "const")
+                    {
+                        assert_equals(obj[member.name], constValue(member.value));
+                    }
+                    if (member.type == "attribute")
                     {
-                        property = obj[member.name];
-                    }
-                    catch (e)
-                    {
-                        thrown = true;
+                        // Attributes are accessor properties, so they might
+                        // legitimately throw an exception rather than returning
+                        // anything.
+                        var property, thrown = false;
+                        try
+                        {
+                            property = obj[member.name];
+                        }
+                        catch (e)
+                        {
+                            thrown = true;
+                        }
+                        if (!thrown)
+                        {
+                            this.array.assert_type_is(property, member.idlType);
+                        }
                     }
-                    if (!thrown)
+                    if (member.type == "operation")
                     {
-                        this.array.assert_type_is(property, member.idlType);
+                        assert_equals(typeof obj[member.name], "function");
                     }
                 }
-                if (member.type == "operation")
-                {
-                    assert_equals(typeof obj[member.name], "function");
-                }
             }.bind(this), this.name + " interface: " + desc + ' must inherit property "' + member.name + '" with the proper type (' + i + ')');
         }
         // TODO: This is wrong if there are multiple operations with the same
         // identifier.
         // TODO: Test passing arguments of the wrong type.
         if (member.type == "operation" && member.name && member.arguments.length)
         {
             test(function()
             {
                 assert_equals(exception, null, "Unexpected exception when evaluating object");
                 assert_equals(typeof obj, expected_typeof, "wrong typeof object");
-                assert_inherits(obj, member.name);
+                if (!member["static"]) {
+                    assert_inherits(obj, member.name);
+                }
+                else
+                {
+                    assert_false(member.name in obj);
+                }
                 var args = [];
                 for (var i = 0; i < member.arguments.length; i++)
                 {
                     if (member.arguments[i].optional)
                     {
                         break;
                     }
                     assert_throws(new TypeError(), function()
@@ -1780,16 +1817,17 @@ function create_suitable_object(type)
     {
         case "any":
         case "boolean":
             return true;
 
         case "byte": case "octet": case "short": case "unsigned short":
         case "long": case "unsigned long": case "long long":
         case "unsigned long long": case "float": case "double":
+        case "unrestricted float": case "unrestricted double":
             return 7;
 
         case "DOMString":
             return "foo";
 
         case "object":
             return {a: "b"};
 
@@ -1808,30 +1846,39 @@ function IdlEnum(obj)
     /**
      * obj is an object produced by the WebIDLParser.js "dictionary"
      * production.
      */
 
     /** Self-explanatory. */
     this.name = obj.name;
 
-    console.log("Name is " + this.name);
-
     /** An array of values produced by the "enum" production. */
     this.values = obj.values;
 
 }
 //@}
 
 IdlEnum.prototype = Object.create(IdlObject.prototype);
 
-IdlEnum.prototype.test = function()
+/// IdlTypedef ///
+// Used for IdlArray.prototype.assert_type_is
+function IdlTypedef(obj)
 //@{
 {
-            test(function()
-            {
-		// NOTHING to test
-		return;
-	    });
+    /**
+     * obj is an object produced by the WebIDLParser.js "typedef"
+     * production.
+     */
+
+    /** Self-explanatory. */
+    this.name = obj.name;
+
+    /** An array of values produced by the "typedef" production. */
+    this.values = obj.values;
+
 }
 //@}
+
+IdlTypedef.prototype = Object.create(IdlObject.prototype);
+
 }());
 // vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker:
--- a/dom/imptests/testharness.js
+++ b/dom/imptests/testharness.js
@@ -128,32 +128,41 @@ policies and contribution forms [3].
  *
  * The description parameter is used to present more useful error messages when
  * a test fails
  *
  * NOTE: All asserts must be located in a test() or a step of an async_test().
  *       asserts outside these places won't be detected correctly by the harness
  *       and may cause a file to stop testing.
  *
+ * == Harness Timeout ==
+ * 
+ * The overall harness admits two timeout values "normal" (the
+ * default) and "long", used for tests which have an unusually long
+ * runtime. After the timeout is reached, the harness will stop
+ * waiting for further async tests to complete. By default the
+ * timeouts are set to 10s and 60s, respectively, but may be changed
+ * when the test is run on hardware with different performance
+ * characteristics to a common desktop computer.  In order to opt-in
+ * to the longer test timeout, the test must specify a meta element:
+ * <meta name="timeout" content="long">
+ *
  * == 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)
  *
  * output_document - The document to which results should be logged. By default
  *                   this is the current document but could be an ancestor
  *                   document in some cases e.g. a SVG test loaded in an HTML
  *                   wrapper
@@ -162,16 +171,18 @@ policies and contribution forms [3].
  *                    when the timeout() function is called (typically for
  *                    use when integrating with some existing test framework
  *                    that has its own timeout mechanism).
  *
  * allow_uncaught_exception - don't treat an uncaught exception as an error;
  *                            needed when e.g. testing the window.onerror
  *                            handler.
  *
+ * timeout_multiplier - Multiplier to apply to per-test timeouts.
+ *
  * == 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
@@ -354,21 +365,22 @@ policies and contribution forms [3].
  * assert_not_exists(object, property_name, description)
  *   *** deprecated ***
  *   assert that object does not have own property property_name
  */
 
 (function ()
 {
     var debug = false;
-    // default timeout is 5 seconds, test can override if needed
+    // default timeout is 10 seconds, test can override if needed
     var settings = {
       output:true,
-      timeout:5000,
-      test_timeout:2000
+      harness_timeout:{"normal":10000,
+                       "long":60000},
+      test_timeout:null
     };
 
     var xhtml_ns = "http://www.w3.org/1999/xhtml";
 
     // script_prefix is used by Output.prototype.show_results() to figure out
     // where to get testharness.css from.  It's enclosed in an extra closure to
     // not pollute the library's namespace with variables like "src".
     var script_prefix = null;
@@ -410,17 +422,17 @@ policies and contribution forms [3].
     }
 
     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) {
+        if (test_obj.phase === test_obj.phases.STARTED) {
             test_obj.done();
         }
     }
 
     function async_test(func, name, properties)
     {
         if (typeof func !== "function") {
             properties = name;
@@ -489,30 +501,59 @@ policies and contribution forms [3].
     {
         if (s.length > len) {
             return s.substring(0, len - 3) + "...";
         }
         return s;
     }
 
     /*
+     * Return true if object is probably a Node object.
+     */
+    function is_node(object)
+    {
+        // I use duck-typing instead of instanceof, because
+        // instanceof doesn't work if the node is from another window (like an
+        // iframe's contentWindow):
+        // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295
+        if ("nodeType" in object
+        && "nodeName" in object
+        && "nodeValue" in object
+        && "childNodes" in object)
+        {
+            try
+            {
+                object.nodeType;
+            }
+            catch (e)
+            {
+                // The object is probably Node.prototype or another prototype
+                // object that inherits from it, and not a Node instance.
+                return false;
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /*
      * Convert a value to a nice, human-readable string
      */
     function format_value(val, seen)
     {
-	if (!seen) {
-	    seen = [];
+        if (!seen) {
+            seen = [];
         }
         if (typeof val === "object" && val !== null)
         {
             if (seen.indexOf(val) >= 0)
             {
                 return "[...]";
             }
-	    seen.push(val);
+            seen.push(val);
         }
         if (Array.isArray(val))
         {
             return "[" + val.map(function(x) {return format_value(x, seen)}).join(", ") + "]";
         }
 
         switch (typeof val)
         {
@@ -571,24 +612,18 @@ policies and contribution forms [3].
             return String(val);
         case "object":
             if (val === null)
             {
                 return "null";
             }
 
             // Special-case Node objects, since those come up a lot in my tests.  I
-            // ignore namespaces.  I use duck-typing instead of instanceof, because
-            // instanceof doesn't work if the node is from another window (like an
-            // iframe's contentWindow):
-            // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295
-            if ("nodeType" in val
-            && "nodeName" in val
-            && "nodeValue" in val
-            && "childNodes" in val)
+            // ignore namespaces.
+            if (is_node(val))
             {
                 switch (val.nodeType)
                 {
                 case Node.ELEMENT_NODE:
                     var ret = "<" + val.tagName.toLowerCase();
                     for (var i = 0; i < val.attributes.length; i++)
                     {
                         ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"';
@@ -1065,22 +1100,35 @@ policies and contribution forms [3].
             throw new AssertionError(errors.join("\n\n"));
         }
     }
     expose(assert_any, "assert_any");
 
     function Test(name, properties)
     {
         this.name = name;
+
+        this.phases = {
+            INITIAL:0,
+            STARTED:1,
+            HAS_RESULT:2,
+            COMPLETE:3
+        };
+        this.phase = this.phases.INITIAL;
+
         this.status = this.NOTRUN;
         this.timeout_id = null;
-        this.is_done = false;
 
         this.properties = properties;
-        this.timeout_length = properties.timeout ? properties.timeout : settings.test_timeout;
+        var timeout = properties.timeout ? properties.timeout : settings.test_timeout
+        if (timeout != null) {
+            this.timeout_length = timeout * tests.timeout_multiplier;
+        } else {
+            this.timeout_length = null;
+        }
 
         this.message = null;
 
         var this_obj = this;
         this.steps = [];
 
         tests.push(this);
     }
@@ -1106,60 +1154,59 @@ policies and contribution forms [3].
                 message:msg
             }, Test.statuses);
         }
         return this._structured_clone;
     };
 
     Test.prototype.step = function(func, this_obj)
     {
-        //In case the test has already failed
-        if (this.status !== this.NOTRUN)
+        if (this.phase > this.phases.STARTED)
         {
           return;
         }
+        this.phase = this.phases.STARTED;
+        //If we don't get a result before the harness times out that will be a test timout
+        this.set_status(this.TIMEOUT, "Test timed out");
 
         tests.started = true;
 
-        if (this.timeout_id === null) {
+        if (this.timeout_id === null)
+        {
             this.set_timeout();
         }
 
         this.steps.push(func);
 
         if (arguments.length === 1)
         {
             this_obj = this;
         }
 
         try
         {
             return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
         }
         catch(e)
         {
-            //This can happen if something called synchronously invoked another
-            //step
-            if (this.status !== this.NOTRUN)
+            if (this.phase >= this.phases.HAS_RESULT)
             {
                 return;
             }
-            this.status = this.FAIL;
-            this.message = (typeof e === "object" && e !== null) ? e.message : e;
+            var message = (typeof e === "object" && e !== null) ? e.message : e;
             if (typeof e.stack != "undefined" && typeof e.message == "string") {
                 //Try to make it more informative for some exceptions, at least
                 //in Gecko and WebKit.  This results in a stack dump instead of
                 //just errors like "Cannot read property 'parentNode' of null"
                 //or "root is null".  Makes it a lot longer, of course.
-                this.message += "(stack: " + e.stack + ")";
+                message += "(stack: " + e.stack + ")";
             }
+            this.set_status(this.FAIL, message);
+            this.phase = this.phases.HAS_RESULT;
             this.done();
-            if (debug && e.constructor !== AssertionError) {
-                throw e;
-            }
         }
     };
 
     Test.prototype.step_func = function(func, this_obj)
     {
         var test_this = this;
 
         if (arguments.length === 1)
@@ -1184,46 +1231,61 @@ policies and contribution forms [3].
         }
 
         return function()
         {
             test_this.step.apply(test_this, [func, this_obj].concat(
                 Array.prototype.slice.call(arguments)));
             test_this.done();
         };
-    };
+    }
 
     Test.prototype.set_timeout = function()
     {
-        var this_obj = this;
-        this.timeout_id = setTimeout(function()
-                                     {
-                                         this_obj.timeout();
-                                     }, this.timeout_length);
-    };
+        if (this.timeout_length !== null)
+        {
+            var this_obj = this;
+            this.timeout_id = setTimeout(function()
+                                         {
+                                             this_obj.timeout();
+                                         }, this.timeout_length);
+        }
+    }
+
+    Test.prototype.set_status = function(status, message)
+    {
+        this.status = status;
+        this.message = message;
+    }
 
     Test.prototype.timeout = function()
     {
-        this.status = this.TIMEOUT;
         this.timeout_id = null;
-        this.message = "Test timed out";
+        this.set_status(this.TIMEOUT, "Test timed out")
+        this.phase = this.phases.HAS_RESULT;
         this.done();
     };
 
     Test.prototype.done = function()
     {
-        if (this.is_done) {
+        if (this.phase == this.phases.COMPLETE) {
             return;
+        } else if (this.phase <= this.phases.STARTED)
+        {
+            this.set_status(this.PASS, null);
         }
+
+        if (this.status == this.NOTRUN)
+        {
+            alert(this.phase);
+        }
+
+        this.phase = this.phases.COMPLETE;
+
         clearTimeout(this.timeout_id);
-        if (this.status === this.NOTRUN)
-        {
-            this.status = this.PASS;
-        }
-        this.is_done = true;
         tests.result(this);
     };
 
 
     /*
      * Harness
      */
 
@@ -1273,17 +1335,18 @@ policies and contribution forms [3].
 
         //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.allow_uncaught_exception = false;
 
-        this.timeout_length = settings.timeout;
+        this.timeout_multiplier = 1;
+        this.timeout_length = this.get_timeout();
         this.timeout_id = null;
 
         this.start_callbacks = [];
         this.test_done_callbacks = [];
         this.all_done_callbacks = [];
 
         this.status = new TestsStatus();
 
@@ -1315,29 +1378,33 @@ policies and contribution forms [3].
 
         this.properties = properties;
 
         for (var p in properties)
         {
             if (properties.hasOwnProperty(p))
             {
                 var value = properties[p]
-                if (p == "timeout")
-                {
-                    this.timeout_length = value;
-                }
-                else if (p == "allow_uncaught_exception") {
+                if (p == "allow_uncaught_exception") {
                     this.allow_uncaught_exception = value;
                 }
                 else if (p == "explicit_done" && value)
                 {
                     this.wait_for_finish = true;
                 }
                 else if (p == "explicit_timeout" && value) {
                     this.timeout_length = null;
+                    if (this.timeout_id)
+                    {
+                        clearTimeout(this.timeout_id);
+                    }
+                }
+                else if (p == "timeout_multiplier")
+                {
+                    this.timeout_multiplier = value;
                 }
             }
         }
 
         if (func)
         {
             try
             {
@@ -1346,16 +1413,33 @@ policies and contribution forms [3].
             {
                 this.status.status = this.status.ERROR;
                 this.status.message = e;
             };
         }
         this.set_timeout();
     };
 
+    Tests.prototype.get_timeout = function()
+    {
+        var metas = document.getElementsByTagName("meta");
+        for (var i=0; i<metas.length; i++)
+        {
+            if (metas[i].name == "timeout")
+            {
+                if (metas[i].content == "long")
+                {
+                    return settings.harness_timeout.long;
+                }
+                break;
+            }
+        }
+        return settings.harness_timeout.normal;
+    }
+
     Tests.prototype.set_timeout = function()
     {
         var this_obj = this;
         clearTimeout(this.timeout_id);
         if (this.timeout_length !== null)
         {
             this.timeout_id = setTimeout(function() {
                                              this_obj.timeout();
@@ -1625,17 +1709,17 @@ policies and contribution forms [3].
     };
 
     Output.prototype.resolve_log = function()
     {
         var output_document;
         if (typeof this.output_document === "function")
         {
             output_document = this.output_document.apply(undefined);
-        } else
+        } else 
         {
             output_document = this.output_document;
         }
         if (!output_document)
         {
             return;
         }
         var node = output_document.getElementById("log");