/* * 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; } };