blob: b611ef4404192745eda89245839d5efd879c5e72 [file] [log] [blame]
// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* @fileoverview JavaScript unit testing framework for synchronous and
* asynchronous tests.
*
* This file contains the lib.TestManager and related classes. At the moment
* it's all collected in a single file since it's reasonably small
* (=~1k lines), and it's a lot easier to include one file into your test
* harness than it is to include seven.
*
* The following classes are defined...
*
* lib.TestManager - The root class and entrypoint for creating test runs.
* lib.TestManager.Log - Logging service.
* lib.TestManager.Suite - A collection of tests.
* lib.TestManager.Test - A single test.
* lib.TestManager.TestRun - Manages the execution of a set of tests.
* lib.TestManager.Result - A single test result.
*/
/**
* Root object in the unit test hierarchy, and keeper of the log object.
*
* @param {lib.TestManager.Log} opt_log Optional lib.TestManager.Log object.
* Logs to the JavaScript console if omitted.
*/
lib.TestManager = function(opt_log) {
this.log = opt_log || new lib.TestManager.Log();
};
/**
* Create a new test run object for this test manager.
*
* @param {Object} opt_cx An object to be passed to test suite setup(),
* preamble(), and test cases during this test run. This object is opaque
* to lib.TestManager.* code. It's entirely up to the test suite what it's
* used for.
*/
lib.TestManager.prototype.createTestRun = function(opt_cx) {
return new lib.TestManager.TestRun(this, opt_cx);
};
/**
* Called when a test run associated with this test manager completes.
*
* Clients may override this to call an appropriate function.
*/
lib.TestManager.prototype.onTestRunComplete = function(testRun) {};
/**
* Called before a test associated with this test manager is run.
*
* @param {lib.TestManager.Result} result The result object for the upcoming
* test.
* @param {Object} cx The context object for a test run.
*/
lib.TestManager.prototype.testPreamble = function(result, cx) {};
/**
* Called after a test associated with this test manager finishes.
*
* @param {lib.TestManager.Result} result The result object for the finished
* test.
* @param {Object} cx The context object for a test run.
*/
lib.TestManager.prototype.testPostamble = function(result, cx) {};
/**
* Destination for test case output.
*
* Thw API will be the same as the console object. e.g. We support info(),
* warn(), error(), etc... just like console.info(), etc...
*
* @param {Object} opt_console The console object to route all logging through.
* Should provide saome API as the standard console API.
*/
lib.TestManager.Log = function(opt_console=console) {
this.save = false;
this.data = '';
this.prefix_ = '';
this.prefixStack_ = 0;
// Capture all the console entry points in case code at runtime calls these
// directly. We want to be able to still see things.
// We also expose the direct API to our callers (e.g. we provide warn()).
this.console_ = opt_console;
['log', 'debug', 'info', 'warn', 'error'].forEach((level) => {
let msgPrefix = '';
switch (level) {
case 'debug':
case 'warn':
case 'error':
msgPrefix = level.toUpperCase() + ': ';
break;
}
const oLog = this.console_[level];
this[level] = this.console_[level] = (...args) => {
if (this.save)
this.data += this.prefix_ + msgPrefix + args.join(' ') + '\n';
oLog.apply(this.console_, args);
};
});
// Wrap/bind the group functions.
['group', 'groupCollapsed'].forEach((group) => {
const oGroup = this.console_[group];
this[group] = this.console_[group] = (label='') => {
oGroup(label);
if (this.save)
this.data += this.prefix_ + label + '\n';
this.prefix_ = ' '.repeat(++this.prefixStack_);
};
});
const oGroupEnd = this.console_.groupEnd;
this.groupEnd = this.console_.groupEnd = () => {
oGroupEnd();
this.prefix_ = ' '.repeat(--this.prefixStack_);
};
};
/**
* Returns a new constructor function that will inherit from
* lib.TestManager.Suite.
*
* Use this function to create a new test suite subclass. It will return a
* properly initialized constructor function for the subclass. You can then
* override the setup() and preamble() methods if necessary and add test cases
* to the subclass.
*
* var MyTests = new lib.TestManager.Suite('MyTests');
*
* MyTests.prototype.setup = function(cx) {
* // Sets this.size to cx.size if it exists, or the default value of 10
* // if not.
* this.setDefault(cx, {size: 10});
* };
*
* MyTests.prototype.preamble = function(result, cx) {
* // Some tests (even successful ones) may side-effect this list, so
* // recreate it before every test.
* this.list = [];
* for (var i = 0; i < this.size; i++) {
* this.list[i] = i;
* }
* };
*
* // Basic synchronous test case.
* MyTests.addTest('pop-length', function(result, cx) {
* this.list.pop();
*
* // If this assertion fails, the testcase will stop here.
* result.assertEQ(this.list.length, this.size - 1);
*
* // A test must indicate it has passed by calling this method.
* result.pass();
* });
*
* // Sample asynchronous test case.
* MyTests.addTest('async-pop-length', function(result, cx) {
* var callback = () => {
* result.assertEQ(this.list.length, this.size - 1);
* result.pass();
* };
*
* // Wait 100ms to check the array length for the sake of this example.
* setTimeout(callback, 100);
*
* this.list.pop();
*
* // Indicate that this test needs another 200ms to complete.
* // If the test does not report pass/fail by then, it is considered to
* // have timed out.
* result.requestTime(200);
* });
*
* ...
*
* @param {string} suiteName The name of the test suite.
*/
lib.TestManager.Suite = function(suiteName) {
function ctor(testManager, cx) {
this.testManager_ = testManager;
this.suiteName = suiteName;
this.setup(cx);
}
ctor.suiteName = suiteName;
ctor.addTest = lib.TestManager.Suite.addTest;
ctor.disableTest = lib.TestManager.Suite.disableTest;
ctor.getTest = lib.TestManager.Suite.getTest;
ctor.getTestList = lib.TestManager.Suite.getTestList;
ctor.testList_ = [];
ctor.testMap_ = {};
ctor.prototype = Object.create(lib.TestManager.Suite.prototype);
ctor.constructor = lib.TestManager.Suite;
lib.TestManager.Suite.subclasses.push(ctor);
return ctor;
};
/**
* List of lib.TestManager.Suite subclasses, in the order they were defined.
*/
lib.TestManager.Suite.subclasses = [];
/**
* Add a test to a lib.TestManager.Suite.
*
* This method is copied to new subclasses when they are created.
*/
lib.TestManager.Suite.addTest = function(testName, testFunction) {
if (testName in this.testMap_)
throw 'Duplicate test name: ' + testName;
var test = new lib.TestManager.Test(this, testName, testFunction);
this.testMap_[testName] = test;
this.testList_.push(test);
};
/**
* Defines a disabled test.
*/
lib.TestManager.Suite.disableTest = function(testName, testFunction) {
if (testName in this.testMap_)
throw 'Duplicate test name: ' + testName;
var test = new lib.TestManager.Test(this, testName, testFunction);
console.log('Disabled test: ' + test.fullName);
};
/**
* Get a lib.TestManager.Test instance by name.
*
* This method is copied to new subclasses when they are created.
*
* @param {string} testName The name of the desired test.
* @return {lib.TestManager.Test} The requested test, or undefined if it was not
* found.
*/
lib.TestManager.Suite.getTest = function(testName) {
return this.testMap_[testName];
};
/**
* Get an array of lib.TestManager.Tests associated with this Suite.
*
* This method is copied to new subclasses when they are created.
*/
lib.TestManager.Suite.getTestList = function() {
return this.testList_;
};
/**
* Set properties on a test suite instance, pulling the property value from
* the context if it exists and from the defaults dictionary if not.
*
* This is intended to be used in your test suite's setup() method to
* define parameters for the test suite which may be overridden through the
* context object. For example...
*
* MySuite.prototype.setup = function(cx) {
* this.setDefaults(cx, {size: 10});
* };
*
* If the context object has a 'size' property then this.size will be set to
* the value of cx.size, otherwise this.size will get a default value of 10.
*
* @param {Object} cx The context object for a test run.
* @param {Object} defaults An object containing name/value pairs to set on
* this test suite instance. The value listed here will be used if the
* name is not defined on the context object.
*/
lib.TestManager.Suite.prototype.setDefaults = function(cx, defaults) {
for (var k in defaults) {
this[k] = (k in cx) ? cx[k] : defaults[k];
}
};
/**
* Subclassable method called to set up the test suite.
*
* The default implementation of this method is a no-op. If your test suite
* requires some kind of suite-wide setup, this is the place to do it.
*
* It's fine to store state on the test suite instance, that state will be
* accessible to all tests in the suite. If any test case fails, the entire
* test suite object will be discarded and a new one will be created for
* the remaining tests.
*
* Any side effects outside of this test suite instance must be idempotent.
* For example, if you're adding DOM nodes to a document, make sure to first
* test that they're not already there. If they are, remove them rather than
* reuse them. You should not count on their state, since they were probably
* left behind by a failed testcase.
*
* Any exception here will abort the remainder of the test run.
*
* @param {Object} cx The context object for a test run.
*/
lib.TestManager.Suite.prototype.setup = function(cx) {};
/**
* Subclassable method called to do pre-test set up.
*
* The default implementation of this method is a no-op. If your test suite
* requires some kind of pre-test setup, this is the place to do it.
*
* This can be used to avoid a bunch of boilerplate setup/teardown code in
* this suite's testcases.
*
* Any exception here will abort the remainder of the test run.
*
* @param {lib.TestManager.Result} result The result object for the upcoming
* test.
* @param {Object} cx The context object for a test run.
*/
lib.TestManager.Suite.prototype.preamble = function(result, cx) {};
/**
* Subclassable method called to do post-test tear-down.
*
* The default implementation of this method is a no-op. If your test suite
* requires some kind of pre-test setup, this is the place to do it.
*
* This can be used to avoid a bunch of boilerplate setup/teardown code in
* this suite's testcases.
*
* Any exception here will abort the remainder of the test run.
*
* @param {lib.TestManager.Result} result The result object for the finished
* test.
* @param {Object} cx The context object for a test run.
*/
lib.TestManager.Suite.prototype.postamble = function(result, cx) {};
/**
* Object representing a single test in a test suite.
*
* These are created as part of the lib.TestManager.Suite.addTest() method.
* You should never have to construct one by hand.
*
* @param {lib.TestManager.Suite} suiteClass The test suite class containing
* this test.
* @param {string} testName The local name of this test case, not including the
* test suite name.
* @param {function(lib.TestManager.Result, Object)} testFunction The function
* to invoke for this test case. This is passed a Result instance and the
* context object associated with the test run.
*
*/
lib.TestManager.Test = function(suiteClass, testName, testFunction) {
/**
* The test suite class containing this function.
*/
this.suiteClass = suiteClass;
/**
* The local name of this test, not including the test suite name.
*/
this.testName = testName;
/**
* The global name of this test, including the test suite name.
*/
this.fullName = suiteClass.suiteName + '[' + testName + ']';
// The function to call for this test.
this.testFunction_ = testFunction;
};
/**
* Execute this test.
*
* This is called by a lib.TestManager.Result instance, as part of a
* lib.TestManager.TestRun. You should not call it by hand.
*
* @param {lib.TestManager.Result} result The result object for the test.
*/
lib.TestManager.Test.prototype.run = function(result) {
try {
// Tests are applied to the parent lib.TestManager.Suite subclass.
this.testFunction_.apply(result.suite,
[result, result.testRun.cx]);
} catch (ex) {
if (ex instanceof lib.TestManager.Result.TestComplete)
return;
result.println('Test raised an exception: ' + ex);
if (ex.stack) {
if (ex.stack instanceof Array) {
result.println(ex.stack.join('\n'));
} else {
result.println(ex.stack);
}
}
result.completeTest_(result.FAILED, false);
}
};
/**
* Used to choose a set of tests and run them.
*
* It's slightly more convenient to construct one of these from
* lib.TestManager.prototype.createTestRun().
*
* @param {lib.TestManager} testManager The testManager associated with this
* TestRun.
* @param {Object} cx A context to be passed into the tests. This can be used
* to set parameters for the test suite or individual test cases.
*/
lib.TestManager.TestRun = function(testManager, cx) {
/**
* The associated lib.TestManager instance.
*/
this.testManager = testManager;
/**
* Shortcut to the lib.TestManager's log.
*/
this.log = testManager.log;
/**
* The test run context. It's entirely up to the test suite and test cases
* how this is used. It is opaque to lib.TestManager.* classes.
*/
this.cx = cx || {};
/**
* The list of test cases that encountered failures.
*/
this.failures = [];
/**
* The list of test cases that passed.
*/
this.passes = [];
/**
* The time the test run started, or null if it hasn't been started yet.
*/
this.startDate = null;
/**
* The time in milliseconds that the test run took to complete, or null if
* it hasn't completed yet.
*/
this.duration = null;
/**
* The most recent result object, or null if the test run hasn't started
* yet. In order to detect late failures, this is not cleared when the test
* completes.
*/
this.currentResult = null;
/**
* Number of maximum failures. The test run will stop when this number is
* reached. If 0 or omitted, the entire set of selected tests is run, even
* if some fail.
*/
this.maxFailures = 0;
/**
* True if this test run ended early because of an unexpected condition.
*/
this.panic = false;
// List of pending test cases.
this.testQueue_ = [];
};
/**
* This value can be passed to select() to indicate that all tests should
* be selected.
*/
lib.TestManager.TestRun.prototype.ALL_TESTS = lib.f.createEnum('<all-tests>');
/**
* Add a single test to the test run.
*/
lib.TestManager.TestRun.prototype.selectTest = function(test) {
this.testQueue_.push(test);
};
lib.TestManager.TestRun.prototype.selectSuite = function(
suiteClass, opt_pattern) {
var pattern = opt_pattern || this.ALL_TESTS;
var selectCount = 0;
var testList = suiteClass.getTestList();
for (var j = 0; j < testList.length; j++) {
var test = testList[j];
// Note that we're using "!==" rather than "!=" so that we're matching
// the ALL_TESTS String object, rather than the contents of the string.
if (pattern !== this.ALL_TESTS) {
if (pattern instanceof RegExp) {
if (!pattern.test(test.testName))
continue;
} else if (test.testName != pattern) {
continue;
}
}
this.selectTest(test);
selectCount++;
}
return selectCount;
};
/**
* Selects one or more tests to gather results for.
*
* Selecting the same test more than once is allowed.
*
* @param {string|RegExp} pattern Pattern used to select tests.
* If TestRun.prototype.ALL_TESTS, all tests are selected.
* If a string, only the test that exactly matches is selected.
* If a RegExp, only tests matching the RegExp are added.
*
* @return {int} The number of additional tests that have been selected into
* this TestRun.
*/
lib.TestManager.TestRun.prototype.selectPattern = function(pattern) {
var selectCount = 0;
for (var i = 0; i < lib.TestManager.Suite.subclasses.length; i++) {
selectCount += this.selectSuite(lib.TestManager.Suite.subclasses[i],
pattern);
}
if (!selectCount) {
this.log.warn('No tests matched selection criteria: ' + pattern);
}
return selectCount;
};
/**
* Hooked up to window.onerror during a test run in order to catch exceptions
* that would otherwise go uncaught.
*/
lib.TestManager.TestRun.prototype.onUncaughtException_ = function(
message, file, line) {
if (message.indexOf('Uncaught lib.TestManager.Result.TestComplete') == 0 ||
message.indexOf('status: passed') != -1) {
// This is a result.pass() or result.fail() call from a callback. We're
// already going to deal with it as part of the completeTest_() call
// that raised it. We can safely squelch this error message.
return true;
}
if (!this.currentResult)
return;
if (message == 'Uncaught ' + this.currentResult.expectedErrorMessage_) {
// Test cases may need to raise an unhandled exception as part of the test.
return;
}
var when = 'during';
if (this.currentResult.status != this.currentResult.PENDING)
when = 'after';
this.log.error('Uncaught exception ' + when + ' test case: ' +
this.currentResult.test.fullName);
this.log.error(message + ', ' + file + ':' + line);
this.currentResult.completeTest_(this.currentResult.FAILED, false);
return false;
};
/**
* Called to when this test run has completed.
*
* This method typically re-runs itself asynchronously, in order to let the
* DOM stabilize and short-term timeouts to complete before declaring the
* test run complete.
*
* @param {boolean} opt_skipTimeout If true, the timeout is skipped and the
* test run is completed immediately. This should only be used from within
* this function.
*/
lib.TestManager.TestRun.prototype.onTestRunComplete_ = function(
opt_skipTimeout) {
if (!opt_skipTimeout) {
// The final test may have left a lingering setTimeout(..., 0), or maybe
// poked at the DOM in a way that will trigger a event to fire at the end
// of this stack, so we give things a chance to settle down before our
// final cleanup...
setTimeout(this.onTestRunComplete_.bind(this), 0, true);
return;
}
this.duration = (new Date()) - this.startDate;
this.log.groupEnd();
this.log.info(this.passes.length + ' passed, ' +
this.failures.length + ' failed, ' +
this.msToSeconds_(this.duration));
this.summarize();
window.onerror = null;
this.testManager.onTestRunComplete(this);
};
/**
* Called by the lib.TestManager.Result object when a test completes.
*
* @param {lib.TestManager.Result} result The result object which has just
* completed.
*/
lib.TestManager.TestRun.prototype.onResultComplete = function(result) {
try {
this.testManager.testPostamble(result, this.cx);
result.suite.postamble(result, this.ctx);
} catch (ex) {
this.log.error('Unexpected exception in postamble: ' +
(ex.stack ? ex.stack : ex));
this.panic = true;
}
if (result.status != result.PASSED)
this.log.error(result.status);
else if (result.duration > 500)
this.log.warn('Slow test took ' + this.msToSeconds_(result.duration));
this.log.groupEnd();
if (result.status == result.FAILED) {
this.failures.push(result);
this.currentSuite = null;
} else if (result.status == result.PASSED) {
this.passes.push(result);
} else {
this.log.error('Unknown result status: ' + result.test.fullName + ': ' +
result.status);
this.panic = true;
return;
}
this.runNextTest_();
};
/**
* Called by the lib.TestManager.Result object when a test which has already
* completed reports another completion.
*
* This is usually indicative of a buggy testcase. It is probably reporting a
* result on exit and then again from an asynchronous callback.
*
* It may also be the case that the last act of the testcase causes a DOM change
* which triggers some event to run after the test returns. If the event
* handler reports a failure or raises an uncaught exception, the test will
* fail even though it has already completed.
*
* In any case, re-completing a test ALWAYS moves it into the failure pile.
*
* @param {lib.TestManager.Result} result The result object which has just
* completed.
* @param {string} lateStatus The status that the test attempted to record this
* time around.
*/
lib.TestManager.TestRun.prototype.onResultReComplete = function(
result, lateStatus) {
this.log.error('Late complete for test: ' + result.test.fullName + ': ' +
lateStatus);
// Consider any late completion a failure, even if it's a double-pass, since
// it's a misuse of the testing API.
var index = this.passes.indexOf(result);
if (index >= 0) {
this.passes.splice(index, 1);
this.failures.push(result);
}
};
/**
* Run the next test in the queue.
*/
lib.TestManager.TestRun.prototype.runNextTest_ = function() {
if (this.panic || !this.testQueue_.length) {
this.onTestRunComplete_();
return;
}
if (this.maxFailures && this.failures.length >= this.maxFailures) {
this.log.error('Maximum failure count reached, aborting test run.');
this.onTestRunComplete_();
return;
}
// Peek at the top test first. We remove it later just before it's about
// to run, so that we don't disturb the incomplete test count in the
// event that we fail before running it.
var test = this.testQueue_[0];
var suite = this.currentResult ? this.currentResult.suite : null;
try {
if (!suite || !(suite instanceof test.suiteClass)) {
if (suite)
this.log.groupEnd();
this.log.group(test.suiteClass.suiteName);
suite = new test.suiteClass(this.testManager, this.cx);
}
} catch (ex) {
// If test suite setup fails we're not even going to try to run the tests.
this.log.error('Exception during setup: ' + (ex.stack ? ex.stack : ex));
this.panic = true;
this.onTestRunComplete_();
return;
}
try {
this.log.group(test.testName);
this.currentResult = new lib.TestManager.Result(this, suite, test);
this.testManager.testPreamble(this.currentResult, this.cx);
suite.preamble(this.currentResult, this.cx);
this.testQueue_.shift();
} catch (ex) {
this.log.error('Unexpected exception during test preamble: ' +
(ex.stack ? ex.stack : ex));
this.log.groupEnd();
this.panic = true;
this.onTestRunComplete_();
return;
}
try {
this.currentResult.run();
} catch (ex) {
// Result.run() should catch test exceptions and turn them into failures.
// If we got here, it means there is trouble in the testing framework.
this.log.error('Unexpected exception during test run: ' +
(ex.stack ? ex.stack : ex));
this.panic = true;
}
};
/**
* Run the selected list of tests.
*
* Some tests may need to run asynchronously, so you cannot assume the run is
* complete when this function returns. Instead, pass in a function to be
* called back when the run has completed.
*
* This function will log the results of the test run as they happen into the
* log defined by the associated lib.TestManager. By default this is
* console.log, which can be viewed in the JavaScript console of most browsers.
*
* The browser state is determined by the last test to run. We intentionally
* don't do any cleanup so that you can inspect the state of a failed test, or
* leave the browser ready for manual testing.
*
* Any failures in lib.TestManager.* code or test suite setup or test case
* preamble will cause the test run to abort.
*/
lib.TestManager.TestRun.prototype.run = function() {
this.log.info('Running ' + this.testQueue_.length + ' test(s)');
window.onerror = this.onUncaughtException_.bind(this);
this.startDate = new Date();
this.runNextTest_();
};
/**
* Format milliseconds as fractional seconds.
*/
lib.TestManager.TestRun.prototype.msToSeconds_ = function(ms) {
var secs = (ms / 1000).toFixed(2);
return secs + 's';
};
/**
* Log the current result summary.
*/
lib.TestManager.TestRun.prototype.summarize = function() {
if (this.failures.length) {
for (var i = 0; i < this.failures.length; i++) {
this.log.error('FAILED: ' + this.failures[i].test.fullName);
}
}
if (this.testQueue_.length) {
this.log.warn('Test run incomplete: ' + this.testQueue_.length +
' test(s) were not run.');
}
};
/**
* Record of the result of a single test.
*
* These are constructed during a test run, you shouldn't have to make one
* on your own.
*
* An instance of this class is passed in to each test function. It can be
* used to add messages to the test log, to record a test pass/fail state, to
* test assertions, or to create exception-proof wrappers for callback
* functions.
*
* @param {lib.TestManager.TestRun} testRun The TestRun instance associated with
* this result.
* @param {lib.TestManager.Suit} suite The Suite containing the test we're
* collecting this result for.
* @param {lib.TestManager.Test} test The test we're collecting this result for.
*/
lib.TestManager.Result = function(testRun, suite, test) {
/**
* The TestRun instance associated with this result.
*/
this.testRun = testRun;
/**
* The Suite containing the test we're collecting this result for.
*/
this.suite = suite;
/**
* The test we're collecting this result for.
*/
this.test = test;
/**
* The time we started to collect this result, or null if we haven't started.
*/
this.startDate = null;
/**
* The time in milliseconds that the test took to complete, or null if
* it hasn't completed yet.
*/
this.duration = null;
/**
* The current status of this test result.
*/
this.status = this.PENDING;
// An error message that the test case is expected to generate.
this.expectedErrorMessage_ = null;
};
/**
* Possible values for this.status.
*/
lib.TestManager.Result.prototype.PENDING = 'pending';
lib.TestManager.Result.prototype.FAILED = 'FAILED';
lib.TestManager.Result.prototype.PASSED = 'passed';
/**
* Exception thrown when a test completes (pass or fail), to ensure no more of
* the test is run.
*/
lib.TestManager.Result.TestComplete = function(result) {
this.result = result;
};
lib.TestManager.Result.TestComplete.prototype.toString = function() {
return 'lib.TestManager.Result.TestComplete: ' + this.result.test.fullName +
', status: ' + this.result.status;
};
/**
* Start the test associated with this result.
*/
lib.TestManager.Result.prototype.run = function() {
this.startDate = new Date();
this.test.run(this);
if (this.status == this.PENDING && !this.timeout_) {
this.println('Test did not return a value and did not request more time.');
this.completeTest_(this.FAILED, false);
}
};
/**
* Unhandled error message this test expects to generate.
*
* This must be the exact string that would appear in the JavaScript console,
* minus the 'Uncaught ' prefix.
*
* The test case does *not* automatically fail if the error message is not
* encountered.
*/
lib.TestManager.Result.prototype.expectErrorMessage = function(str) {
this.expectedErrorMessage_ = str;
};
/**
* Function called when a test times out.
*/
lib.TestManager.Result.prototype.onTimeout_ = function() {
this.timeout_ = null;
if (this.status != this.PENDING)
return;
this.println('Test timed out.');
this.completeTest_(this.FAILED, false);
};
/**
* Indicate that a test case needs more time to complete.
*
* Before a test case returns it must report a pass/fail result, or request more
* time to do so.
*
* If a test does not report pass/fail before the time expires it will
* be reported as a timeout failure. Any late pass/fails will be noted in the
* test log, but will not affect the final result of the test.
*
* Test cases may call requestTime more than once. If you have a few layers
* of asynchronous API to go through, you should call this once per layer with
* an estimate of how long each callback will take to complete.
*
* @param {int} ms Number of milliseconds requested.
*/
lib.TestManager.Result.prototype.requestTime = function(ms) {
if (this.timeout_)
clearTimeout(this.timeout_);
this.timeout_ = setTimeout(this.onTimeout_.bind(this), ms);
};
/**
* Report the completion of a test.
*
* @param {string} status The status of the test case.
* @param {boolean} opt_throw Optional boolean indicating whether or not
* to throw the TestComplete exception.
*/
lib.TestManager.Result.prototype.completeTest_ = function(status, opt_throw) {
if (this.status == this.PENDING) {
this.duration = (new Date()) - this.startDate;
this.status = status;
this.testRun.onResultComplete(this);
} else {
this.testRun.onResultReComplete(this, status);
}
if (arguments.length < 2 || opt_throw)
throw new lib.TestManager.Result.TestComplete(this);
};
/**
* Check that two arrays are equal.
*/
lib.TestManager.Result.prototype.arrayEQ_ = function(actual, expected) {
if (!actual || !expected)
return (!actual && !expected);
if (actual.length != expected.length)
return false;
for (var i = 0; i < actual.length; ++i)
if (actual[i] != expected[i])
return false;
return true;
};
/**
* Assert that an actual value is exactly equal to the expected value.
*
* This uses the JavaScript '===' operator in order to avoid type coercion.
*
* If the assertion fails, the test is marked as a failure and a TestCompleted
* exception is thrown.
*
* @param {*} actual The actual measured value.
* @param {*} expected The value expected.
* @param {string} opt_name An optional name used to identify this
* assertion in the test log. If omitted it will be the file:line
* of the caller.
*/
lib.TestManager.Result.prototype.assertEQ = function(
actual, expected, opt_name) {
// Utility function to pretty up the log.
function format(value) {
if (typeof value == 'number')
return value;
var str = String(value);
var ary = str.split('\n').map((e) => JSON.stringify(e));
if (ary.length > 1) {
// If the string has newlines, start it off on its own line so that
// it's easier to compare against another string with newlines.
return '\n' + ary.join('\n');
} else {
return ary.join('\n');
}
}
if (actual === expected)
return;
// Deal with common object types since JavaScript can't.
if (expected instanceof Array)
if (this.arrayEQ_(actual, expected))
return;
var name = opt_name ? '[' + opt_name + ']' : '';
this.fail('assertEQ' + name + ': ' + this.getCallerLocation_(1) + ': ' +
format(actual) + ' !== ' + format(expected));
};
/**
* Assert that a value is true.
*
* This uses the JavaScript '===' operator in order to avoid type coercion.
* The must be the boolean value `true`, not just some "truish" value.
*
* If the assertion fails, the test is marked as a failure and a TestCompleted
* exception is thrown.
*
* @param {boolean} actual The actual measured value.
* @param {string} opt_name An optional name used to identify this
* assertion in the test log. If omitted it will be the file:line
* of the caller.
*/
lib.TestManager.Result.prototype.assert = function(actual, opt_name) {
if (actual === true)
return;
var name = opt_name ? '[' + opt_name + ']' : '';
this.fail('assert' + name + ': ' + this.getCallerLocation_(1) + ': ' +
String(actual));
};
/**
* Return the filename:line of a calling stack frame.
*
* This uses a dirty hack. It throws an exception, catches it, and examines
* the stack property of the caught exception.
*
* @param {int} frameIndex The stack frame to return. 0 is the frame that
* called this method, 1 is its caller, and so on.
* @return {string} A string of the format "filename:linenumber".
*/
lib.TestManager.Result.prototype.getCallerLocation_ = function(frameIndex) {
try {
throw new Error();
} catch (ex) {
var frame = ex.stack.split('\n')[frameIndex + 2];
var ary = frame.match(/([^/]+:\d+):\d+\)?$/);
return ary ? ary[1] : '???';
}
};
/**
* Write a message to the result log.
*/
lib.TestManager.Result.prototype.println = function(message) {
this.testRun.log.info(message);
};
/**
* Mark a failed test and exit out of the rest of the test.
*
* This will throw a TestCompleted exception, causing the current test to stop.
*
* @param {string} opt_message Optional message to add to the log.
*/
lib.TestManager.Result.prototype.fail = function(opt_message) {
if (arguments.length)
this.println(opt_message);
this.completeTest_(this.FAILED, true);
};
/**
* Mark a passed test and exit out of the rest of the test.
*
* This will throw a TestCompleted exception, causing the current test to stop.
*/
lib.TestManager.Result.prototype.pass = function() {
this.completeTest_(this.PASSED, true);
};