/* -*- 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) { /********************************************************************** * CACHED PRIMORDIAL FUNCTIONALITY (before a test might overwrite it) * **********************************************************************/ var undefined; // sigh var Error = global.Error; var Number = global.Number; var String = global.String; var TypeError = global.TypeError; var ArrayIsArray = global.Array.isArray; var ObjectCreate = global.Object.create; var ObjectDefineProperty = global.Object.defineProperty; var ReflectApply = global.Reflect.apply; var StringPrototypeEndsWith = global.String.prototype.endsWith; 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 runningInShell = typeof window === "undefined"; /**************************** * GENERAL HELPER FUNCTIONS * ****************************/ // We could use Array.prototype.pop, but we don't so it's clear exactly what // dependencies this function has on test-modifiable behavior (i.e. none). function ArrayPop(arr) { assertEq(ArrayIsArray(arr), true, "ArrayPop must only be used on actual arrays"); var len = arr.length; if (len === 0) return undefined; var v = arr[len - 1]; arr.length--; return v; } // 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 StringEndsWith(str, needle) { return ReflectApply(StringPrototypeEndsWith, str, [needle]); } /**************************** * 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) { 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 quit = global.quit; if (typeof quit !== "function") { // XXX There's something very strange about quit() in browser runs being a // function that doesn't quit at all (!). We should get rid of quit() // as an integral part of tests in favor of something else. quit = function quit() {}; global.quit = quit; } /****************************************************** * TEST METADATA EXPORTS (these are of dubious value) * ******************************************************/ global.SECTION = ""; global.VERSION = ""; global.BUGNUMBER = ""; /************************************************************************* * HARNESS-CENTRIC EXPORTS (we should generally work to eliminate these) * *************************************************************************/ var PASSED = " PASSED! "; global.PASSED = PASSED; var FAILED = " FAILED! "; global.FAILED = FAILED; /** Set up test environment. */ function startTest() { if (global.BUGNUMBER) global.print("BUGNUMBER: " + global.BUGNUMBER); } global.startTest = startTest; var callStack = []; /** * Puts funcName at the top of the call stack. This stack is used to show * a function-reported-from field when reporting failures. */ function enterFunc(funcName) { assertEq(typeof funcName, "string", "enterFunc must be given a string funcName"); if (!StringEndsWith(funcName, "()")) funcName += "()"; ArrayPush(callStack, funcName); } global.enterFunc = enterFunc; /** * Pops the top funcName off the call stack. funcName, if provided, is used * to check push-pop balance. */ function exitFunc(funcName) { assertEq(typeof funcName === "string" || typeof funcName === "undefined", true, "exitFunc must be given no arguments or a string"); var lastFunc = ArrayPop(callStack); assertEq(typeof lastFunc, "string", "exitFunc called too many times"); if (funcName) { if (!StringEndsWith(funcName, "()")) funcName += "()"; if (lastFunc !== funcName) { // XXX Eliminate this dependency on global.reportCompare's identity. global.reportCompare(funcName, lastFunc, "Test driver failure wrong exit function "); } } } global.exitFunc = exitFunc; /** Peeks at the top of the call stack. */ function currentFunc() { if (callStack.length == 0) return "top level script"; return callStack[callStack.length - 1]; } global.currentFunc = currentFunc; // XXX This function is *only* used in harness functions and really shouldn't // be exported. var writeFormattedResult = function writeFormattedResult(expect, actual, string, passed) { print((passed ? PASSED : FAILED) + string + ' expected: ' + expect); }; global.writeFormattedResult = writeFormattedResult; /***************************************************** * RHINO-SPECIFIC EXPORTS (are these used any more?) * *****************************************************/ function inRhino() { return typeof global.defineClass === "function"; } global.inRhino = inRhino; function GetContext() { return global.Packages.com.netscape.javascript.Context.getCurrentContext(); } global.GetContext = GetContext; function OptLevel(i) { i = Number(i); var cx = GetContext(); cx.setOptimizationLevel(i); } global.OptLevel = OptLevel; })(this); (function(global) { 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; } 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; })(this); var STATUS = "STATUS: "; var gDelayTestDriverEnd = false; var gTestcases = new Array(); var gTc = gTestcases.length; var summary = ''; var description = ''; var expected = ''; var actual = ''; var msg = ''; /* * constant strings */ var GLOBAL = this + ''; var DESCRIPTION; var EXPECTED; /* * Signals to results.py that the current test case should be considered to * have passed if it doesn't throw an exception. * * When the test suite is run in the browser, this function gets overridden by * the same-named function in browser.js. */ function testPassesUnlessItThrows() { print(PASSED); } /* * wrapper for test case constructor that doesn't require the SECTION * argument. */ function AddTestCase( description, expect, actual ) { new TestCase( SECTION, description, expect, actual ); } function TestCase(n, d, e, a) { this.name = n; this.description = d; this.expect = e; this.actual = a; this.passed = getTestCaseResult(e, a); this.reason = ''; this.bugnumber = typeof(BUGNUMER) != 'undefined' ? BUGNUMBER : ''; this.type = (typeof window == 'undefined' ? 'shell' : 'browser'); ({}).constructor.defineProperty( gTestcases, gTc++, { value: this, enumerable: true, configurable: true, writable: true } ); } gFailureExpected = false; TestCase.prototype.dump = function () { // let reftest handle error reporting, otherwise // output a summary line. if (typeof document != "object" || !document.location.href.match(/jsreftest.html/)) { dump('\njstest: ' + this.path + ' ' + 'bug: ' + this.bugnumber + ' ' + 'result: ' + (this.passed ? 'PASSED':'FAILED') + ' ' + 'type: ' + this.type + ' ' + 'description: ' + toPrinted(this.description) + ' ' + // 'expected: ' + toPrinted(this.expect) + ' ' + // 'actual: ' + toPrinted(this.actual) + ' ' + 'reason: ' + toPrinted(this.reason) + '\n'); } }; 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 getTestCases() { return gTestcases; } /* * 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 + ' ---'); } /* * Statuses current section of a test */ function inSection(x) { return "Section " + x + " of test - "; } /* * Report a failure in the 'accepted' manner */ function reportFailure (msg) { var lines = msg.split ("\n"); var l; var funcName = currentFunc(); var prefix = (funcName) ? "[reported from " + funcName + "] ": ""; for (var i=0; i<lines.length; i++) print (FAILED + prefix + lines[i]); } /* * Print a non-failure message. */ function printStatus (msg) { /* js1_6 had... msg = String(msg); msg = msg.toString(); */ msg = msg.toString(); var lines = msg.split ("\n"); var l; for (var i=0; i<lines.length; i++) print (STATUS + lines[i]); } /* * Print a bugnumber message. */ function printBugNumber (num) { BUGNUMBER = num; print ('BUGNUMBER: ' + num); } function toPrinted(value) { value = String(value); value = value.replace(/\\n/g, 'NL') .replace(/\n/g, 'NL') .replace(/\\r/g, 'CR') .replace(/[^\x20-\x7E]+/g, escapeString); return value; } function escapeString (str) { var a, b, c, d; var len = str.length; var result = ""; var digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]; for (var i=0; i<len; i++) { var ch = str.charCodeAt(i); a = digits[ch & 0xf]; ch >>= 4; b = digits[ch & 0xf]; ch >>= 4; if (ch) { c = digits[ch & 0xf]; ch >>= 4; d = digits[ch & 0xf]; result += "\\u" + d + c + b + a; } else { result += "\\x" + b + a; } } return result; } /* * 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) + "' "; } var testcase = new TestCase("unknown-test-name", description, expected, actual); testcase.reason = output; // if running under reftest, let it handle result reporting. if (typeof document != "object" || !document.location.href.match(/jsreftest.html/)) { if (testcase.passed) { print(PASSED + description); } else { reportFailure (description + " : " + output); } } return testcase.passed; } /* * 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 = expectedRegExp.test(actual); if (!matches) { output += "Expected match to '" + toPrinted(expectedRegExp) + "', Actual value '" + toPrinted(actual) + "' "; } var testcase = new TestCase("unknown-test-name", description, true, matches); testcase.reason = output; // if running under reftest, let it handle result reporting. if (typeof document != "object" || !document.location.href.match(/jsreftest.html/)) { if (testcase.passed) { print(PASSED + description); } else { reportFailure (description + " : " + output); } } return testcase.passed; } /* * 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; } } function compareSource(expect, actual, summary) { // compare source var expectP = expect. replace(/([(){},.:\[\]])/mg, ' $1 '). replace(/(\w+)/mg, ' $1 '). replace(/<(\/)? (\w+) (\/)?>/mg, '<$1$2$3>'). replace(/\s+/mg, ' '). replace(/new (\w+)\s*\(\s*\)/mg, 'new $1'); var actualP = actual. replace(/([(){},.:\[\]])/mg, ' $1 '). replace(/(\w+)/mg, ' $1 '). replace(/<(\/)? (\w+) (\/)?>/mg, '<$1$2$3>'). replace(/\s+/mg, ' '). replace(/new (\w+)\s*\(\s*\)/mg, 'new $1'); 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; eval(expect); try { eval(actual); actualCompile = 'No Error'; } catch(ex1) { actualCompile = ex1 + ''; } reportCompare(expectCompile, actualCompile, summary + ': compile actual'); } catch(ex) { } } function optionsInit() { // record initial values to support resetting // options to their initial values options.initvalues = {}; // record values in a stack to support pushing // and popping options options.stackvalues = []; var optionNames = options().split(','); for (var i = 0; i < optionNames.length; i++) { var optionName = optionNames[i]; if (optionName) { options.initvalues[optionName] = ''; } } } function optionsClear() { // turn off current settings // except jit. var optionNames = options().split(','); for (var i = 0; i < optionNames.length; i++) { var optionName = optionNames[i]; if (optionName && optionName != "methodjit" && optionName != "methodjit_always" && optionName != "ion") { options(optionName); } } } function optionsPush() { var optionsframe = {}; options.stackvalues.push(optionsframe); var optionNames = options().split(','); for (var i = 0; i < optionNames.length; i++) { var optionName = optionNames[i]; if (optionName) { optionsframe[optionName] = ''; } } optionsClear(); } function optionsPop() { var optionsframe = options.stackvalues.pop(); optionsClear(); for (optionName in optionsframe) { options(optionName); } } function optionsReset() { try { optionsClear(); // turn on initial settings for (var optionName in options.initvalues) { if (!options.hasOwnProperty(optionName)) continue; options(optionName); } } catch(ex) { print('optionsReset: caught ' + ex); } } if (typeof options == 'function') { optionsInit(); optionsClear(); } 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 Math.abs(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 test() { for ( gTc=0; gTc < gTestcases.length; gTc++ ) { // temporary hack to work around some unknown issue in 1.7 try { gTestcases[gTc].passed = writeTestCaseResult( gTestcases[gTc].expect, gTestcases[gTc].actual, gTestcases[gTc].description +" = "+ gTestcases[gTc].actual ); gTestcases[gTc].reason += ( gTestcases[gTc].passed ) ? "" : "wrong value "; } catch(e) { print('test(): empty testcase for gTc = ' + gTc + ' ' + e); } } return ( gTestcases ); } /* * Begin printing functions. These functions use the shell's * print function. When running tests in the browser, browser.js * overrides these functions to write to the page. */ function writeTestCaseResult( expect, actual, string ) { var passed = getTestCaseResult( expect, actual ); // if running under reftest, let it handle result reporting. if (typeof document != "object" || !document.location.href.match(/jsreftest.html/)) { writeFormattedResult( expect, actual, string, passed ); } return passed; } function writeHeaderToLog( string ) { print( string ); } /* end of print functions */ function jsTestDriverEnd() { // gDelayTestDriverEnd is used to // delay collection of the test result and // signal to Spider so that tests can continue // to run after page load has fired. They are // responsible for setting gDelayTestDriverEnd = true // then when completed, setting gDelayTestDriverEnd = false // then calling jsTestDriverEnd() if (gDelayTestDriverEnd) { return; } try { optionsReset(); } catch(ex) { dump('jsTestDriverEnd ' + ex); } for (var i = 0; i < gTestcases.length; i++) { gTestcases[i].dump(); } }