David Wheeler - Test.Simple-0.29

Documentation | Source

Name

Test.Builder - Back end for building test libraries

Synopsis

  var Test = new Test.Builder();

  function ok (test, description) {
      Test.ok(test, description);
  }

Description

Test.Builder provides the buildings block upon which to write test libraries like Test.Simple and Test.More that can work together. All tests are expected to use a plan and to be run in an HTML element with its "id" attribute set to "test". See Test.Simple and Test.More for details. Users of this class, however, are expected to be folks who want to write test functions that interoperate with Test.Simple and Test.More.

Constants

Test.PLATFORM

This constant contains a string that defines the platform in which the tests are currently running. Possible values are:

browser

A Web browser.

director

Adobe Director.

wsh

Windows Scripting Host.

Construction

Test.Builder
  var Test = new Test.Builder();

Returns a new Test.Builder object. Since you generally only run one test per program, there should be one and only one Test.Builder object. So, in general, you should call Test.Builder.instance() to get at the singleton object used for all tests. Only use new Test.Builder() if you need to create a new Test.Builder object to, for example, test a Test.Builder-based test library.

instance
  var Test = Test.Builder.instance();

Returns a Test.Builder object representing the current state of the test. No matter how many times you call Test.Builder.instance(), you'll get the same object. (This is called a singleton).

reset
  Test.reset();

Reinitializes the Test.Builder singleton to its original state. Mostly useful for tests run in persistent environments where the same test might be run multiple times in the same process.

Setting up tests

These methods are for setting up tests and declaring how many there are. You usually only want to call one of these methods.

plan
  Test.plan({ noPlan:  true     });
  Test.plan({ skipAll: reason   });
  Test.plan({ tests:   numTests });

A convenient way to set up your tests. Call this method and Test.Builder will print the appropriate headers and take the appropriate actions.

If you call plan(), don't call any of the other test setup methods.

expectedTests
    var max = Test.expectedTests();
    Test.expectedTests(max);

Gets/sets the number of tests we expect this test to run and prints out the appropriate headers.

noPlan
  Test.noPlan();

Declares that this test will run an indeterminate number tests.

hasPlan
  var plan = Test.hasPlan();

Find out whether a plan has been defined. plan is either null (no plan has been set) "noPlan" (indeterminate number of tests) or an integer (the number of expected tests).

skipAll
  Test.skipAll();
  Test.skipAll(reason);

Skips all the tests in the test file, using the given reason.

Running tests

These methods actually run the tests. The description argument is always optional.

ok
  Test.ok(test, description);

Your basic test. Pass if test is true, fail if test is false. Returns a boolean indicating passage or failure.

isEq
  Test.isEq(got, expect, description);

Tests to see whether got is equivalent to expect.

isNum
  Test.isNum(got, expect, description);

Tests to see whether the numeric form of got is equivalent to the numeric form of expect as converted by Number().

isntEq
  Test.isntEq(got, dontExpect, description);

The opposite of isEq(). Tests to see whether got is not equivalent to dontExpect.

isntNum
  Test.isntNum(got, dontExpect, description);

The opposite of isNum(). Tests to see whether the numeric form of got is not equivalent to the numeric form of dontExpect as converted by Number().

like
  Test.like(got, /regex/, description);
  Test.like(got, 'regex', description);

Tests to see whether got matches the regular expression in regex. If a string is passed for the regex argument, it will be converted to a regular expression object for testing. If <got> is not a string, the test will fail.

unlike
  Test.unlike(got, /regex/, description);
  Test.unlike(got, 'regex', description);

The opposite of unlike(). Tests to see whether got does not match the regular expression in regex. If a string is passed for the regex argument, it will be converted to a regular expression object for testing. If <got> is not a string, the test will pass.

cmpOK
  Test.cmpOK(got, op, expect, description);

Performs a comparison of two values, got and expect. Specify any binary comparison operator as a string via the op argument. In addition to the usual JavaScript operators, cmpOK() also supports the Perl-style string comparison operators:

eq - String equal
ne - String not equal
lt - String less than
gt - String greater than
le - String less than or equal
ge - String greater than or equal
BAILOUT
    Test.BAILOUT(reason);

Indicates to the Test.Harness that things are going so badly all testing should terminate. This includes running any additional test files.

skip
    Test.skip();
    Test.skip(why);

Skips the current test, reporting why.

todoSkip
  Test.todoSkip();
  Test.todoSkip(why);

Like skip(), only it will declare the test as failing and TODO.

skipRest
  Test.skipRest();
  Test.skipRest(reason);

Like skip(), only it skips all the rest of the tests you plan to run and terminates the test.

If you're running under "noPlan", it skips once and terminates the test.

Test style

useNumbers
    Test.useNumbers(onOrOff);

Whether or not the test should output numbers. That is, this if true:

  ok 1
  ok 2
  ok 3

or this if false

  ok
  ok
  ok

Most useful when you can't depend on the test output order. Test.Harness will accept either, but avoid mixing the two styles. Defaults to true.

noHeader
    Test.noHeader(noHeader);

If set to true, no "1..N" header will be printed.

noEnding
    Test.noEnding(noEnding);

Normally, Test.Builder does some extra diagnostics when the test ends. It also changes the exit code as described below. If this is true, none of that will be done.

Output

Controlling where the test output goes. It's ok for your test to change where document.write points to; Test.Builder's default output settings will not be affected.

diag
    Test.diag(msg);
    Test.diag(msg, msg2, msg3);

Prints out all of its arguments. All arguments are simply appended together for output.

Normally, it uses the failureOutput() handle, but if this is for a TODO test, the todoOutput() handle is used.

Output will be indented and marked with a "#" so as not to interfere with test output. A newline will be put on the end if there isn't one already.

We encourage using this method rather than outputting diagnostics directly.

Returns false. Why? Because diag() is often used in conjunction with a failing test (ok() || diag()) it "passes through" the failure.

    return ok(...) || diag(...);

Output

These methods specify where test output and diagnostics will be sent. By default, in a browser they all default to appending to the element with the "test" ID or, failing that, to using document.write(). In Adobe Director, they use trace() for their output, and in Windows Scripting Host, they use WScript.StdOut.writeline(). If you wish to specify other functions that lack the apply() method, you'll need to supply them instead as custom anonymous functions that take a single argument (multiple arguments will be concatenated before being passed to the output function):

  Test.output(function (msg) { foo(msg) });
output
    Test.output(function);

Function to call with normal "ok/not ok" test output.

failureOutput
    Test.failureOutput(function);

Function to call with diagnostic output on test failures and diag.

todoOutput
    Test.todoOutput(function);

Function to call with diagnostic about todo test failures and diag.

warnOutput
    Test.warnOutput(function);

Function to call with warnings.

endOutput
    Test.endOutput(function);

Function to which to pass any end messages (such as "Looks like you planed 8 tests but ran 2 extra").

Test Status and Info

currentTest
    var currTest = Test.currentTest();
    Test.currentTest(num);

Gets/sets the current test number we're on. You usually shouldn't have to set this property.

If set forward, the details of the missing tests are filled in as "unknown". if set backward, the details of the intervening tests are deleted. You can erase history if you really want to.

summary
    my @tests = Test.summary();

A simple summary of the tests so far returned as an array or boolean values, true for pass, false for fail. This is a logical pass/fail, so todos are passes.

Of course, test #1 is tests[0], etc...

details
    my @tests = Test.details();

Like summary(), but with a lot more detail.

  tests[testNum - 1] = {
      ok:        is the test considered a pass?
      actual_ok: did it literally say 'ok'?
      desc:      description of the test (if any)
      type:      type of test (if any, see below).
      reason:    reason for the above (if any)
  };
  • "ok" is true if Test.Harness will consider the test to be a pass.
  • "actual_ok" is a reflection of whether or not the test literally printed "ok" or "not ok". This is for examining the result of "todo" tests.
  • "description is the description of the test.
  • "type" indicates if it was a special test. Normal tests have a type of "". Type can be one of the following:
    skip see skip()
    todo see todo()
    todo_skip see todoSkip()
    unknown see below

Sometimes the Test.Builder test counter is incremented without it printing any test output, for example, when currentTest() is changed. In these cases, Test.Builder doesn't know the result of the test, so it's type is "unknown". The details for these tests are filled in. They are considered ok, but the name and actual_ok is left null.

For example "not ok 23 - hole count # TODO insufficient donuts" would result in this structure:

  tests[22] = {          // 23 - 1, since arrays start from 0.
      ok:        1,      // logically, the test passed since it's todo
      actual_ok: 0,      // in absolute terms, it failed
      desc:      'hole count',
      type:      'todo',
      reason:    'insufficient donuts'
  };
todo
    TODO: {
        Test.todo(why, howMany);
        ...normal testing code goes here...
    }

Declares a series of tests that you expect to fail and why. Perhaps it's because you haven't fixed a bug or haven't finished a new feature. The next howMany tests will be expected to fail and thus marked as "TODO" tests.

caller
    var package = Test.caller();
    my(pack, file, line) = Test.caller();
    my(pack, file, line) = Test.caller(height);

Like the normal caller(), except it reports according to your level().

beginAsync
endAsync
  var timeout = 3000;
  var asyncID = Test.beginAsync(timeout);
  window.setTimeout(
      function () {
          Test.ok(true, "Pass after 2 seconds");
          Test.endAsync(asyncID);
      }, timeout - 1000
  );

Sometimes you may need to run tests in an asynchronous process. Such processes can be started using window.setTimeout() or window.setInterval() in a browser, or by making an XMLHttpRequest call. In such cases, the tests might normally run after the test script has completed, and thus the summary message at the end of the test script will be incorrect--and the test results will appear after the summary.

To get around this problem, tell the Test.Builder object that you're running asyncronous tests by calling beginAsync(). The test script will not finish until you pass the ID returned by beginAsync() to endAsync(). If you've called beginAsync() with the optional timout argument, then the test will finish if endAsync() has not been called with the appropriate ID before the timeout has elapsed. The timeout can be specified in milliseconds.

exporter
  if (typeof JSAN != 'undefined') new JSAN().use('Test.Builder');
  else {
      if (typeof Test == 'undefined' || typeof Test.Builder == 'undefined')
          throw new Error(
              "You must load either JSAN or Test.Builder "
              + "before loading Test.Simple"
          );
  }

  Test.Simple = {};
  Test.Simple.EXPORT      = ['plan', 'ok'];
  Test.Simple.EXPORT_TAGS = { ':all': Test.Simple.EXPORT };
  Test.Simple.VERSION     = '0.29';

  // .... Declare exportable functions, then export them.

  if (typeof JSAN == 'undefined') Test.Builder.exporter(Test.Simple);

This method is used by Test.More and Test.Simple to export functions into the global namespace. It is only used if JSAN (http://www.openjsan.org/) is not available. Other test modules built with Test.Builder should also use this method to export functions. An optional second argument specifies the name space in which to export the functionls. If it is not defined, it defaults to the window object in browsers and the _global object in Director.

Examples

CPAN can provide the best examples. Test.Simple and Test.More both use Test.Builder.

See Also

Test.Simple

Simple testing with a single testing function, ok(). Built with Test.Builder.

Test.More

Offers a panoply of test functions for your testing pleasure. Also built with Test.Builder.

http://www.edwardh.com/jsunit/

JSUnit: elaborate xUnit-style testing framework. Completely unrelated to Test.Builder.

ToDo

  • Finish porting tests from Test::Simple.
  • Properly catch native exceptions, such as for syntax errors (is this even possible?).

Authors

Original Perl code by chromatic and maintained by Michael G Schwern <schwern@pobox.com>. Ported to JavaScript by David Wheeler <david@kineticode.com>.

Copyright

Copyright 2002, 2004 by chromatic <chromatic@wgz.org> and Michael G Schwern <schwern@pobox.com>, 2005 by David Wheeler <david@kineticode.com>.

This program is free software; you can redistribute it and/or modify it under the terms of the Perl Artistic License or the GNU GPL.

See http://www.perl.com/perl/misc/Artistic.html and http://www.gnu.org/copyleft/gpl.html.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 627:

'=item' outside of any '=over'

// $Id$

/*global JSAN, Test, WScript, _global */

// Set up namespace.
if (typeof self != 'undefined') {
    // Browser
    if (typeof Test == 'undefined') Test = {PLATFORM: 'browser'};
    else Test.PLATFORM = 'browser';
} else if (typeof _global != 'undefined') {
    //Director
    if (typeof _global.Test == "undefined") _global.Test = {PLATFORM: 'director'};
    else _global.Test.PLATFORM = 'director';
} else if (typeof WScript != 'undefined') {
    // WSH
    if (typeof Test == 'undefined') Test = {PLATFORM: 'wsh'};
    else Test.PLATFORM = 'wsh';
} else {
    // Assume command-line interpreter.
    if (typeof Test == 'undefined') Test = {PLATFORM: 'interp'};
    else Test.PLATFORM = 'interp';
}

// Constructor.
Test.Builder = function () {
    Test.Builder.Instances.push(this.reset());
    if (!Test.Builder.Test) Test.Builder.Test = this;
};

// Static variables.
Test.Builder.globalScope = typeof JSAN != 'undefined'
  ? JSAN.globalScope
  :  typeof window != 'undefined'
    ? window
    : typeof _global != 'undefined'
      ? _global
      : this;

Test.Builder.VERSION = '0.29';
Test.Builder.Instances = [];
Test.Builder.lineEndingRx = /\r?\n|\r/g;
Test.Builder.StringOps = {
    eq: '==',
    ne: '!=',
    lt: '<',
    gt: '>',
    ge: '>=',
    le: '<='
};

// Stoopid IE.
Test.Builder.LF = typeof navigator != "undefined"
    && navigator.userAgent.toLowerCase().indexOf('msie') + 1
    && Test.Builder.globalScope.opera == undefined
  ? "\r"
  : "\n";

// Static methods.
Test.Builder.Error = function (msg) {
    this.message = msg;
    this.name    = "Test.Builder.Error";
}
Test.Builder.Error.prototype = new Error();

Test.Builder.die = function (msg) {
    throw new Test.Builder.Error(msg);
};

Test.Builder._whoa = function (check, desc) {
    if (!check) return;
    Test.Builder.die(
        "WHOA! " + desc + Test.Builder.LF +
        "This should never happen! Please contact the author immediately!"
    );
};

Test.Builder.typeOf = function (object) {
    var c = Object.prototype.toString.apply(object);
    var name = c.substring(8, c.length - 1);
    if (name != 'Object') return name;
    // It may be a non-core class. Try to extract the class name from
    // the constructor function. This may not work in all implementations.
    if (/function ([^(\s]+)/.test(Function.toString.call(object.constructor))) {
        return RegExp.$1;
    }
    // No idea. :-(
    return name;
};

Test.Builder.instance = function () {
    if (!Test.Builder.Test) return new Test.Builder();
    return Test.Builder.Test;
};

Test.Builder.create = function () {
    var ret = new Test.Builder();
    ret.diag(
        "Test.Builder.create() has been deprecated. "
        + "Use new Test.Builder() instead"
    );
    return ret;
};

// Instance methods.

Test.Builder.prototype.reset = function () {
    this.TestDied      = false;
    this.HavePlan      = false;
    this.NoPlan        = false;
    this.CurrTest      = 0;
    this.ExpectedTests = 0;
    this.UseNums       = true;
    this.NoHeader      = false;
    this.NoEnding      = false;
    this.TestResults   = [];
    this.ToDo          = [];
    this.Buffer        = [];
    this.asyncs        = [0];
    this.asyncID       = 0;
    return this._setupOutput();
};

Test.Builder.prototype._print = function (msg) {
    this.output().call(this, msg);
};

Test.Builder.prototype.warn = function (msg) {
    this.warnOutput().apply(this, arguments);
};

Test.Builder.prototype.plan = function (arg) {
    if (!arg) return;
    if (this.HavePlan) Test.Builder.die("You tried to plan twice!");

    if (!(arg instanceof Object))
        Test.Builder.die("plan() doesn't understand " + arg);
    for (var cmd in arg) {
        if (cmd == 'tests') {
            if (arg[cmd] == null) {
                Test.Bulder.die(
                    "Got an undefined number of tests. Looks like you tried to "
                    + "say how many tests you plan to run but made a mistake."
                    + Test.Builder.LF
                );
            } else if (!arg[cmd]) {
                Test.Builder.die(
                    "You said to run 0 tests! You've got to run something."
                    + Test.Builder.LF
                );
            } else {
                this.expectedTests(arg[cmd]);
            }
        } else if (cmd == 'skipAll') {
            this.skipAll(arg[cmd]);
        } else if (cmd == 'noPlan' && arg[cmd]) {
            this.noPlan();
        } else if ({}.hasOwnProperty) { 
            // see if the param belongs to Object.prototype.
            if (arg.hasOwnProperty(cmd)) {
                Test.Builder.die("plan() doesn't understand "
                    + cmd + (arg[cmd] ? (" " + arg[cmd]) : ''));
            }
        } else {
            // Ignore errors if hasOwnProperty() isn't available, as in 
            // Safari < 2.0.2 and Explorer 5.
        }
    }
};

Test.Builder.prototype.expectedTests = function (max) {
    if (max) {
        if (isNaN(max)) {
            Test.Builder.die(
                "Number of tests must be a postive integer. You gave it '"
                + max + "'." + Test.Builder.LF
            );
        }

        this.ExpectedTests = max.valueOf();
        this.HavePlan       = true;
        if (!this.noHeader()) this._print("1.." + max + Test.Builder.LF);
    }
    return this.ExpectedTests;
};

Test.Builder.prototype.noPlan = function () {
    this.NoPlan   = 1;
    this.HavePlan = 1;
};

Test.Builder.prototype.hasPlan = function () {
    if (this.ExpectedTests) return this.ExpectedTests;
    if (this.NoPlan) return 'noPlan';
};

Test.Builder.prototype.skipAll = function (reason) {
    var out = "1..0";
    if (reason) out += " # Skip " + reason;
    out += Test.Builder.LF;
    this.SkipAll = 1;
    if (!this.noHeader()) this.output()(out);
    // Just throw and catch an exception.
    Test.Builder.globalScope.onerror = function () { return true; }
    throw new Error("__SKIP_ALL__");
};

Test.Builder.prototype.ok = function (test, desc) {
    // test might contain an object that we don't want to accidentally
    // store, so we turn it into a boolean.
    test = !!test;

    if (!this.HavePlan) {
        Test.Builder.die(
            "You tried to run a test without a plan! Gotta have a plan."
        );
    }

    // Append any output to the previous test's results.
    if (this.Buffer.length && this.TestResults.length) {
        this.TestResults[this.TestResults.length - 1].appendOutput(
            this.Buffer.splice(0, this.Buffer.length).join('')
        );
    }

    // I don't think we need to worry about threading in JavaScript.
    this.CurrTest++;

    // In case desc is a string overloaded object, force it to stringify.
    if (desc) desc = desc.toString();

    var startsNumber
    if (desc != null && /^[\d\s]+$/.test(desc)) {
        this.diag( "Your test description is '" + desc + "'. You shouldn't use",
                   Test.Builder.LF,
                   "numbers for your test names. Very confusing.");
    }

    var todo = this._todo();
    // I don't think we need to worry about result beeing shared between
    // threads.
    var out = '';
    var result = new Test.Builder.TestResult();

    if (test) {
        result.setOK(true);
        result.setActualOK(test);
    } else {
        out += 'not ';
        result.setOK(todo ? true : false);
        result.setActualOK(false);
    }

    out += 'ok';
    if (this.useNumbers) out += ' ' + this.CurrTest;

    if (desc == null) {
        result.setDesc('');
    } else {
        desc = desc.replace(Test.Builder.lineEndingRx, Test.Builder.LF + "# ");
        // XXX Does this matter since we don't have a TestHarness?
        desc.split('#').join('\\#'); // # # in a desc can confuse TestHarness.
        out += ' - ' + desc;
        result.setDesc(desc);
    }

    if (todo) {
        todo = todo.replace(Test.Builder.lineEndingRx, Test.Builder.LF + "# ");
        out += " # TODO " + todo;
        result.setReason(todo);
        result.setType('todo');
    } else {
        result.setReason('');
        result.setType('');
    }

    this.TestResults[this.CurrTest - 1] = result;

    out += Test.Builder.LF;
    this._print(out);

    if (!test) {
        // Add URL and line numer using code from http://pastie.org/253058?
        var msg = todo ? "Failed (TODO)" : "Failed";
        this.diag("    " + msg + " test");
    }
    result.setOutput(this.Buffer.splice(0, this.Buffer.length).join(''));
    return test;
};

Test.Builder.prototype.isEq = function (got, expect, desc) {
    if (got == null || expect == null) {
        // undefined only matches undefined and nothing else
        return this.isUndef(got, '==', expect, desc);
    }
    return this.cmpOK(got, '==', expect, desc);
};

Test.Builder.prototype.isNum = function (got, expect, desc) {
    if (got == null || expect == null) {
        // undefined only matches undefined and nothing else
        return this.isUndef(got, '==', expect, desc);
    }
    return this.cmpOK(Number(got), '==', Number(expect), desc);
};

Test.Builder.prototype.isntEq = function (got, dontExpect, desc) {
    if (got == null || dontExpect == null) {
        // undefined only matches undefined and nothing else
        return this.isUndef(got, '!=', dontExpect, desc);
    }
    return this.cmpOK(got, '!=', dontExpect, desc);
};

Test.Builder.prototype.isntNum = function (got, dontExpect, desc) {
    if (got == null || dontExpect == null) {
        // undefined only matches undefined and nothing else
        return this.isUndef(got, '!=', dontExpect, desc);
    }
    return this.cmpOK(Number(got), '!=', Number(dontExpect), desc);
};

Test.Builder.prototype.like = function (val, regex, desc) {
    return this._regexOK(val, regex, '=~', desc);
};

Test.Builder.prototype.unlike = function (val, regex, desc) {
    return this._regexOK(val, regex, '!~', desc);
};

Test.Builder.prototype._regexOK = function (val, regex, cmp, desc) {
    // Create a regex object.
    var type = Test.Builder.typeOf(regex);
    var ok;
    if (type.toLowerCase() == 'string') {
        // Create a regex object.
        regex = new RegExp(regex);
    } else {
        if (type != 'RegExp') {
            ok = this.ok(false, desc);
            this.diag("'" + regex + "' doesn't look much like a regex to me.");
            return ok;
        }
    }

    if (val == null || typeof val != 'string') {
        if (cmp == '=~') {
            // The test fails.
            ok = this.ok(false, desc);
            this._diagLike(val, regex, cmp);
        } else {
            // undefined matches nothing (unlike in Perl, where undef =~ //).
            ok = this.ok(true, desc);
        }
        return ok;
    }

    // Use val.match() instead of regex.test() in case they've set g.
    var test = val.match(regex);
    if (cmp == '!~') test = !test;
    ok = this.ok(test, desc);
    if (!ok) this._diagLike(val, regex, cmp);
    return ok;
};

Test.Builder.prototype._diagLike = function (val, regex, cmp) {
    var match = cmp == '=~' ? "doesn't match" : "      matches";
    return this.diag(
        "                  '" + val + "" + Test.Builder.LF +
        "    " + match + " /" + regex.source + "/"
    );
};

Test.Builder.prototype.cmpOK = function (got, op, expect, desc) {

    var test;
    if (Test.Builder.StringOps[op]) {
        // Force string context.
        test = eval("got.toString() " + Test.Builder.StringOps[op] + " expect.toString()");
    } else {
        test = eval("got " + op + " expect");
    }

    var ok = this.ok(test, desc);
    if (!ok) {
        if (/^(eq|==)$/.test(op)) {
            this._isDiag(got, op, expect);
        } else {
            this._cmpDiag(got, op, expect);
        }
    }
    return ok;
};

Test.Builder.prototype._cmpDiag = function (got, op, expect) {
    if (got != null) got = "'" + got.toString() + "'";
    if (expect != null) expect = "'" + expect.toString() + "'";
    return this.diag("    " + got + Test.Builder.LF + "        " + op
                     + Test.Builder.LF + "    " + expect);
};

Test.Builder.prototype._isDiag = function (got, op, expect) {
    var args = [got, expect];
    for (var i = 0; i < args.length; i++) {
        if (args[i] != null) {
            args[i] = op == 'eq' ? "'" + args[i].toString() + "'" : args[i].valueOf();
        }
    }

    return this.diag(
        "        have: " + args[0] + Test.Builder.LF +
        "        want: " + args[1] + Test.Builder.LF
    );
};

Test.Builder.prototype.BAILOUT = function (reason) {
    this._print("Bail out! " + reason);
    // Just throw and catch an exception.
    Test.Builder.globalScope.onerror = function () {
        // XXX Do something to tell TestHarness it was a bailout?
        return true;
    }
    throw new Error("__BAILOUT__");
};

Test.Builder.prototype.skip = function (why) {
    if (!this.HavePlan)
        Test.Builder.die("You tried to run a test without a plan! Gotta have a plan.");

    // In case desc is a string overloaded object, force it to stringify.
    if (why) why = why.toString().replace(Test.Builder.lineEndingRx,
                                          Test.Builder.LF+ "# ");

    this.CurrTest++;
    this.TestResults[this.CurrTest - 1] = new Test.Builder.TestResult({
        ok:        true,
        actualOK:  true,
        desc:      '',
        type:      'skip',
        reason:    why
    });

    var out = "ok";
    if (this.useNumbers) out += ' ' + this.CurrTest;
    out    += " # skip " + why + Test.Builder.LF;
    this._print(out);
    this.TestResults[this.CurrTest - 1].setOutput(
        this.Buffer.splice(0, this.Buffer.length).join('')
    );
    return true;
};

Test.Builder.prototype.todoSkip = function (why) {
    if (!this.HavePlan)
        Test.Builder.die("You tried to run a test without a plan! Gotta have a plan.");

    // In case desc is a string overloaded object, force it to stringify.
    if (why) why = why.toString().replace(Test.Builder.lineEndingRx,
                                          Test.Builder.LF + "# ");
    

    this.CurrTest++;
    this.TestResults[this.CurrTest - 1] = new Test.Builder.TestResult({
        ok:        true,
        actualOK:  false,
        desc:      '',
        type:      'todo_skip',
        reason:    why
    });

    var out = "not ok";
    if (this.useNumbers) out += ' ' + this.CurrTest;
    out    += " # TODO & SKIP " + why + Test.Builder.LF;
    this._print(out);
    this.TestResults[this.CurrTest - 1].setOutput(
        this.Buffer.splice(0, this.Buffer.length).join('')
    );
    return true;
};

Test.Builder.prototype.skipRest = function (reason) {
    var out = "# Skip";
    if (reason) out += " " + reason;
    out += Test.Builder.LF;
    if (this.NoPlan) this.skip(reason);
    else {
        for (var i = this.CurrTest; i < this.ExpectedTests; i++) {
            this.skip(reason);
        }
    }
    // Just throw and catch an exception.
    Test.Builder.globalScope.onerror = function () { return true; }
    throw new Error("__SKIP_REST__");
};

Test.Builder.prototype.useNumbers = function (useNums) {
    if (useNums != null) this.UseNums = useNums;
    return this.UseNums;
};

Test.Builder.prototype.noHeader = function (noHeader) {
    if (noHeader != null) this.NoHeader = !!noHeader;
    return this.NoHeader;
};

Test.Builder.prototype.noEnding = function (noEnding) {
    if (noEnding != null) this.NoEnding = !!noEnding;
    return this.NoEnding;
};

Test.Builder.prototype.diag = function () {
    if (!arguments.length) return;

    var msg = '# ';
    // Join each agument and escape each line with a #.
    for (var i = 0; i < arguments.length; i++) {
        // Replace any newlines.
        msg += arguments[i].toString().replace(Test.Builder.lineEndingRx,
                                               Test.Builder.LF + "# ");
    }

    // Append a new line to the end of the message if there isn't one.
    if (!(new RegExp(Test.Builder.LF + '$').test(msg)))
        msg += Test.Builder.LF;
    // Append the diag message to the most recent result.
    return this._printDiag(msg);
};

Test.Builder.prototype._printDiag = function () {
    var fn = this.todo() ? this.todoOutput() : this.failureOutput();
    fn.apply(this, arguments);
    return false;
};

Test.Builder.prototype.output = function (fn) {
    if (fn != null) {
        var buffer = this.Buffer;
        this.Output = function (msg) { buffer.push(msg); fn(msg) };
    }
    return this.Output;
};

Test.Builder.prototype.failureOutput = function (fn) {
    if (fn != null) {
        var buffer = this.Buffer;
        this.FailureOutput = function (msg) { buffer.push(msg); fn(msg) };
    }
    return this.FailureOutput;
};

Test.Builder.prototype.todoOutput = function (fn) {
    if (fn != null) {
        var buffer = this.Buffer;
        this.TodoOutput = function (msg) { buffer.push(msg); fn(msg) };
    }
    return this.TodoOutput;
};

Test.Builder.prototype.endOutput = function (fn) {
    if (fn != null) {
        var buffer = this.Buffer;
        this.EndOutput = function (msg) { buffer.push(msg); fn(msg) };
    }
    return this.EndOutput;
};

Test.Builder.prototype.warnOutput = function (fn) {
    if (fn != null) {
        var buffer = this.Buffer;
        this.WarnOutput = function (msg) { buffer.push(msg); fn(msg) };
    }
    return this.WarnOutput;
};

Test.Builder.prototype._setupOutput = function () {
    if (Test.PLATFORM == 'browser') {
        var top = Test.Builder.globalScope;
        var doc = top.document;
        var writer = function (msg) {
            // I'm sure that there must be a more efficient way to do this,
            // but if I store the node in a variable outside of this function
            // and refer to it via the closure, then things don't work right
            // --the order of output can become all screwed up (see
            // buffer.html).  I have no idea why this is.
            var body = doc.body || doc.getElementsByTagName("body")[0];
            var node = doc.getElementById('test_output')
                || doc.getElementById('test');
            if (!node) {
                node = document.createElement('pre');
                node.id = 'test_output';
                body.appendChild(node);
            }

            // This approach is neater, but causes buffering problems when
            // mixed with document.write. See tests/buffer.html.
            //node.appendChild(document.createTextNode(msg));
            //return;
            for (var i = 0; i < node.childNodes.length; i++) {
                if (node.childNodes[i].nodeType == 3 /* Text Node */) {
                    // Append to the node and scroll down.
                    node.childNodes[i].appendData(msg);
                    top.scrollTo( 0, body.offsetHeight || body.scrollHeight );
                    return;
                }
            }

            // If there was no text node, add one.
            node.appendChild(doc.createTextNode(msg));
            top.scrollTo(0, body.offsetHeight || body.scrollHeight);
            return;
        };

        this.output(writer);
        this.failureOutput(function (msg) {
            writer(msg);
        });
        this.todoOutput(writer);
        this.endOutput(writer);

        if (top.alert.apply) {
            this.warnOutput(top.alert, top);
        } else {
            this.warnOutput(function (msg) { top.alert(msg); });
        }

    } else if (Test.PLATFORM == 'director') {
        // Macromedia-Adobe:Director MX 2004 Support
        // XXX Is _player a definitive enough object?
        // There may be an even more explicitly Director object.
        /*global trace */
        this.output(trace);       
        this.failureOutput(trace);
        this.todoOutput(trace);
        this.warnOutput(trace);

    } else if (Test.PLATFORM == 'wsh') {
        // Windows Scripting Host Support
        var printer = function (msg) {
			WScript.StdOut.writeline(msg);
		}
		this.output(printer);
		this.failureOutput(printer);
		this.todoOutput(printer);
		this.warnOutput(printer);

    } else if (Test.PLATFORM == 'interp') {
        // Command-line interpeter.
        var out = function (toOut) { print( toOut.replace(/\n$/, '') ); };
        this.output(out);
        this.failureOutput(out);
        this.todoOutput(out);
        this.warnOutput(out);
	}
    return this;
};

Test.Builder.prototype.currentTest = function (num) {
    if (num == null) return this.CurrTest;

    if (!this.HavePlan)
        Test.Builder.die("Can't change the current test number without a plan!");
    this.CurrTest = num;
    if (num >= this.TestResults.length ) {
        var reason = 'incrementing test number';
        for (var i = this.TestResults.length; i < num; i++) {
            this.TestResults[i] = new Test.Builder.TestResult({
                ok:        true, 
                actualOK:  null,
                reason:    reason,
                type:      'unknown', 
                output:    'ok - ' + reason + Test.Builder.LF
            });
        }
    } else if (num < this.TestResults.length) {
        // IE requires the second argument to truncate the array.
        this.TestResults.splice(num, this.TestResults.length);
    }
    return this.CurrTest;
};

Test.Builder.prototype.summary = function () {
    var results = new Array(this.TestResults.length);
    for (var i = 0; i < this.TestResults.length; i++) {
        var result = this.TestResults[i];
        results[i] = result ? null : result.getOK();
    }
    return results
};

Test.Builder.prototype.details = function () {
    var details = new Array();
    for (var i = 0; i < this.TestResults.length; i++) {
        var result = this.TestResults[i];
        details.push(result ? result.exportDetails() : null);
    }
    return details;
};

Test.Builder.prototype.todo = function (why, howMany) {
    if (howMany) this.ToDo = [why, howMany];
    return this.ToDo[1];
};

Test.Builder.prototype._todo = function () {
    if (this.ToDo[1]) {
        if (this.ToDo[1]--) return this.ToDo[0];
        this.ToDo = [];
    }
    return false;
};

Test.Builder.prototype._sanity_check = function () {
    Test.Builder._whoa(
        this.CurrTest < 0,
        'Says here you ran a negative number of tests!'
    );

    Test.Builder._whoa(
        !this.HavePlan && this.CurrTest, 
        'Somehow your tests ran without a plan!'
    );

    Test.Builder._whoa(
        this.CurrTest != this.TestResults.length,
        'Somehow you got a different number of results than tests ran!'
    );
};

Test.Builder.prototype._notifyHarness = function () {
    var top = Test.Builder.globalScope;
    // Special treatment for the browser harness.
    if (top.parent && top.parent.Test && top.parent.Test.Harness) {
        top.parent.Test.Harness.Done++;
    }
};

Test.Builder.prototype._ending = function () {
    if (this.Ended) return;
    this.Ended = true;
    if (this.noEnding()) {
        this._notifyHarness();
        return;
    }
    this._sanity_check();
    var out = this.endOutput();

    // Figure out if we passed or failed and print helpful messages.
    if( this.TestResults.length ) {
        // The plan?  We have no plan.
        if (this.NoPlan) {
            if (!this.noHeader())
                this._print("1.." + this.CurrTest + Test.Builder.LF);
            this.ExpectedTests = this.CurrTest;
        }

        var numFailed = 0;
        for (var i = 0; i < this.TestResults.length; i++) {
            if (!this.TestResults[i].ok) numFailed++;
        }
        numFailed += Math.abs(
            this.ExpectedTests - this.TestResults.length
        );

        if (this.CurrTest != this.ExpectedTests) {
            out(
                "# Looks like you planned " + this.ExpectedTests + " test"
                + (this.ExpectedTests == 1 ? '' : 's')
                + " but ran " + this.CurrTest + "." + Test.Builder.LF
            );
        } else if (numFailed) {
            out(
                "# Looks like you failed " + numFailed + " test"
                + (numFailed == 1 ? '' : 's') + " of "
                + this.ExpectedTests + "." + Test.Builder.LF
            );
        }

        if (this.TestDied) {
            out(
                "# Looks like your test died just after " 
                + this.CurrTest + "." + Test.Builder.LF
            );
        }

    } else if (!this.SkipAll) {
        // skipAll requires no status output.
        if (this.TestDied) {
            out(
                "# Looks like your test died before it could output anything."
                + Test.Builder.LF
            );
        } else {
            out("# No tests run!" + Test.Builder.LF);
        }
    }
    this._notifyHarness();
};

Test.Builder.prototype.isUndef = function (got, op, expect, desc) {
    // Undefined only matches undefined, so we don't need to cast anything.
    var test = eval("got " + (Test.Builder.StringOps[op] || op) + " expect");
    this.ok(test, desc);
    if (!test) this._isDiag(got, op, expect);
    return test;
};

Test.Builder._finish = function (pkg) {
    if (!pkg) pkg = Test;
    for (var i = 0; i < pkg.Builder.Instances.length; i++) {
        // The main process is always async ID 0.
        if (!pkg.Builder.Instances[i].Ended)
            pkg.Builder.Instances[i].endAsync(0);
    }
};

if (Test.Builder.globalScope) {
    // Set up an onload function to end all tests.
    Test.Builder.globalScope.onload = function (event, pkg) {
        // The package may be passed in if onload() is called explicitly.
        // This is to get around a very weird scoping bug in my version of
        // Firefox. See Test.Harness.Browser.runTest() for this usage.
        Test.Builder._finish(pkg)
    };

    // Set up an exception handler. This is so that we can capture deaths but
    // still output information for TestHarness to pick up.
    Test.Builder.globalScope.onerror = function (msg, url, line) {
        // Output the exception.
        Test.Builder.Test.TestDied = true;
        Test.Builder.Test.diag("Error in " + url + " at line " + line + ": " + msg);
        return true;
    };
};

Test.Builder.prototype.beginAsync = function (timeout) {
	var id = ++this.asyncID;
    var top = Test.Builder.globalScope;
    if (timeout && top && top.setTimeout) {
        // Are there other ways of setting timeout in non-browser settings?
        var aTest = this;
        this.asyncs[id] = top.setTimeout(
            function () { aTest.endAsync(id) }, timeout
        );
    } else {
        // Make sure it's defined.
        this.asyncs[id] = 0;
    }
	return id;
};

Test.Builder.prototype.endAsync = function (id) {
    if (this.asyncs[id] == undefined) return;
    if (this.asyncs[id]) {
		// Remove the timeout
		Test.Builder.globalScope.clearTimeout(this.asyncs[id]);
	}
    if (--this.asyncID < 0) this._ending();
};

Test.Builder.exporter = function (pkg, root) {
    if (typeof root == 'undefined') {
        root = Test.Builder.globalScope;
        if (!root) throw new Error("Platform unknown");
    }
    for (var i = 0; i < pkg.EXPORT.length; i++) {
        if (typeof root[pkg.EXPORT[i]] == 'undefined')
            root[pkg.EXPORT[i]] = pkg[pkg.EXPORT[i]];
    }
};

// Package-private utility class for describing the results of a single test.
Test.Builder.TestResult = function (args) {
    var defaults = Test.Builder.TestResult.defaults;
    for (var name in defaults) {
        this[name] = defaults[name];
    }
    if (args) {
        for (var name in args) {
            // Validate params.
            if ({}.hasOwnProperty) {
                if (   args.hasOwnProperty(name) 
                    && !defaults.hasOwnProperty(name)
                ) {
                    throw new Test.Builder.Error("Invalid parameter: " + name);
                }
            }
            this[name] = args[name];
        }
    }
}

Test.Builder.TestResult.defaults = {
    ok:       null,
    actualOK: null,
    desc:     '',
    reason:   '',
    type:     null,
    output:   '' 
};

// Set up get/set accessors
Test.Builder.TestResult.makeGetSet = function (varName, reCasedName) {
    Test.Builder.TestResult.prototype[ 'get' + reCasedName ] 
        = function () { return this[varName] };
    Test.Builder.TestResult.prototype[ 'set' + reCasedName ] 
        = function (newVal) { this[varName] = newVal };
}
Test.Builder.TestResult.makeGetSet('ok',       'OK');
Test.Builder.TestResult.makeGetSet('actualOK', 'ActualOK'); 
Test.Builder.TestResult.makeGetSet('desc',     'Desc');
Test.Builder.TestResult.makeGetSet('reason',   'Reason');
Test.Builder.TestResult.makeGetSet('type',     'Type');
Test.Builder.TestResult.makeGetSet('output',   'Output');

// Append string to 'output' member var.
Test.Builder.TestResult.prototype.appendOutput = function (more) {
    this.output += more;
};

Test.Builder.TestResult.prototype.exportDetails = function () {
    return {
        ok:        this.ok,
        actual_ok: this.actualOK, // backwards compatible
        desc:      this.desc,
        reason:    this.reason,
        type:      this.type,
        output:    this.output
    };
};

//////////////////////////////////////////////////////////////////////////////
// Package-private utility class for describing the results of a single test.
//////////////////////////////////////////////////////////////////////////////
Test.Builder.TestResult = function (args) {
    var defaults = Test.Builder.TestResult.defaults;
    for (var name in defaults) {
        this[name] = defaults[name];
    }
    if (args) {
        for (var prop in args) {
            // Validate params.
            if ({}.hasOwnProperty
                && args.hasOwnProperty(prop) 
                && !defaults.hasOwnProperty(prop)
            ) {
                throw new Test.Builder.Error("Invalid parameter: " + prop);
            }
            this[prop] = args[prop];
        }
    }
}

Test.Builder.TestResult.defaults = {
    ok:       null,
    actualOK: null,
    desc:     '',
    reason:   '',
    type:     null,
    output:   '' 
};

// Set up get/set accessors
Test.Builder.TestResult.makeGetSet = function (varName, reCasedName) {
    Test.Builder.TestResult.prototype[ 'get' + reCasedName ] 
        = function () { return this[varName] };
    Test.Builder.TestResult.prototype[ 'set' + reCasedName ] 
        = function (newVal) { this[varName] = newVal };
}
Test.Builder.TestResult.makeGetSet('ok',       'OK');
Test.Builder.TestResult.makeGetSet('actualOK', 'ActualOK'); 
Test.Builder.TestResult.makeGetSet('desc',     'Desc');
Test.Builder.TestResult.makeGetSet('reason',   'Reason');
Test.Builder.TestResult.makeGetSet('type',     'Type');
Test.Builder.TestResult.makeGetSet('output',   'Output');

// Append string to 'output' member var.
Test.Builder.TestResult.prototype.appendOutput = function (more) {
    this.output += more;
};

Test.Builder.TestResult.prototype.exportDetails = function () {
    return {
        ok:        this.ok,
        actual_ok: this.actualOK, // backwards compatible
        desc:      this.desc,
        reason:    this.reason,
        type:      this.type,
        output:    this.output
    };
};