blob: 5e8192ec9b074e29c0a74033338ef54f298a8a71 [file] [log] [blame]
/*
* Copyright (C) 2015 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
TestSuite = class TestSuite
{
constructor(harness, name) {
if (!(harness instanceof TestHarness))
throw new Error("Must pass the test's harness as the first argument.");
if (typeof name !== "string" || !name.trim().length)
throw new Error("Tried to create TestSuite without string suite name.");
this.name = name;
this._harness = harness;
this.testcases = [];
this.runCount = 0;
this.failCount = 0;
}
// Use this if the test file only has one suite, and no handling
// of the value returned by runTestCases() is needed.
runTestCasesAndFinish()
{
throw new Error("Must be implemented by subclasses.");
}
runTestCases()
{
throw new Error("Must be implemented by subclasses.");
}
get passCount()
{
return this.runCount - (this.failCount - this.skipCount);
}
get skipCount()
{
if (this.failCount)
return this.testcases.length - this.runCount;
else
return 0;
}
addTestCase(testcase)
{
if (!testcase || !(testcase instanceof Object))
throw new Error("Tried to add non-object test case.");
if (typeof testcase.name !== "string" || !testcase.name.trim().length)
throw new Error("Tried to add test case without a name.");
if (typeof testcase.test !== "function")
throw new Error("Tried to add test case without `test` function.");
if (testcase.setup && typeof testcase.setup !== "function")
throw new Error("Tried to add test case with invalid `setup` parameter (must be a function).");
if (testcase.teardown && typeof testcase.teardown !== "function")
throw new Error("Tried to add test case with invalid `teardown` parameter (must be a function).");
this.testcases.push(testcase);
}
// Protected
logThrownObject(e)
{
let message = e;
let stack = "(unknown)";
if (e instanceof Error) {
message = e.message;
if (e.stack)
stack = e.stack;
}
if (typeof message !== "string")
message = JSON.stringify(message);
let sanitizedStack = this._harness.sanitizeStack(stack);
let result = `!! EXCEPTION: ${message}`;
if (stack)
result += `\nStack Trace: ${sanitizedStack}`;
this._harness.log(result);
}
};
AsyncTestSuite = class AsyncTestSuite extends TestSuite
{
runTestCasesAndFinish()
{
let finish = () => { this._harness.completeTest(); };
this.runTestCases()
.then(finish)
.catch(finish);
}
runTestCases()
{
if (!this.testcases.length)
throw new Error("Tried to call runTestCases() for suite with no test cases");
if (this._startedRunning)
throw new Error("Tried to call runTestCases() more than once.");
this._startedRunning = true;
this._harness.log("");
this._harness.log(`== Running test suite: ${this.name}`);
// Avoid adding newlines if nothing was logged.
let priorLogCount = this._harness.logCount;
return Promise.resolve().then(() => Promise.chain(this.testcases.map((testcase, i) => () => new Promise(async (resolve, reject) => {
if (i > 0 && priorLogCount < this._harness.logCount)
this._harness.log("");
priorLogCount = this._harness.logCount;
let hasTimeout = testcase.timeout !== -1;
let timeoutId = undefined;
if (hasTimeout) {
let delay = testcase.timeout || 10000;
timeoutId = setTimeout(() => {
if (!timeoutId)
return;
timeoutId = undefined;
this.failCount++;
this._harness.log(`!! TIMEOUT: took longer than ${delay}ms`);
resolve();
}, delay);
}
try {
if (testcase.setup) {
this._harness.log("-- Running test setup.");
priorLogCount++;
if (testcase.setup[Symbol.toStringTag] === "AsyncFunction")
await testcase.setup();
else
await new Promise(testcase.setup);
}
this.runCount++;
this._harness.log(`-- Running test case: ${testcase.name}`);
priorLogCount++;
if (testcase.test[Symbol.toStringTag] === "AsyncFunction")
await testcase.test();
else
await new Promise(testcase.test);
if (testcase.teardown) {
this._harness.log("-- Running test teardown.");
priorLogCount++;
if (testcase.teardown[Symbol.toStringTag] === "AsyncFunction")
await testcase.teardown();
else
await new Promise(testcase.teardown);
}
} catch (e) {
this.failCount++;
this.logThrownObject(e);
}
if (!hasTimeout || timeoutId) {
clearTimeout(timeoutId);
timeoutId = undefined;
resolve();
}
})))
// Clear result value.
.then(() => {}));
}
};
SyncTestSuite = class SyncTestSuite extends TestSuite
{
addTestCase(testcase)
{
if ([testcase.setup, testcase.teardown, testcase.test].some((fn) => fn && fn[Symbol.toStringTag] === "AsyncFunction"))
throw new Error("Tried to pass a test case with an async `setup`, `test`, or `teardown` function, but this is a synchronous test suite.");
super.addTestCase(testcase);
}
runTestCasesAndFinish()
{
this.runTestCases();
this._harness.completeTest();
}
runTestCases()
{
if (!this.testcases.length)
throw new Error("Tried to call runTestCases() for suite with no test cases");
if (this._startedRunning)
throw new Error("Tried to call runTestCases() more than once.");
this._startedRunning = true;
this._harness.log("");
this._harness.log(`== Running test suite: ${this.name}`);
let priorLogCount = this._harness.logCount;
for (let i = 0; i < this.testcases.length; i++) {
let testcase = this.testcases[i];
if (i > 0 && priorLogCount < this._harness.logCount)
this._harness.log("");
priorLogCount = this._harness.logCount;
try {
// Run the setup action, if one was provided.
if (testcase.setup) {
this._harness.log("-- Running test setup.");
priorLogCount++;
let setupResult = testcase.setup();
if (setupResult === false) {
this._harness.log("!! SETUP FAILED");
this.failCount++;
continue;
}
}
this.runCount++;
this._harness.log(`-- Running test case: ${testcase.name}`);
priorLogCount++;
let testResult = testcase.test();
if (testResult === false) {
this.failCount++;
continue;
}
// Run the teardown action, if one was provided.
if (testcase.teardown) {
this._harness.log("-- Running test teardown.");
priorLogCount++;
let teardownResult = testcase.teardown();
if (teardownResult === false) {
this._harness.log("!! TEARDOWN FAILED");
this.failCount++;
continue;
}
}
} catch (e) {
this.failCount++;
this.logThrownObject(e);
}
}
return true;
}
};