diff --git a/testing/modules/Assert.jsm b/testing/modules/Assert.jsm new file mode 100644 index 00000000000..45374b8084d --- /dev/null +++ b/testing/modules/Assert.jsm @@ -0,0 +1,426 @@ +/* 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/. */ + +// http://wiki.commonjs.org/wiki/Unit_Testing/1.0 +// When you see a javadoc comment that contains a number, it's a reference to a +// specific section of the CommonJS spec. +// +// Originally from narwhal.js (http://narwhaljs.org) +// Copyright (c) 2009 Thomas Robinson <280north.com> +// MIT license: http://opensource.org/licenses/MIT + +"use strict"; + +this.EXPORTED_SYMBOLS = [ + "Assert" +]; + +/** + * 1. The assert module provides functions that throw AssertionError's when + * particular conditions are not met. + * + * To use the module you'll need to instantiate it first, which allows consumers + * to override certain behavior on the newly obtained instance. For examples, + * see the javadoc comments for the `report` member function. + */ +let Assert = this.Assert = function(reporterFunc) { + if (reporterFunc) + this.setReporter(reporterFunc); +}; + +function instanceOf(object, type) { + return Object.prototype.toString.call(object) == "[object " + type + "]"; +} + +function replacer(key, value) { + if (value === undefined) { + return "" + value; + } + if (typeof value === "number" && (isNaN(value) || !isFinite(value))) { + return value.toString(); + } + if (typeof value === "function" || instanceOf(value, "RegExp")) { + return value.toString(); + } + return value; +} + +const kTruncateLength = 128; + +function truncate(text, newLength = kTruncateLength) { + if (typeof text == "string") { + return text.length < newLength ? text : text.slice(0, newLength); + } else { + return text; + } +} + +function getMessage(error) { + return truncate(JSON.stringify(error.actual, replacer)) + " " + + (error.operator ? error.operator + " " : "") + + truncate(JSON.stringify(error.expected, replacer)); +} + +/** + * 2. The AssertionError is defined in assert. + * + * Example: + * new assert.AssertionError({ + * message: message, + * actual: actual, + * expected: expected, + * operator: operator + * }); + * + * At present only the four keys mentioned above are used and + * understood by the spec. Implementations or sub modules can pass + * other keys to the AssertionError's constructor - they will be + * ignored. + */ +Assert.AssertionError = function(options) { + this.name = "AssertionError"; + this.actual = options.actual; + this.expected = options.expected; + this.operator = options.operator; + this.message = options.message || getMessage(this); + // The part of the stack that comes from this module is not interesting. + let stack = Components.stack; + do { + stack = stack.caller; + } while(stack.filename && stack.filename.contains("Assert.jsm")) + this.stack = stack; +}; + +// assert.AssertionError instanceof Error +Assert.AssertionError.prototype = Object.create(Error.prototype, { + constructor: { + value: Assert.AssertionError, + enumerable: false, + writable: true, + configurable: true + } +}); + +let proto = Assert.prototype; + +proto._reporter = null; +/** + * Set a custom assertion report handler function. Arguments passed in to this + * function are: + * err (AssertionError|null) An error object when the assertion failed or null + * when it passed + * message (string) Message describing the assertion + * stack (stack) Stack trace of the assertion function + * + * Example: + * ```js + * Assert.setReporter(function customReporter(err, message, stack) { + * if (err) { + * do_report_result(false, err.message, err.stack); + * } else { + * do_report_result(true, message, stack); + * } + * }); + * ``` + * + * @param reporterFunc + * (function) Report handler function + */ +proto.setReporter = function(reporterFunc) { + this._reporter = reporterFunc; +}; + +/** + * 3. All of the following functions must throw an AssertionError when a + * corresponding condition is not met, with a message that may be undefined if + * not provided. All assertion methods provide both the actual and expected + * values to the assertion error for display purposes. + * + * This report method only throws errors on assertion failures, as per spec, + * but consumers of this module (think: xpcshell-test, mochitest) may want to + * override this default implementation. + * + * Example: + * ```js + * // The following will report an assertion failure. + * this.report(1 != 2, 1, 2, "testing JS number math!", "=="); + * ``` + * + * @param failed + * (boolean) Indicates if the assertion failed or not + * @param actual + * (mixed) The result of evaluating the assertion + * @param expected (optional) + * (mixed) Expected result from the test author + * @param message (optional) + * (string) Short explanation of the expected result + * @param operator (optional) + * (string) Operation qualifier used by the assertion method (ex: '==') + */ +proto.report = function(failed, actual, expected, message, operator) { + let err = new Assert.AssertionError({ + message: message, + actual: actual, + expected: expected, + operator: operator + }); + if (!this._reporter) { + // If no custom reporter is set, throw the error. + if (failed) { + throw err; + } + } else { + this._reporter(failed ? err : null, message, err.stack); + } +}; + +/** + * 4. Pure assertion tests whether a value is truthy, as determined by !!guard. + * assert.ok(guard, message_opt); + * This statement is equivalent to assert.equal(true, !!guard, message_opt);. + * To test strictly for the value true, use assert.strictEqual(true, guard, + * message_opt);. + * + * @param value + * (mixed) Test subject to be evaluated as truthy + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.ok = function(value, message) { + this.report(!value, value, true, message, "=="); +}; + +/** + * 5. The equality assertion tests shallow, coercive equality with ==. + * assert.equal(actual, expected, message_opt); + * + * @param actual + * (mixed) Test subject to be evaluated as equivalent to `expected` + * @param expected + * (mixed) Test reference to evaluate against `actual` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.equal = function equal(actual, expected, message) { + this.report(actual != expected, actual, expected, message, "=="); +}; + +/** + * 6. The non-equality assertion tests for whether two objects are not equal + * with != assert.notEqual(actual, expected, message_opt); + * + * @param actual + * (mixed) Test subject to be evaluated as NOT equivalent to `expected` + * @param expected + * (mixed) Test reference to evaluate against `actual` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.notEqual = function notEqual(actual, expected, message) { + this.report(actual == expected, actual, expected, message, "!="); +}; + +/** + * 7. The equivalence assertion tests a deep equality relation. + * assert.deepEqual(actual, expected, message_opt); + * + * We check using the most exact approximation of equality between two objects + * to keep the chance of false positives to a minimum. + * `JSON.stringify` is not designed to be used for this purpose; objects may + * have ambiguous `toJSON()` implementations that would influence the test. + * + * @param actual + * (mixed) Test subject to be evaluated as equivalent to `expected`, including nested properties + * @param expected + * (mixed) Test reference to evaluate against `actual` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.deepEqual = function deepEqual(actual, expected, message) { + this.report(!_deepEqual(actual, expected), actual, expected, message, "deepEqual"); +}; + +function _deepEqual(actual, expected) { + // 7.1. All identical values are equivalent, as determined by ===. + if (actual === expected) { + return true; + // 7.2. If the expected value is a Date object, the actual value is + // equivalent if it is also a Date object that refers to the same time. + } else if (instanceOf(actual, "Date") && instanceOf(expected, "Date")) { + return actual.getTime() === expected.getTime(); + // 7.3 If the expected value is a RegExp object, the actual value is + // equivalent if it is also a RegExp object with the same source and + // properties (`global`, `multiline`, `lastIndex`, `ignoreCase`). + } else if (instanceOf(actual, "RegExp") && instanceOf(expected, "RegExp")) { + return actual.source === expected.source && + actual.global === expected.global && + actual.multiline === expected.multiline && + actual.lastIndex === expected.lastIndex && + actual.ignoreCase === expected.ignoreCase; + // 7.4. Other pairs that do not both pass typeof value == "object", + // equivalence is determined by ==. + } else if (typeof actual != "object" && typeof expected != "object") { + return actual == expected; + // 7.5 For all other Object pairs, including Array objects, equivalence is + // determined by having the same number of owned properties (as verified + // with Object.prototype.hasOwnProperty.call), the same set of keys + // (although not necessarily the same order), equivalent values for every + // corresponding key, and an identical 'prototype' property. Note: this + // accounts for both named and indexed properties on Arrays. + } else { + return objEquiv(actual, expected); + } +} + +function isUndefinedOrNull(value) { + return value === null || value === undefined; +} + +function isArguments(object) { + return instanceOf(object, "Arguments"); +} + +function objEquiv(a, b) { + if (isUndefinedOrNull(a) || isUndefinedOrNull(b)) { + return false; + } + // An identical 'prototype' property. + if (a.prototype !== b.prototype) { + return false; + } + // Object.keys may be broken through screwy arguments passing. Converting to + // an array solves the problem. + if (isArguments(a)) { + if (!isArguments(b)) { + return false; + } + a = pSlice.call(a); + b = pSlice.call(b); + return _deepEqual(a, b); + } + let ka, kb, key, i; + try { + ka = Object.keys(a); + kb = Object.keys(b); + } catch (e) { + // Happens when one is a string literal and the other isn't + return false; + } + // Having the same number of owned properties (keys incorporates + // hasOwnProperty) + if (ka.length != kb.length) + return false; + // The same set of keys (although not necessarily the same order), + ka.sort(); + kb.sort(); + // Equivalent values for every corresponding key, and possibly expensive deep + // test + for (i = ka.length - 1; i >= 0; i--) { + key = ka[i]; + if (!_deepEqual(a[key], b[key])) { + return false; + } + } + return true; +} + +/** + * 8. The non-equivalence assertion tests for any deep inequality. + * assert.notDeepEqual(actual, expected, message_opt); + * + * @param actual + * (mixed) Test subject to be evaluated as NOT equivalent to `expected`, including nested properties + * @param expected + * (mixed) Test reference to evaluate against `actual` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.notDeepEqual = function notDeepEqual(actual, expected, message) { + this.report(_deepEqual(actual, expected), actual, expected, message, "notDeepEqual"); +}; + +/** + * 9. The strict equality assertion tests strict equality, as determined by ===. + * assert.strictEqual(actual, expected, message_opt); + * + * @param actual + * (mixed) Test subject to be evaluated as strictly equivalent to `expected` + * @param expected + * (mixed) Test reference to evaluate against `actual` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.strictEqual = function strictEqual(actual, expected, message) { + this.report(actual !== expected, actual, expected, message, "==="); +}; + +/** + * 10. The strict non-equality assertion tests for strict inequality, as + * determined by !==. assert.notStrictEqual(actual, expected, message_opt); + * + * @param actual + * (mixed) Test subject to be evaluated as NOT strictly equivalent to `expected` + * @param expected + * (mixed) Test reference to evaluate against `actual` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.notStrictEqual = function notStrictEqual(actual, expected, message) { + this.report(actual === expected, actual, expected, message, "!=="); +}; + +function expectedException(actual, expected) { + if (!actual || !expected) { + return false; + } + + if (instanceOf(expected, "RegExp")) { + return expected.test(actual); + } else if (actual instanceof expected) { + return true; + } else if (expected.call({}, actual) === true) { + return true; + } + + return false; +} + +/** + * 11. Expected to throw an error: + * assert.throws(block, Error_opt, message_opt); + * + * @param block + * (function) Function block to evaluate and catch eventual thrown errors + * @param expected (optional) + * (mixed) Test reference to evaluate against the thrown result from `block` + * @param message (optional) + * (string) Short explanation of the expected result + */ +proto.throws = function(block, expected, message) { + let actual; + + if (typeof expected === "string") { + message = expected; + expected = null; + } + + try { + block(); + } catch (e) { + actual = e; + } + + message = (expected && expected.name ? " (" + expected.name + ")." : ".") + + (message ? " " + message : "."); + + if (!actual) { + this.report(true, actual, expected, "Missing expected exception" + message); + } + + if ((actual && expected && !expectedException(actual, expected))) { + throw actual; + } + + this.report(false, expected, expected, message); +}; diff --git a/testing/modules/Makefile.in b/testing/modules/Makefile.in index 2fdde35eec6..f0606d46225 100644 --- a/testing/modules/Makefile.in +++ b/testing/modules/Makefile.in @@ -4,4 +4,5 @@ TESTING_JS_MODULES := \ AppInfo.jsm \ + Assert.jsm \ $(NULL) diff --git a/testing/modules/moz.build b/testing/modules/moz.build index 895d11993cf..001efd322f0 100644 --- a/testing/modules/moz.build +++ b/testing/modules/moz.build @@ -4,3 +4,4 @@ # 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/. +XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini'] diff --git a/testing/modules/tests/xpcshell/test_assert.js b/testing/modules/tests/xpcshell/test_assert.js new file mode 100644 index 00000000000..378717f629e --- /dev/null +++ b/testing/modules/tests/xpcshell/test_assert.js @@ -0,0 +1,281 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test cases borrowed and adapted from: +// https://github.com/joyent/node/blob/6101eb184db77d0b11eb96e48744e57ecce4b73d/test/simple/test-assert.js +// MIT license: http://opensource.org/licenses/MIT + +function run_test() { + let ns = {}; + Components.utils.import("resource://testing-common/Assert.jsm", ns); + let assert = new ns.Assert(); + + function makeBlock(f, ...args) { + return function() { + return f.apply(assert, args); + }; + } + + function protoCtrChain(o) { + let result = []; + while (o = o.__proto__) { + result.push(o.constructor); + } + return result.join(); + } + + function indirectInstanceOf(obj, cls) { + if (obj instanceof cls) { + return true; + } + let clsChain = protoCtrChain(cls.prototype); + let objChain = protoCtrChain(obj); + return objChain.slice(-clsChain.length) === clsChain; + }; + + assert.ok(indirectInstanceOf(ns.Assert.AssertionError.prototype, Error), + "Assert.AssertionError instanceof Error"); + + assert.throws(makeBlock(assert.ok, false), + ns.Assert.AssertionError, "ok(false)"); + + assert.ok(true, "ok(true)"); + + assert.ok("test", "ok('test')"); + + assert.throws(makeBlock(assert.equal, true, false), ns.Assert.AssertionError, "equal"); + + assert.equal(null, null, "equal"); + + assert.equal(undefined, undefined, "equal"); + + assert.equal(null, undefined, "equal"); + + assert.equal(true, true, "equal"); + + assert.notEqual(true, false, "notEqual"); + + assert.throws(makeBlock(assert.notEqual, true, true), + ns.Assert.AssertionError, "notEqual"); + + assert.throws(makeBlock(assert.strictEqual, 2, "2"), + ns.Assert.AssertionError, "strictEqual"); + + assert.throws(makeBlock(assert.strictEqual, null, undefined), + ns.Assert.AssertionError, "strictEqual"); + + assert.notStrictEqual(2, "2", "notStrictEqual"); + + // deepEquals joy! + // 7.2 + assert.deepEqual(new Date(2000, 3, 14), new Date(2000, 3, 14), "deepEqual date"); + + assert.throws(makeBlock(assert.deepEqual, new Date(), new Date(2000, 3, 14)), + ns.Assert.AssertionError, + "deepEqual date"); + + // 7.3 + assert.deepEqual(/a/, /a/); + assert.deepEqual(/a/g, /a/g); + assert.deepEqual(/a/i, /a/i); + assert.deepEqual(/a/m, /a/m); + assert.deepEqual(/a/igm, /a/igm); + assert.throws(makeBlock(assert.deepEqual, /ab/, /a/)); + assert.throws(makeBlock(assert.deepEqual, /a/g, /a/)); + assert.throws(makeBlock(assert.deepEqual, /a/i, /a/)); + assert.throws(makeBlock(assert.deepEqual, /a/m, /a/)); + assert.throws(makeBlock(assert.deepEqual, /a/igm, /a/im)); + + let re1 = /a/; + re1.lastIndex = 3; + assert.throws(makeBlock(assert.deepEqual, re1, /a/)); + + // 7.4 + assert.deepEqual(4, "4", "deepEqual == check"); + assert.deepEqual(true, 1, "deepEqual == check"); + assert.throws(makeBlock(assert.deepEqual, 4, "5"), + ns.Assert.AssertionError, + "deepEqual == check"); + + // 7.5 + // having the same number of owned properties && the same set of keys + assert.deepEqual({a: 4}, {a: 4}); + assert.deepEqual({a: 4, b: "2"}, {a: 4, b: "2"}); + assert.deepEqual([4], ["4"]); + assert.throws(makeBlock(assert.deepEqual, {a: 4}, {a: 4, b: true}), + ns.Assert.AssertionError); + assert.deepEqual(["a"], {0: "a"}); + + let a1 = [1, 2, 3]; + let a2 = [1, 2, 3]; + a1.a = "test"; + a1.b = true; + a2.b = true; + a2.a = "test"; + assert.throws(makeBlock(assert.deepEqual, Object.keys(a1), Object.keys(a2)), + ns.Assert.AssertionError); + assert.deepEqual(a1, a2); + + let nbRoot = { + toString: function() { return this.first + " " + this.last; } + }; + + function nameBuilder(first, last) { + this.first = first; + this.last = last; + return this; + } + nameBuilder.prototype = nbRoot; + + function nameBuilder2(first, last) { + this.first = first; + this.last = last; + return this; + } + nameBuilder2.prototype = nbRoot; + + let nb1 = new nameBuilder("Ryan", "Dahl"); + let nb2 = new nameBuilder2("Ryan", "Dahl"); + + assert.deepEqual(nb1, nb2); + + nameBuilder2.prototype = Object; + nb2 = new nameBuilder2("Ryan", "Dahl"); + assert.throws(makeBlock(assert.deepEqual, nb1, nb2), ns.Assert.AssertionError); + + // String literal + object + assert.throws(makeBlock(assert.deepEqual, "a", {}), ns.Assert.AssertionError); + + // Testing the throwing + function thrower(errorConstructor) { + throw new errorConstructor("test"); + } + let aethrow = makeBlock(thrower, ns.Assert.AssertionError); + aethrow = makeBlock(thrower, ns.Assert.AssertionError); + + // the basic calls work + assert.throws(makeBlock(thrower, ns.Assert.AssertionError), + ns.Assert.AssertionError, "message"); + assert.throws(makeBlock(thrower, ns.Assert.AssertionError), ns.Assert.AssertionError); + assert.throws(makeBlock(thrower, ns.Assert.AssertionError)); + + // if not passing an error, catch all. + assert.throws(makeBlock(thrower, TypeError)); + + // when passing a type, only catch errors of the appropriate type + let threw = false; + try { + assert.throws(makeBlock(thrower, TypeError), ns.Assert.AssertionError); + } catch (e) { + threw = true; + assert.ok(e instanceof TypeError, "type"); + } + assert.equal(true, threw, + "Assert.throws with an explicit error is eating extra errors", + ns.Assert.AssertionError); + threw = false; + + function ifError(err) { + if (err) { + throw err; + } + } + assert.throws(function() { + ifError(new Error("test error")); + }); + + // make sure that validating using constructor really works + threw = false; + try { + assert.throws( + function() { + throw ({}); + }, + Array + ); + } catch (e) { + threw = true; + } + assert.ok(threw, "wrong constructor validation"); + + // use a RegExp to validate error message + assert.throws(makeBlock(thrower, TypeError), /test/); + + // use a fn to validate error object + assert.throws(makeBlock(thrower, TypeError), function(err) { + if ((err instanceof TypeError) && /test/.test(err)) { + return true; + } + }); + + // Make sure deepEqual doesn't loop forever on circular refs + + let b = {}; + b.b = b; + + let c = {}; + c.b = c; + + let gotError = false; + try { + assert.deepEqual(b, c); + } catch (e) { + gotError = true; + } + + dump("All OK\n"); + assert.ok(gotError); + + function testAssertionMessage(actual, expected) { + try { + assert.equal(actual, ""); + } catch (e) { + assert.equal(e.toString(), + ["AssertionError:", expected, "==", '""'].join(" ")); + } + } + testAssertionMessage(undefined, '"undefined"'); + testAssertionMessage(null, "null"); + testAssertionMessage(true, "true"); + testAssertionMessage(false, "false"); + testAssertionMessage(0, "0"); + testAssertionMessage(100, "100"); + testAssertionMessage(NaN, '"NaN"'); + testAssertionMessage(Infinity, '"Infinity"'); + testAssertionMessage(-Infinity, '"-Infinity"'); + testAssertionMessage("", '""'); + testAssertionMessage("foo", '"foo"'); + testAssertionMessage([], "[]"); + testAssertionMessage([1, 2, 3], "[1,2,3]"); + testAssertionMessage(/a/, '"/a/"'); + testAssertionMessage(/abc/gim, '"/abc/gim"'); + testAssertionMessage(function f() {}, '"function f() {}"'); + testAssertionMessage({}, "{}"); + testAssertionMessage({a: undefined, b: null}, '{"a":"undefined","b":null}'); + testAssertionMessage({a: NaN, b: Infinity, c: -Infinity}, + '{"a":"NaN","b":"Infinity","c":"-Infinity"}'); + + // https://github.com/joyent/node/issues/2893 + try { + assert.throws(function () { + ifError(null); + }); + } catch (e) { + threw = true; + assert.equal(e.message, "Missing expected exception.."); + } + assert.ok(threw); + + // https://github.com/joyent/node/issues/5292 + try { + assert.equal(1, 2); + } catch (e) { + assert.equal(e.toString().split("\n")[0], "AssertionError: 1 == 2") + } + + try { + assert.equal(1, 2, "oh no"); + } catch (e) { + assert.equal(e.toString().split("\n")[0], "AssertionError: oh no") + } +} diff --git a/testing/modules/tests/xpcshell/xpcshell.ini b/testing/modules/tests/xpcshell/xpcshell.ini new file mode 100644 index 00000000000..eaa59599cb2 --- /dev/null +++ b/testing/modules/tests/xpcshell/xpcshell.ini @@ -0,0 +1,5 @@ +[DEFAULT] +head = +tail = + +[test_assert.js] diff --git a/testing/xpcshell/head.js b/testing/xpcshell/head.js index 083f01a17c4..de022c065a2 100644 --- a/testing/xpcshell/head.js +++ b/testing/xpcshell/head.js @@ -184,6 +184,15 @@ function _do_quit() { } function _format_exception_stack(stack) { + if (typeof stack == "object" && stack.caller) { + let frame = stack; + let strStack = ""; + while (frame != null) { + strStack += frame + "\n"; + frame = frame.caller; + } + stack = strStack; + } // frame is of the form "fname@file:line" let frame_regexp = new RegExp("(.*)@(.*):(\\d*)", "g"); return stack.split("\n").reduce(function(stack_msg, frame) {