summaryrefslogtreecommitdiffstats
path: root/testing/modules/Assert.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'testing/modules/Assert.sys.mjs')
-rw-r--r--testing/modules/Assert.sys.mjs731
1 files changed, 731 insertions, 0 deletions
diff --git a/testing/modules/Assert.sys.mjs b/testing/modules/Assert.sys.mjs
new file mode 100644
index 0000000000..47e2d3700d
--- /dev/null
+++ b/testing/modules/Assert.sys.mjs
@@ -0,0 +1,731 @@
+/* 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/. */
+
+// Originally from narwhal.js (http://narwhaljs.org)
+// Copyright (c) 2009 Thomas Robinson <280north.com>
+// MIT license: http://opensource.org/licenses/MIT
+
+import { ObjectUtils } from "resource://gre/modules/ObjectUtils.sys.mjs";
+
+/**
+ * This module is based on the
+ * `CommonJS spec <https://wiki.commonjs.org/wiki/Unit_Testing/1.0>`_
+ *
+ * When you see a jsdoc comment that contains a number, it's a reference to a
+ * specific section of the CommonJS spec.
+ *
+ * 1. The assert module provides functions that throw AssertionError's when
+ * particular conditions are not met.
+ *
+ * To use the module you may instantiate it first.
+ *
+ * @param {reporterFunc} reporterFunc
+ * Allows consumers to override reporting for this instance.
+ * @param {boolean} isDefault
+ * Used by test suites to set ``reporterFunc`` as the default
+ * used by the global instance, which is called for example
+ * by other test-only modules. This is false when the
+ * reporter is set by content scripts, because they may still
+ * run in the parent process.
+ *
+ * @class
+ */
+export function Assert(reporterFunc, isDefault) {
+ if (reporterFunc) {
+ this.setReporter(reporterFunc);
+ }
+ if (isDefault) {
+ Assert.setReporter(reporterFunc);
+ }
+}
+
+// This allows using the Assert object as an additional global instance.
+Object.setPrototypeOf(Assert, Assert.prototype);
+
+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();
+ }
+ if (
+ typeof value === "object" &&
+ value !== null &&
+ "QueryInterface" in value
+ ) {
+ 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);
+ }
+ return text;
+}
+
+function getMessage(error, prefix = "") {
+ let actual, expected;
+ // Wrap calls to JSON.stringify in try...catch blocks, as they may throw. If
+ // so, fall back to toString().
+ try {
+ actual = JSON.stringify(error.actual, replacer);
+ } catch (ex) {
+ actual = Object.prototype.toString.call(error.actual);
+ }
+ try {
+ expected = JSON.stringify(error.expected, replacer);
+ } catch (ex) {
+ expected = Object.prototype.toString.call(error.expected);
+ }
+ let message = prefix;
+ if (error.operator) {
+ let truncateLength = error.truncate ? kTruncateLength : Infinity;
+ message +=
+ (prefix ? " - " : "") +
+ truncate(actual, truncateLength) +
+ " " +
+ error.operator +
+ " " +
+ truncate(expected, truncateLength);
+ }
+ return message;
+}
+
+/**
+ * 2. The AssertionError is defined in assert.
+ *
+ * At present only the four keys mentioned below are used and
+ * understood by the spec. Implementations or sub modules can pass
+ * other keys to the AssertionError's constructor - they will be
+ * ignored.
+ *
+ * @example
+ *
+ * new assert.AssertionError({
+ * message: message,
+ * actual: actual,
+ * expected: expected,
+ * operator: operator,
+ * truncate: truncate,
+ * stack: stack, // Optional, defaults to the current stack.
+ * });
+ *
+ */
+Assert.AssertionError = function (options) {
+ this.name = "AssertionError";
+ this.actual = options.actual;
+ this.expected = options.expected;
+ this.operator = options.operator;
+ this.message = getMessage(this, options.message, options.truncate);
+ // The part of the stack that comes from this module is not interesting.
+ let stack = options.stack || Components.stack;
+ do {
+ stack = stack.asyncCaller || stack.caller;
+ } while (
+ stack &&
+ stack.filename &&
+ stack.filename.includes("Assert.sys.mjs")
+ );
+ this.stack = stack;
+};
+
+// assert.AssertionError instanceof Error
+Assert.AssertionError.prototype = Object.create(Error.prototype, {
+ constructor: {
+ value: Assert.AssertionError,
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ },
+});
+
+Assert.prototype._reporter = null;
+
+/**
+ * This callback type is used for custom assertion report handling.
+ *
+ * @callback reporterFunc
+ * @param {AssertionError|null} err
+ * An error object when the assertion failed, or null when it passed.
+ * @param {String} message
+ * Message describing the assertion.
+ * @param {Stack} stack
+ * Stack trace of the assertion function.
+ */
+
+/**
+ * Set a custom assertion report handler function.
+ *
+ * @example
+ *
+ * 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} reporterFunc
+ * Report handler function.
+ */
+Assert.prototype.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
+ *
+ * // The following will report an assertion failure.
+ * this.report(1 != 2, 1, 2, "testing JS number math!", "==");
+ *
+ * @param {boolean} failed
+ * Indicates if the assertion failed or not.
+ * @param {*} actual
+ * The result of evaluating the assertion.
+ * @param {*} [expected]
+ * Expected result from the test author.
+ * @param {String} [message]
+ * Short explanation of the expected result.
+ * @param {String} [operator]
+ * Operation qualifier used by the assertion method (ex: '==').
+ * @param {boolean} [truncate=true]
+ * Whether or not ``actual`` and ``expected`` should be truncated when printing.
+ * @param {nsIStackFrame} [stack]
+ * The stack trace including the caller of the assertion method,
+ * if this cannot be inferred automatically (e.g. due to async callbacks).
+ */
+Assert.prototype.report = function (
+ failed,
+ actual,
+ expected,
+ message,
+ operator,
+ truncate = true,
+ stack = null // Defaults to Components.stack in AssertionError.
+) {
+ // Although not ideal, we allow a "null" message due to the way some of the extension tests
+ // work.
+ if (message !== undefined && message !== null && typeof message != "string") {
+ this.ok(
+ false,
+ `Expected a string or undefined for the error message to Assert.*, got ${typeof message}`
+ );
+ }
+ let err = new Assert.AssertionError({
+ message,
+ actual,
+ expected,
+ operator,
+ truncate,
+ stack,
+ });
+ if (!this._reporter) {
+ // If no custom reporter is set, throw the error.
+ if (failed) {
+ throw err;
+ }
+ } else {
+ this._reporter(failed ? err : null, err.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
+ * Test subject to be evaluated as truthy.
+ * @param {String} [message]
+ * Short explanation of the expected result.
+ */
+Assert.prototype.ok = function (value, message) {
+ if (arguments.length > 2) {
+ this.report(
+ true,
+ false,
+ true,
+ "Too many arguments passed to `Assert.ok()`",
+ "=="
+ );
+ } else {
+ this.report(!value, value, true, message, "==");
+ }
+};
+
+/**
+ * 5. The equality assertion tests shallow, coercive equality with ==.
+ * ``assert.equal(actual, expected, message_opt);``
+ *
+ * @param {*} actual
+ * Test subject to be evaluated as equivalent to ``expected``.
+ * @param {*} expected
+ * Test reference to evaluate against ``actual``.
+ * @param {String} [message]
+ * Short explanation of the expected result.
+ */
+Assert.prototype.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 ``!=``
+ *
+ * @example
+ * assert.notEqual(actual, expected, message_opt);
+ *
+ * @param {*} actual
+ * Test subject to be evaluated as NOT equivalent to ``expected``.
+ * @param {*} expected
+ * Test reference to evaluate against ``actual``.
+ * @param {String} [message]
+ * Short explanation of the expected result.
+ */
+Assert.prototype.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
+ * Test subject to be evaluated as equivalent to ``expected``, including nested properties.
+ * @param {*} expected
+ * Test reference to evaluate against ``actual``.
+ * @param {String} [message]
+ * Short explanation of the expected result.
+ */
+Assert.prototype.deepEqual = function deepEqual(actual, expected, message) {
+ this.report(
+ !ObjectUtils.deepEqual(actual, expected),
+ actual,
+ expected,
+ message,
+ "deepEqual",
+ false
+ );
+};
+
+/**
+ * 8. The non-equivalence assertion tests for any deep inequality.
+ * assert.notDeepEqual(actual, expected, message_opt);
+ *
+ * @param {*} actual
+ * Test subject to be evaluated as NOT equivalent to ``expected``, including nested
+ * properties.
+ * @param {*} expected
+ * Test reference to evaluate against ``actual``.
+ * @param {String} [message]
+ * Short explanation of the expected result.
+ */
+Assert.prototype.notDeepEqual = function notDeepEqual(
+ actual,
+ expected,
+ message
+) {
+ this.report(
+ ObjectUtils.deepEqual(actual, expected),
+ actual,
+ expected,
+ message,
+ "notDeepEqual",
+ false
+ );
+};
+
+/**
+ * 9. The strict equality assertion tests strict equality, as determined by ===.
+ * ``assert.strictEqual(actual, expected, message_opt);``
+ *
+ * @param {*} actual
+ * Test subject to be evaluated as strictly equivalent to ``expected``.
+ * @param {*} expected
+ * Test reference to evaluate against ``actual``.
+ * @param {String} [message]
+ * Short explanation of the expected result.
+ */
+Assert.prototype.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
+ * Test subject to be evaluated as NOT strictly equivalent to ``expected``.
+ * @param {*} expected
+ * Test reference to evaluate against ``actual``.
+ * @param {String} [message]
+ * Short explanation of the expected result.
+ */
+Assert.prototype.notStrictEqual = function notStrictEqual(
+ actual,
+ expected,
+ message
+) {
+ this.report(actual === expected, actual, expected, message, "!==");
+};
+
+function checkExpectedArgument(instance, funcName, expected) {
+ if (!expected) {
+ instance.ok(
+ false,
+ `Error: The 'expected' argument was not supplied to Assert.${funcName}()`
+ );
+ }
+
+ if (
+ !instanceOf(expected, "RegExp") &&
+ typeof expected !== "function" &&
+ typeof expected !== "object"
+ ) {
+ instance.ok(
+ false,
+ `Error: The 'expected' argument to Assert.${funcName}() must be a RegExp, function or an object`
+ );
+ }
+}
+
+function expectedException(actual, expected) {
+ if (!actual || !expected) {
+ return false;
+ }
+
+ if (instanceOf(expected, "RegExp")) {
+ return expected.test(actual);
+ // We need to guard against the right hand parameter of "instanceof" lacking
+ // the "prototype" property, which is true of arrow functions in particular.
+ } else if (
+ !(typeof expected === "function" && !expected.prototype) &&
+ 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);
+ *
+ * Example:
+ * ```js
+ * // The following will verify that an error of type TypeError was thrown:
+ * Assert.throws(() => testBody(), TypeError);
+ * // The following will verify that an error was thrown with an error message matching "hello":
+ * Assert.throws(() => testBody(), /hello/);
+ * ```
+ *
+ * @param {Function} block
+ * Function to evaluate and catch eventual thrown errors.
+ * @param {RegExp|Function} expected
+ * This parameter can be either a RegExp or a function. The function is
+ * either the error type's constructor, or it's a method that returns
+ * a boolean that describes the test outcome.
+ * @param {String} [message]
+ * Short explanation of the expected result.
+ */
+Assert.prototype.throws = function (block, expected, message) {
+ checkExpectedArgument(this, "throws", expected);
+
+ // `true` if we realize that we have added an
+ // error to `ChromeUtils.recentJSDevError` and
+ // that we probably need to clean it up.
+ let cleanupRecentJSDevError = false;
+ if ("recentJSDevError" in ChromeUtils) {
+ // Check that we're in a build of Firefox that supports
+ // the `recentJSDevError` mechanism (i.e. Nightly build).
+ if (ChromeUtils.recentJSDevError === undefined) {
+ // There was no previous error, so if we throw
+ // an error here, we may need to clean it up.
+ cleanupRecentJSDevError = true;
+ }
+ }
+
+ let actual;
+
+ try {
+ block();
+ } catch (e) {
+ actual = e;
+ }
+
+ message =
+ (expected.name ? " (" + expected.name + ")." : ".") +
+ (message ? " " + message : ".");
+
+ if (!actual) {
+ this.report(true, actual, expected, "Missing expected exception" + message);
+ }
+
+ if (actual && !expectedException(actual, expected)) {
+ throw actual;
+ }
+
+ this.report(false, expected, expected, message);
+
+ // Make sure that we don't cause failures for JS Dev Errors that
+ // were expected, typically for tests that attempt to check
+ // that we react properly to TypeError, ReferenceError, SyntaxError.
+ if (cleanupRecentJSDevError) {
+ let recentJSDevError = ChromeUtils.recentJSDevError;
+ if (recentJSDevError) {
+ if (expectedException(recentJSDevError)) {
+ ChromeUtils.clearRecentJSDevError();
+ }
+ }
+ }
+};
+
+/**
+ * A promise that is expected to reject:
+ * assert.rejects(promise, expected, message);
+ *
+ * @param {Promise} promise
+ * A promise that is expected to reject.
+ * @param {?} [expected]
+ * Test reference to evaluate against the rejection result.
+ * @param {String} [message]
+ * Short explanation of the expected result.
+ */
+Assert.prototype.rejects = function (promise, expected, message) {
+ checkExpectedArgument(this, "rejects", expected);
+ const operator = undefined; // Should we use "rejects" here?
+ const stack = Components.stack;
+ return new Promise((resolve, reject) => {
+ return promise
+ .then(
+ () => {
+ this.report(
+ true,
+ null,
+ expected,
+ "Missing expected exception " + message,
+ operator,
+ true,
+ stack
+ );
+ // this.report() above should raise an AssertionError. If _reporter
+ // has been overridden and doesn't throw an error, just resolve.
+ // Otherwise we'll have a never-resolving promise that got stuck.
+ resolve();
+ },
+ err => {
+ if (!expectedException(err, expected)) {
+ // TODO bug 1480075: Should report error instead of rejecting.
+ reject(err);
+ return;
+ }
+ this.report(false, err, expected, message, operator, truncate, stack);
+ resolve();
+ }
+ )
+ .catch(reject);
+ });
+};
+
+function compareNumbers(expression, lhs, rhs, message, operator) {
+ let lhsIsNumber = typeof lhs == "number" && !Number.isNaN(lhs);
+ let rhsIsNumber = typeof rhs == "number" && !Number.isNaN(rhs);
+
+ if (lhsIsNumber && rhsIsNumber) {
+ this.report(expression, lhs, rhs, message, operator);
+ return;
+ }
+ let lhsIsDate =
+ typeof lhs == "object" && lhs.constructor.name == "Date" && !isNaN(lhs);
+ let rhsIsDate =
+ typeof rhs == "object" && rhs.constructor.name == "Date" && !isNaN(rhs);
+ if (lhsIsDate && rhsIsDate) {
+ this.report(expression, lhs, rhs, message, operator);
+ return;
+ }
+
+ let errorMessage;
+ if (!lhsIsNumber && !rhsIsNumber && !lhsIsDate && !rhsIsDate) {
+ errorMessage = `Neither '${lhs}' nor '${rhs}' are numbers or dates.`;
+ } else if ((lhsIsNumber && rhsIsDate) || (lhsIsDate && rhsIsNumber)) {
+ errorMessage = `'${lhsIsNumber ? lhs : rhs}' is a number and '${
+ rhsIsDate ? rhs : lhs
+ }' is a date.`;
+ } else {
+ errorMessage = `'${
+ lhsIsNumber || lhsIsDate ? rhs : lhs
+ }' is not a number or date.`;
+ }
+ this.report(true, lhs, rhs, errorMessage);
+}
+
+/**
+ * The lhs must be greater than the rhs.
+ * assert.greater(lhs, rhs, message_opt);
+ *
+ * @param {Number} lhs
+ * The left-hand side value.
+ * @param {Number} rhs
+ * The right-hand side value.
+ * @param {String} [message]
+ * Short explanation of the comparison result.
+ */
+Assert.prototype.greater = function greater(lhs, rhs, message) {
+ compareNumbers.call(this, lhs <= rhs, lhs, rhs, message, ">");
+};
+
+/**
+ * The lhs must be greater than or equal to the rhs.
+ * assert.greaterOrEqual(lhs, rhs, message_opt);
+ *
+ * @param {Number} [lhs]
+ * The left-hand side value.
+ * @param {Number} [rhs]
+ * The right-hand side value.
+ * @param {String} [message]
+ * Short explanation of the comparison result.
+ */
+Assert.prototype.greaterOrEqual = function greaterOrEqual(lhs, rhs, message) {
+ compareNumbers.call(this, lhs < rhs, lhs, rhs, message, ">=");
+};
+
+/**
+ * The lhs must be less than the rhs.
+ * assert.less(lhs, rhs, message_opt);
+ *
+ * @param {Number} [lhs]
+ * The left-hand side value.
+ * @param {Number} [rhs]
+ * The right-hand side value.
+ * @param {String} [message]
+ * Short explanation of the comparison result.
+ */
+Assert.prototype.less = function less(lhs, rhs, message) {
+ compareNumbers.call(this, lhs >= rhs, lhs, rhs, message, "<");
+};
+
+/**
+ * The lhs must be less than or equal to the rhs.
+ * assert.lessOrEqual(lhs, rhs, message_opt);
+ *
+ * @param {Number} [lhs]
+ * The left-hand side value.
+ * @param {Number} [rhs]
+ * The right-hand side value.
+ * @param {String} [message]
+ * Short explanation of the comparison result.
+ */
+Assert.prototype.lessOrEqual = function lessOrEqual(lhs, rhs, message) {
+ compareNumbers.call(this, lhs > rhs, lhs, rhs, message, "<=");
+};
+
+/**
+ * The lhs must be a string that matches the rhs regular expression.
+ * rhs can be specified either as a string or a RegExp object. If specified as a
+ * string it will be interpreted as a regular expression so take care to escape
+ * special characters such as "?" or "(" if you need the actual characters.
+ *
+ * @param {String} lhs
+ * The string to be tested.
+ * @param {String|RegExp} rhs
+ * The regular expression that the string will be tested with.
+ * Note that if passed as a string, this will be interpreted.
+ * as a regular expression.
+ * @param {String} [message]
+ * Short explanation of the comparison result.
+ */
+Assert.prototype.stringMatches = function stringMatches(lhs, rhs, message) {
+ if (typeof rhs != "string" && !instanceOf(rhs, "RegExp")) {
+ this.report(
+ true,
+ lhs,
+ String(rhs),
+ `Expected a string or a RegExp for rhs, but "${rhs}" isn't a string or a RegExp object.`
+ );
+ return;
+ }
+
+ if (typeof lhs != "string") {
+ this.report(
+ true,
+ lhs,
+ String(rhs),
+ `Expected a string for lhs, but "${lhs}" isn't a string.`
+ );
+ return;
+ }
+
+ if (typeof rhs == "string") {
+ try {
+ rhs = new RegExp(rhs);
+ } catch {
+ this.report(
+ true,
+ lhs,
+ rhs,
+ `Expected a valid regular expression for rhs, but "${rhs}" isn't one.`
+ );
+ return;
+ }
+ }
+
+ const isCorrect = rhs.test(lhs);
+ this.report(!isCorrect, lhs, rhs.toString(), message, "matches");
+};
+
+/**
+ * The lhs must be a string that contains the rhs string.
+ *
+ * @param {String} lhs
+ * The string to be tested (haystack).
+ * @param {String} rhs
+ * The string to be found (needle).
+ * @param {String} [message]
+ * Short explanation of the expected result.
+ */
+Assert.prototype.stringContains = function stringContains(lhs, rhs, message) {
+ if (typeof lhs != "string" || typeof rhs != "string") {
+ this.report(
+ true,
+ lhs,
+ rhs,
+ `Expected a string for both lhs and rhs, but either "${lhs}" or "${rhs}" is not a string.`
+ );
+ }
+
+ const isCorrect = lhs.includes(rhs);
+ this.report(!isCorrect, lhs, rhs, message, "includes");
+};