/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ // NOTE: If you're adding new test harness functionality -- first, should you // at all? Most stuff is better in specific tests, or in nested shell.js // or browser.js. Second, supposing you should, please add it to this // IIFE for better modularity/resilience against tests that must do // particularly bizarre things that might break the harness. (function(global) { "use strict"; /********************************************************************** * CACHED PRIMORDIAL FUNCTIONALITY (before a test might overwrite it) * **********************************************************************/ var undefined; // sigh var Error = global.Error; var Function = global.Function; var Number = global.Number; var RegExp = global.RegExp; var String = global.String; var Symbol = global.Symbol; var TypeError = global.TypeError; var ArrayIsArray = global.Array.isArray; var MathAbs = global.Math.abs; var ObjectCreate = global.Object.create; var ObjectDefineProperty = global.Object.defineProperty; var ReflectApply = global.Reflect.apply; var RegExpPrototypeExec = global.RegExp.prototype.exec; var StringPrototypeCharCodeAt = global.String.prototype.charCodeAt; var StringPrototypeIndexOf = global.String.prototype.indexOf; var StringPrototypeSubstring = global.String.prototype.substring; var runningInBrowser = typeof global.window !== "undefined"; if (runningInBrowser) { // Certain cached functionality only exists (and is only needed) when // running in the browser. Segregate that caching here. var SpecialPowersSetGCZeal = global.SpecialPowers ? global.SpecialPowers.setGCZeal : undefined; } var evaluate = global.evaluate; var options = global.options; /**************************** * GENERAL HELPER FUNCTIONS * ****************************/ // We *cannot* use Array.prototype.push for this, because that function sets // the new trailing element, which could invoke a setter (left by a test) on // Array.prototype or Object.prototype. function ArrayPush(arr, val) { assertEq(ArrayIsArray(arr), true, "ArrayPush must only be used on actual arrays"); var desc = ObjectCreate(null); desc.value = val; desc.enumerable = true; desc.configurable = true; desc.writable = true; ObjectDefineProperty(arr, arr.length, desc); } function StringCharCodeAt(str, index) { return ReflectApply(StringPrototypeCharCodeAt, str, [index]); } function StringSplit(str, delimiter) { assertEq(typeof str === "string" && typeof delimiter === "string", true, "StringSplit must be called with two string arguments"); assertEq(delimiter.length > 0, true, "StringSplit doesn't support an empty delimiter string"); var parts = []; var last = 0; while (true) { var i = ReflectApply(StringPrototypeIndexOf, str, [delimiter, last]); if (i < 0) { if (last < str.length) ArrayPush(parts, ReflectApply(StringPrototypeSubstring, str, [last])); return parts; } ArrayPush(parts, ReflectApply(StringPrototypeSubstring, str, [last, i])); last = i + delimiter.length; } } function shellOptionsClear() { assertEq(runningInBrowser, false, "Only called when running in the shell."); // Return early if no options are set. var currentOptions = options ? options() : ""; if (currentOptions === "") return; // Turn off current settings. var optionNames = StringSplit(currentOptions, ","); for (var i = 0; i < optionNames.length; i++) { options(optionNames[i]); } } /**************************** * TESTING FUNCTION EXPORTS * ****************************/ function SameValue(v1, v2) { // We could |return Object.is(v1, v2);|, but that's less portable. if (v1 === 0 && v2 === 0) return 1 / v1 === 1 / v2; if (v1 !== v1 && v2 !== v2) return true; return v1 === v2; } var assertEq = global.assertEq; if (typeof assertEq !== "function") { assertEq = function assertEq(actual, expected, message) { if (!SameValue(actual, expected)) { throw new TypeError(`Assertion failed: got "${actual}", expected "${expected}"` + (message ? ": " + message : "")); } }; global.assertEq = assertEq; } function assertEqArray(actual, expected) { var len = actual.length; assertEq(len, expected.length, "mismatching array lengths"); var i = 0; try { for (; i < len; i++) assertEq(actual[i], expected[i], "mismatch at element " + i); } catch (e) { throw new Error(`Exception thrown at index ${i}: ${e}`); } } global.assertEqArray = assertEqArray; function assertThrows(f) { if (arguments.length != 1) { throw new Error("Too many arguments to assertThrows; maybe you meant assertThrowsInstanceOf?"); } var ok = false; try { f(); } catch (exc) { ok = true; } if (!ok) throw new Error(`Assertion failed: ${f} did not throw as expected`); } global.assertThrows = assertThrows; function assertThrowsInstanceOf(f, ctor, msg) { var fullmsg; try { f(); } catch (exc) { if (exc instanceof ctor) return; fullmsg = `Assertion failed: expected exception ${ctor.name}, got ${exc}`; } if (fullmsg === undefined) fullmsg = `Assertion failed: expected exception ${ctor.name}, no exception thrown`; if (msg !== undefined) fullmsg += " - " + msg; throw new Error(fullmsg); } global.assertThrowsInstanceOf = assertThrowsInstanceOf; /**************************** * UTILITY FUNCTION EXPORTS * ****************************/ var dump = global.dump; if (typeof global.dump === "function") { // A presumptively-functional |dump| exists, so no need to do anything. } else { // We don't have |dump|. Try to simulate the desired effect another way. if (runningInBrowser) { // We can't actually print to the console: |global.print| invokes browser // printing functionality here (it's overwritten just below), and // |global.dump| isn't a function that'll dump to the console (presumably // because the preference to enable |dump| wasn't set). Just make it a // no-op. dump = function() {}; } else { // |print| prints to stdout: make |dump| do likewise. dump = global.print; } global.dump = dump; } var print; if (runningInBrowser) { // We're executing in a browser. Using |global.print| would invoke browser // printing functionality: not what tests want! Instead, use a print // function that syncs up with the test harness and console. print = function print() { var s = "TEST-INFO | "; for (var i = 0; i < arguments.length; i++) s += String(arguments[i]) + " "; // Dump the string to the console for developers and the harness. dump(s + "\n"); // AddPrintOutput doesn't require HTML special characters be escaped. global.AddPrintOutput(s); }; global.print = print; } else { // We're executing in a shell, and |global.print| is the desired function. print = global.print; } var gczeal = global.gczeal; if (typeof gczeal !== "function") { if (typeof SpecialPowersSetGCZeal === "function") { gczeal = function gczeal(z) { SpecialPowersSetGCZeal(z); }; } else { gczeal = function() {}; // no-op if not available } global.gczeal = gczeal; } // Evaluates the given source code as global script code. browser.js provides // a different implementation for this function. var evaluateScript = global.evaluateScript; if (typeof evaluate === "function" && typeof evaluateScript !== "function") { evaluateScript = function evaluateScript(code) { evaluate(String(code)); }; global.evaluateScript = evaluateScript; } function toPrinted(value) { value = String(value); var digits = "0123456789ABCDEF"; var result = ""; for (var i = 0; i < value.length; i++) { var ch = StringCharCodeAt(value, i); if (ch === 0x5C && i + 1 < value.length) { var d = value[i + 1]; if (d === "n") { result += "NL"; i++; } else if (d === "r") { result += "CR"; i++; } else { result += "\\"; } } else if (ch === 0x0A) { result += "NL"; } else if (ch < 0x20 || ch > 0x7E) { var a = digits[ch & 0xf]; ch >>= 4; var b = digits[ch & 0xf]; ch >>= 4; if (ch) { var c = digits[ch & 0xf]; ch >>= 4; var d = digits[ch & 0xf]; result += "\\u" + d + c + b + a; } else { result += "\\x" + b + a; } } else { result += value[i]; } } return result; } /* * An xorshift pseudo-random number generator see: * https://en.wikipedia.org/wiki/Xorshift#xorshift.2A * This generator will always produce a value, n, where * 0 <= n <= 255 */ function *XorShiftGenerator(seed, size) { let x = seed; for (let i = 0; i < size; i++) { x ^= x >> 12; x ^= x << 25; x ^= x >> 27; yield x % 256; } } global.XorShiftGenerator = XorShiftGenerator; /************************************************************************* * HARNESS-CENTRIC EXPORTS (we should generally work to eliminate these) * *************************************************************************/ var PASSED = " PASSED! "; var FAILED = " FAILED! "; /* * Same as `new TestCase(description, expect, actual)`, except it doesn't * return the newly created test case object. */ function AddTestCase(description, expect, actual) { new TestCase(description, expect, actual); } global.AddTestCase = AddTestCase; var testCasesArray = []; function TestCase(d, e, a, r) { this.description = d; this.expect = e; this.actual = a; this.passed = getTestCaseResult(e, a); this.reason = typeof r !== 'undefined' ? String(r) : ''; ArrayPush(testCasesArray, this); } global.TestCase = TestCase; TestCase.prototype = ObjectCreate(null); TestCase.prototype.testPassed = (function TestCase_testPassed() { return this.passed; }); TestCase.prototype.testFailed = (function TestCase_testFailed() { return !this.passed; }); TestCase.prototype.testDescription = (function TestCase_testDescription() { return this.description + ' ' + this.reason; }); function getTestCaseResult(expected, actual) { if (typeof expected !== typeof actual) return false; if (typeof expected !== 'number') // Note that many tests depend on the use of '==' here, not '==='. return actual == expected; // Distinguish NaN from other values. Using x !== x comparisons here // works even if tests redefine isNaN. if (actual !== actual) return expected !== expected; if (expected !== expected) return false; // Tolerate a certain degree of error. if (actual !== expected) return MathAbs(actual - expected) <= 1E-10; // Here would be a good place to distinguish 0 and -0, if we wanted // to. However, doing so would introduce a number of failures in // areas where they don't seem important. For example, the WeekDay // function in ECMA-262 returns -0 for Sundays before the epoch, but // the Date functions in SpiderMonkey specified in terms of WeekDay // often don't. This seems unimportant. return true; } function reportTestCaseResult(description, expected, actual, output) { var testcase = new TestCase(description, expected, actual, output); // if running under reftest, let it handle result reporting. if (!runningInBrowser) { if (testcase.passed) { print(PASSED + description); } else { reportFailure(description + " : " + output); } } } function getTestCases() { return testCasesArray; } global.getTestCases = getTestCases; /* * The test driver searches for such a phrase in the test output. * If such phrase exists, it will set n as the expected exit code. */ function expectExitCode(n) { print('--- NOTE: IN THIS TESTCASE, WE EXPECT EXIT CODE ' + n + ' ---'); } global.expectExitCode = expectExitCode; /* * Statuses current section of a test */ function inSection(x) { return "Section " + x + " of test - "; } global.inSection = inSection; /* * Report a failure in the 'accepted' manner */ function reportFailure(msg) { msg = String(msg); var lines = StringSplit(msg, "\n"); for (var i = 0; i < lines.length; i++) print(FAILED + " " + lines[i]); } global.reportFailure = reportFailure; /* * Print a non-failure message. */ function printStatus(msg) { msg = String(msg); var lines = StringSplit(msg, "\n"); for (var i = 0; i < lines.length; i++) print("STATUS: " + lines[i]); } global.printStatus = printStatus; /* * Print a bugnumber message. */ function printBugNumber(num) { print('BUGNUMBER: ' + num); } global.printBugNumber = printBugNumber; /* * Compare expected result to actual result, if they differ (in value and/or * type) report a failure. If description is provided, include it in the * failure report. */ function reportCompare(expected, actual, description) { var expected_t = typeof expected; var actual_t = typeof actual; var output = ""; if (typeof description === "undefined") description = ""; if (expected_t !== actual_t) output += `Type mismatch, expected type ${expected_t}, actual type ${actual_t} `; if (expected != actual) output += `Expected value '${toPrinted(expected)}', Actual value '${toPrinted(actual)}' `; reportTestCaseResult(description, expected, actual, output); } global.reportCompare = reportCompare; /* * Attempt to match a regular expression describing the result to * the actual result, if they differ (in value and/or * type) report a failure. If description is provided, include it in the * failure report. */ function reportMatch(expectedRegExp, actual, description) { var expected_t = "string"; var actual_t = typeof actual; var output = ""; if (typeof description === "undefined") description = ""; if (expected_t !== actual_t) output += `Type mismatch, expected type ${expected_t}, actual type ${actual_t} `; var matches = ReflectApply(RegExpPrototypeExec, expectedRegExp, [actual]) !== null; if (!matches) { output += `Expected match to '${toPrinted(expectedRegExp)}', Actual value '${toPrinted(actual)}' `; } reportTestCaseResult(description, true, matches, output); } global.reportMatch = reportMatch; function compareSource(expect, actual, summary) { // compare source var expectP = String(expect); var actualP = String(actual); print('expect:\n' + expectP); print('actual:\n' + actualP); reportCompare(expectP, actualP, summary); // actual must be compilable if expect is? try { var expectCompile = 'No Error'; var actualCompile; Function(expect); try { Function(actual); actualCompile = 'No Error'; } catch(ex1) { actualCompile = ex1 + ''; } reportCompare(expectCompile, actualCompile, summary + ': compile actual'); } catch(ex) { } } global.compareSource = compareSource; function test() { var testCases = getTestCases(); for (var i = 0; i < testCases.length; i++) { var testCase = testCases[i]; testCase.reason += testCase.passed ? "" : "wrong value "; // if running under reftest, let it handle result reporting. if (!runningInBrowser) { var message = `${testCase.description} = ${testCase.actual} expected: ${testCase.expect}`; print((testCase.passed ? PASSED : FAILED) + message); } } } global.test = test; // This function uses the shell's print function. When running tests in the // browser, browser.js overrides this function to write to the page. function writeHeaderToLog(string) { print(string); } global.writeHeaderToLog = writeHeaderToLog; /************************************ * PROMISE TESTING FUNCTION EXPORTS * ************************************/ function getPromiseResult(promise) { var result, error, caught = false; promise.then(r => { result = r; }, e => { caught = true; error = e; }); drainJobQueue(); if (caught) throw error; return result; } global.getPromiseResult = getPromiseResult; function assertEventuallyEq(promise, expected) { assertEq(getPromiseResult(promise), expected); } global.assertEventuallyEq = assertEventuallyEq; function assertEventuallyThrows(promise, expectedErrorType) { assertThrowsInstanceOf(() => getPromiseResult(promise), expectedErrorType); }; global.assertEventuallyThrows = assertEventuallyThrows; function assertEventuallyDeepEq(promise, expected) { assertDeepEq(getPromiseResult(promise), expected); }; global.assertEventuallyDeepEq = assertEventuallyDeepEq; /******************************************* * RUN ONCE CODE TO SETUP ADDITIONAL STATE * *******************************************/ // Clear all options before running any tests. browser.js performs this // set-up as part of its jsTestDriverBrowserInit function. if (!runningInBrowser) shellOptionsClear(); })(this); var DESCRIPTION;