/* vim:ts=2:sts=2:sw=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/. */ "use strict"; module.metadata = { "stability": "unstable" }; const { isFunction, isNull, isObject, isString, isRegExp, isArray, isDate, isPrimitive, isUndefined, instanceOf, source } = require("../lang/type"); /** * The `AssertionError` is defined in assert. * @extends Error * @example * new assert.AssertionError({ * message: message, * actual: actual, * expected: expected * }) */ function AssertionError(options) { let assertionError = Object.create(AssertionError.prototype); if (isString(options)) options = { message: options }; if ("actual" in options) assertionError.actual = options.actual; if ("expected" in options) assertionError.expected = options.expected; if ("operator" in options) assertionError.operator = options.operator; assertionError.message = options.message; assertionError.stack = new Error().stack; return assertionError; } AssertionError.prototype = Object.create(Error.prototype, { constructor: { value: AssertionError }, name: { value: "AssertionError", enumerable: true }, toString: { value: function toString() { let value; if (this.message) { value = this.name + " : " + this.message; } else { value = [ this.name + " : ", source(this.expected), this.operator, source(this.actual) ].join(" "); } return value; }} }); exports.AssertionError = AssertionError; function Assert(logger) { return Object.create(Assert.prototype, { _log: { value: logger }}); } Assert.prototype = { fail: function fail(e) { if (!e || typeof(e) !== 'object') { this._log.fail(e); return; } let message = e.message; if ('operator' in e) { message += [ " -", source(e.expected), e.operator, source(e.actual) ].join(" "); } this._log.fail(message); }, pass: function pass(message) { this._log.pass(message); }, error: function error(e) { this._log.exception(e); }, ok: function ok(value, message) { if (!!!value) { this.fail({ actual: value, expected: true, message: message, operator: "==" }); } else { this.pass(message); } }, /** * The equality assertion tests shallow, coercive equality with `==`. * @example * assert.equal(1, 1, "one is one"); */ equal: function equal(actual, expected, message) { if (actual == expected) { this.pass(message); } else { this.fail({ actual: actual, expected: expected, message: message, operator: "==" }); } }, /** * The non-equality assertion tests for whether two objects are not equal * with `!=`. * @example * assert.notEqual(1, 2, "one is not two"); */ notEqual: function notEqual(actual, expected, message) { if (actual != expected) { this.pass(message); } else { this.fail({ actual: actual, expected: expected, message: message, operator: "!=", }); } }, /** * The equivalence assertion tests a deep (with `===`) equality relation. * @example * assert.deepEqual({ a: "foo" }, { a: "foo" }, "equivalent objects") */ deepEqual: function deepEqual(actual, expected, message) { if (isDeepEqual(actual, expected)) { this.pass(message); } else { this.fail({ actual: actual, expected: expected, message: message, operator: "deepEqual" }); } }, /** * The non-equivalence assertion tests for any deep (with `===`) inequality. * @example * assert.notDeepEqual({ a: "foo" }, Object.create({ a: "foo" }), * "object's inherit from different prototypes"); */ notDeepEqual: function notDeepEqual(actual, expected, message) { if (!isDeepEqual(actual, expected)) { this.pass(message); } else { this.fail({ actual: actual, expected: expected, message: message, operator: "notDeepEqual" }); } }, /** * The strict equality assertion tests strict equality, as determined by * `===`. * @example * assert.strictEqual(null, null, "`null` is `null`") */ strictEqual: function strictEqual(actual, expected, message) { if (actual === expected) { this.pass(message); } else { this.fail({ actual: actual, expected: expected, message: message, operator: "===" }); } }, /** * The strict non-equality assertion tests for strict inequality, as * determined by `!==`. * @example * assert.notStrictEqual(null, undefined, "`null` is not `undefined`"); */ notStrictEqual: function notStrictEqual(actual, expected, message) { if (actual !== expected) { this.pass(message); } else { this.fail({ actual: actual, expected: expected, message: message, operator: "!==" }) } }, /** * The assertion whether or not given `block` throws an exception. If optional * `Error` argument is provided and it's type of function thrown error is * asserted to be an instance of it, if type of `Error` is string then message * of throw exception is asserted to contain it. * @param {Function} block * Function that is expected to throw. * @param {Error|RegExp} [Error] * Error constructor that is expected to be thrown or a string that * must be contained by a message of the thrown exception, or a RegExp * matching a message of the thrown exception. * @param {String} message * Description message * * @examples * * assert.throws(function block() { * doSomething(4) * }, "Object is expected", "Incorrect argument is passed"); * * assert.throws(function block() { * Object.create(5) * }, TypeError, "TypeError is thrown"); */ throws: function throws(block, Error, message) { let threw = false; let exception = null; // If third argument is not provided and second argument is a string it // means that optional `Error` argument was not passed, so we shift // arguments. if (isString(Error) && isUndefined(message)) { message = Error; Error = undefined; } // Executing given `block`. try { block(); } catch (e) { threw = true; exception = e; } // If exception was thrown and `Error` argument was not passed assert is // passed. if (threw && (isUndefined(Error) || // If passed `Error` is RegExp using it's test method to // assert thrown exception message. (isRegExp(Error) && Error.test(exception.message)) || // If passed `Error` is a constructor function testing if // thrown exception is an instance of it. (isFunction(Error) && instanceOf(exception, Error)))) { this.pass(message); } // Otherwise we report assertion failure. else { let failure = { message: message, operator: "throws" }; if (exception) failure.actual = exception; if (Error) failure.expected = Error; this.fail(failure); } } }; exports.Assert = Assert; function isDeepEqual(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 (isDate(actual) && isDate(expected)) { return actual.getTime() === expected.getTime(); } // XXX specification bug: this should be specified else if (isPrimitive(actual) || isPrimitive(expected)) { return expected === actual; } // 7.3. Other pairs that do not both pass typeof value == "object", // equivalence is determined by ==. else if (!isObject(actual) && !isObject(expected)) { return actual == expected; } // 7.4. 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 actual.prototype === expected.prototype && isEquivalent(actual, expected); } } function isEquivalent(a, b, stack) { let aKeys = Object.keys(a); let bKeys = Object.keys(b); return aKeys.length === bKeys.length && isArrayEquivalent(aKeys.sort(), bKeys.sort()) && aKeys.every(function(key) { return isDeepEqual(a[key], b[key], stack) }); } function isArrayEquivalent(a, b, stack) { return isArray(a) && isArray(b) && a.every(function(value, index) { return isDeepEqual(value, b[index]); }); }