summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--std/node/_errors.ts48
-rw-r--r--std/node/assert.ts2
-rw-r--r--std/node/assert_test.ts10
-rw-r--r--std/node/assertion_error.ts577
-rw-r--r--std/node/assertion_error_test.ts175
-rw-r--r--std/node/util.ts29
6 files changed, 835 insertions, 6 deletions
diff --git a/std/node/_errors.ts b/std/node/_errors.ts
new file mode 100644
index 000000000..2321bb24f
--- /dev/null
+++ b/std/node/_errors.ts
@@ -0,0 +1,48 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors.
+
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// It will do so until we'll have Node errors completely ported (#5944):
+// Ref: https://github.com/nodejs/node/blob/50d28d4b3a616b04537feff014aa70437f064e30/lib/internal/errors.js#L251
+// Ref: https://github.com/nodejs/node/blob/50d28d4b3a616b04537feff014aa70437f064e30/lib/internal/errors.js#L299
+// Ref: https://github.com/nodejs/node/blob/50d28d4b3a616b04537feff014aa70437f064e30/lib/internal/errors.js#L325
+// Ref: https://github.com/nodejs/node/blob/50d28d4b3a616b04537feff014aa70437f064e30/lib/internal/errors.js#L943
+class ERR_INVALID_ARG_TYPE extends TypeError {
+ code = "ERR_INVALID_ARG_TYPE";
+
+ constructor(a1: string, a2: string, a3: unknown) {
+ super(
+ `The "${a1}" argument must be of type ${a2.toLocaleLowerCase()}. Received ${typeof a3} (${a3})`,
+ );
+ const { name } = this;
+ // Add the error code to the name to include it in the stack trace.
+ this.name = `${name} [${this.code}]`;
+ // Access the stack to generate the error message including the error code from the name.
+ this.stack;
+ // Reset the name to the actual name.
+ this.name = name;
+ }
+}
+
+export const codes = {
+ ERR_INVALID_ARG_TYPE,
+};
diff --git a/std/node/assert.ts b/std/node/assert.ts
index 3d738cbbf..2a4f8a0b5 100644
--- a/std/node/assert.ts
+++ b/std/node/assert.ts
@@ -7,6 +7,8 @@ import {
assertThrows,
} from "../testing/asserts.ts";
+export { AssertionError } from "./assertion_error.ts";
+
export {
assert as default,
assert as ok,
diff --git a/std/node/assert_test.ts b/std/node/assert_test.ts
index 8df13187b..9d38ca56b 100644
--- a/std/node/assert_test.ts
+++ b/std/node/assert_test.ts
@@ -9,9 +9,9 @@ import {
fail as denoFail,
} from "../testing/asserts.ts";
-import assert from "./assert.ts";
+import AssertionError from "./assertion_error.ts";
-import {
+import assert, {
ok,
assert as assert_,
deepStrictEqual,
@@ -21,6 +21,7 @@ import {
match,
throws,
fail,
+ AssertionError as AssertionError_,
} from "./assert.ts";
Deno.test("API should be exposed", () => {
@@ -62,4 +63,9 @@ Deno.test("API should be exposed", () => {
"`assertThrows()` should be exposed as `throws()`",
);
assertStrictEquals(fail, denoFail, "`fail()` should be exposed");
+ assertStrictEquals(
+ AssertionError,
+ AssertionError_,
+ "`AssertionError()` constructor should be exposed",
+ );
});
diff --git a/std/node/assertion_error.ts b/std/node/assertion_error.ts
new file mode 100644
index 000000000..46122b658
--- /dev/null
+++ b/std/node/assertion_error.ts
@@ -0,0 +1,577 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors.
+
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+// TODO(schwarzkopfb): change this when `Deno.consoleSize()` will be stable
+interface DenoUnstable {
+ consoleSize?(rid: number): { columns: number };
+}
+function getConsoleWidth(): number {
+ return (Deno as DenoUnstable).consoleSize?.(Deno.stderr.rid).columns ?? 80;
+}
+
+import { inspect } from "./util.ts";
+import { stripColor as removeColors } from "../fmt/colors.ts";
+
+// TODO(schwarzkopfb): we should implement Node's concept of "primordials"
+// Ref: https://github.com/denoland/deno/issues/6040#issuecomment-637305828
+const MathMax = Math.max;
+const { Error } = globalThis;
+const {
+ create: ObjectCreate,
+ defineProperty: ObjectDefineProperty,
+ getPrototypeOf: ObjectGetPrototypeOf,
+ getOwnPropertyDescriptor: ObjectGetOwnPropertyDescriptor,
+ keys: ObjectKeys,
+} = Object;
+
+import { codes } from "./_errors.ts";
+const { ERR_INVALID_ARG_TYPE } = codes;
+
+let blue = "";
+let green = "";
+let red = "";
+let defaultColor = "";
+
+const kReadableOperator: { [key: string]: string } = {
+ deepStrictEqual: "Expected values to be strictly deep-equal:",
+ strictEqual: "Expected values to be strictly equal:",
+ strictEqualObject: 'Expected "actual" to be reference-equal to "expected":',
+ deepEqual: "Expected values to be loosely deep-equal:",
+ notDeepStrictEqual: 'Expected "actual" not to be strictly deep-equal to:',
+ notStrictEqual: 'Expected "actual" to be strictly unequal to:',
+ notStrictEqualObject:
+ 'Expected "actual" not to be reference-equal to "expected":',
+ notDeepEqual: 'Expected "actual" not to be loosely deep-equal to:',
+ notIdentical: "Values have same structure but are not reference-equal:",
+ notDeepEqualUnequal: "Expected values not to be loosely deep-equal:",
+};
+
+// Comparing short primitives should just show === / !== instead of using the
+// diff.
+const kMaxShortLength = 12;
+
+export function copyError(source: Error): Error {
+ const keys = ObjectKeys(source);
+ const target = ObjectCreate(ObjectGetPrototypeOf(source));
+ for (const key of keys) {
+ const desc = ObjectGetOwnPropertyDescriptor(source, key);
+
+ if (desc !== undefined) {
+ ObjectDefineProperty(target, key, desc);
+ }
+ }
+ ObjectDefineProperty(target, "message", { value: source.message });
+ return target;
+}
+
+export function inspectValue(val: unknown): string {
+ // The util.inspect default values could be changed. This makes sure the
+ // error messages contain the necessary information nevertheless.
+ return inspect(
+ val,
+ {
+ compact: false,
+ customInspect: false,
+ depth: 1000,
+ maxArrayLength: Infinity,
+ // Assert compares only enumerable properties (with a few exceptions).
+ showHidden: false,
+ // Assert does not detect proxies currently.
+ showProxy: false,
+ sorted: true,
+ // Inspect getters as we also check them when comparing entries.
+ getters: true,
+ },
+ );
+}
+
+export function createErrDiff(
+ actual: unknown,
+ expected: unknown,
+ operator: string,
+): string {
+ let other = "";
+ let res = "";
+ let end = "";
+ let skipped = false;
+ const actualInspected = inspectValue(actual);
+ const actualLines = actualInspected.split("\n");
+ const expectedLines = inspectValue(expected).split("\n");
+
+ let i = 0;
+ let indicator = "";
+
+ // In case both values are objects or functions explicitly mark them as not
+ // reference equal for the `strictEqual` operator.
+ if (
+ operator === "strictEqual" &&
+ ((typeof actual === "object" && actual !== null &&
+ typeof expected === "object" && expected !== null) ||
+ (typeof actual === "function" && typeof expected === "function"))
+ ) {
+ operator = "strictEqualObject";
+ }
+
+ // If "actual" and "expected" fit on a single line and they are not strictly
+ // equal, check further special handling.
+ if (
+ actualLines.length === 1 && expectedLines.length === 1 &&
+ actualLines[0] !== expectedLines[0]
+ ) {
+ // Check for the visible length using the `removeColors()` function, if
+ // appropriate.
+ const c = inspect.defaultOptions.colors;
+ const actualRaw = c ? removeColors(actualLines[0]) : actualLines[0];
+ const expectedRaw = c ? removeColors(expectedLines[0]) : expectedLines[0];
+ const inputLength = actualRaw.length + expectedRaw.length;
+ // If the character length of "actual" and "expected" together is less than
+ // kMaxShortLength and if neither is an object and at least one of them is
+ // not `zero`, use the strict equal comparison to visualize the output.
+ if (inputLength <= kMaxShortLength) {
+ if (
+ (typeof actual !== "object" || actual === null) &&
+ (typeof expected !== "object" || expected === null) &&
+ (actual !== 0 || expected !== 0)
+ ) { // -0 === +0
+ return `${kReadableOperator[operator]}\n\n` +
+ `${actualLines[0]} !== ${expectedLines[0]}\n`;
+ }
+ } else if (operator !== "strictEqualObject") {
+ // If the stderr is a tty and the input length is lower than the current
+ // columns per line, add a mismatch indicator below the output. If it is
+ // not a tty, use a default value of 80 characters.
+ const maxLength = Deno.isatty(Deno.stderr.rid) ? getConsoleWidth() : 80;
+ if (inputLength < maxLength) {
+ while (actualRaw[i] === expectedRaw[i]) {
+ i++;
+ }
+ // Ignore the first characters.
+ if (i > 2) {
+ // Add position indicator for the first mismatch in case it is a
+ // single line and the input length is less than the column length.
+ indicator = `\n ${" ".repeat(i)}^`;
+ i = 0;
+ }
+ }
+ }
+ }
+
+ // Remove all ending lines that match (this optimizes the output for
+ // readability by reducing the number of total changed lines).
+ let a = actualLines[actualLines.length - 1];
+ let b = expectedLines[expectedLines.length - 1];
+ while (a === b) {
+ if (i++ < 3) {
+ end = `\n ${a}${end}`;
+ } else {
+ other = a;
+ }
+ actualLines.pop();
+ expectedLines.pop();
+ if (actualLines.length === 0 || expectedLines.length === 0) {
+ break;
+ }
+ a = actualLines[actualLines.length - 1];
+ b = expectedLines[expectedLines.length - 1];
+ }
+
+ const maxLines = MathMax(actualLines.length, expectedLines.length);
+ // Strict equal with identical objects that are not identical by reference.
+ // E.g., assert.deepStrictEqual({ a: Symbol() }, { a: Symbol() })
+ if (maxLines === 0) {
+ // We have to get the result again. The lines were all removed before.
+ const actualLines = actualInspected.split("\n");
+
+ // Only remove lines in case it makes sense to collapse those.
+ if (actualLines.length > 50) {
+ actualLines[46] = `${blue}...${defaultColor}`;
+ while (actualLines.length > 47) {
+ actualLines.pop();
+ }
+ }
+
+ return `${kReadableOperator.notIdentical}\n\n${actualLines.join("\n")}\n`;
+ }
+
+ // There were at least five identical lines at the end. Mark a couple of
+ // skipped.
+ if (i >= 5) {
+ end = `\n${blue}...${defaultColor}${end}`;
+ skipped = true;
+ }
+ if (other !== "") {
+ end = `\n ${other}${end}`;
+ other = "";
+ }
+
+ let printedLines = 0;
+ let identical = 0;
+ const msg = kReadableOperator[operator] +
+ `\n${green}+ actual${defaultColor} ${red}- expected${defaultColor}`;
+ const skippedMsg = ` ${blue}...${defaultColor} Lines skipped`;
+
+ let lines = actualLines;
+ let plusMinus = `${green}+${defaultColor}`;
+ let maxLength = expectedLines.length;
+ if (actualLines.length < maxLines) {
+ lines = expectedLines;
+ plusMinus = `${red}-${defaultColor}`;
+ maxLength = actualLines.length;
+ }
+
+ for (i = 0; i < maxLines; i++) {
+ if (maxLength < i + 1) {
+ // If more than two former lines are identical, print them. Collapse them
+ // in case more than five lines were identical.
+ if (identical > 2) {
+ if (identical > 3) {
+ if (identical > 4) {
+ if (identical === 5) {
+ res += `\n ${lines[i - 3]}`;
+ printedLines++;
+ } else {
+ res += `\n${blue}...${defaultColor}`;
+ skipped = true;
+ }
+ }
+ res += `\n ${lines[i - 2]}`;
+ printedLines++;
+ }
+ res += `\n ${lines[i - 1]}`;
+ printedLines++;
+ }
+ // No identical lines before.
+ identical = 0;
+ // Add the expected line to the cache.
+ if (lines === actualLines) {
+ res += `\n${plusMinus} ${lines[i]}`;
+ } else {
+ other += `\n${plusMinus} ${lines[i]}`;
+ }
+ printedLines++;
+ // Only extra actual lines exist
+ // Lines diverge
+ } else {
+ const expectedLine = expectedLines[i];
+ let actualLine = actualLines[i];
+ // If the lines diverge, specifically check for lines that only diverge by
+ // a trailing comma. In that case it is actually identical and we should
+ // mark it as such.
+ let divergingLines = actualLine !== expectedLine &&
+ (!actualLine.endsWith(",") ||
+ actualLine.slice(0, -1) !== expectedLine);
+ // If the expected line has a trailing comma but is otherwise identical,
+ // add a comma at the end of the actual line. Otherwise the output could
+ // look weird as in:
+ //
+ // [
+ // 1 // No comma at the end!
+ // + 2
+ // ]
+ //
+ if (
+ divergingLines &&
+ expectedLine.endsWith(",") &&
+ expectedLine.slice(0, -1) === actualLine
+ ) {
+ divergingLines = false;
+ actualLine += ",";
+ }
+ if (divergingLines) {
+ // If more than two former lines are identical, print them. Collapse
+ // them in case more than five lines were identical.
+ if (identical > 2) {
+ if (identical > 3) {
+ if (identical > 4) {
+ if (identical === 5) {
+ res += `\n ${actualLines[i - 3]}`;
+ printedLines++;
+ } else {
+ res += `\n${blue}...${defaultColor}`;
+ skipped = true;
+ }
+ }
+ res += `\n ${actualLines[i - 2]}`;
+ printedLines++;
+ }
+ res += `\n ${actualLines[i - 1]}`;
+ printedLines++;
+ }
+ // No identical lines before.
+ identical = 0;
+ // Add the actual line to the result and cache the expected diverging
+ // line so consecutive diverging lines show up as +++--- and not +-+-+-.
+ res += `\n${green}+${defaultColor} ${actualLine}`;
+ other += `\n${red}-${defaultColor} ${expectedLine}`;
+ printedLines += 2;
+ // Lines are identical
+ } else {
+ // Add all cached information to the result before adding other things
+ // and reset the cache.
+ res += other;
+ other = "";
+ identical++;
+ // The very first identical line since the last diverging line is be
+ // added to the result.
+ if (identical <= 2) {
+ res += `\n ${actualLine}`;
+ printedLines++;
+ }
+ }
+ }
+ // Inspected object to big (Show ~50 rows max)
+ if (printedLines > 50 && i < maxLines - 2) {
+ return `${msg}${skippedMsg}\n${res}\n${blue}...${defaultColor}${other}\n` +
+ `${blue}...${defaultColor}`;
+ }
+ }
+
+ return `${msg}${skipped ? skippedMsg : ""}\n${res}${other}${end}${indicator}`;
+}
+
+export interface AssertionErrorDetailsDescriptor {
+ message: string;
+ actual: unknown;
+ expected: unknown;
+ operator: string;
+ stack: Error;
+}
+
+export interface AssertionErrorConstructorOptions {
+ message?: string;
+ actual?: unknown;
+ expected?: unknown;
+ operator?: string;
+ details?: AssertionErrorDetailsDescriptor[];
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ stackStartFn?: Function;
+ // Compatibility with older versions.
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ stackStartFunction?: Function;
+}
+
+interface ErrorWithStackTraceLimit extends ErrorConstructor {
+ stackTraceLimit: number;
+}
+
+export class AssertionError extends Error {
+ [key: string]: unknown
+
+ // deno-lint-ignore constructor-super
+ constructor(options: AssertionErrorConstructorOptions) {
+ if (typeof options !== "object" || options === null) {
+ throw new ERR_INVALID_ARG_TYPE("options", "Object", options);
+ }
+ const {
+ message,
+ operator,
+ stackStartFn,
+ details,
+ // Compatibility with older versions.
+ stackStartFunction,
+ } = options;
+ let {
+ actual,
+ expected,
+ } = options;
+
+ // TODO(schwarzkopfb): `stackTraceLimit` should be added to `ErrorConstructor` in
+ // cli/dts/lib.deno.shared_globals.d.ts
+ const limit = (Error as ErrorWithStackTraceLimit).stackTraceLimit;
+ (Error as ErrorWithStackTraceLimit).stackTraceLimit = 0;
+
+ if (message != null) {
+ super(String(message));
+ } else {
+ if (Deno.isatty(Deno.stderr.rid)) {
+ // Reset on each call to make sure we handle dynamically set environment
+ // variables correct.
+ if (Deno.noColor) {
+ blue = "";
+ green = "";
+ defaultColor = "";
+ red = "";
+ } else {
+ blue = "\u001b[34m";
+ green = "\u001b[32m";
+ defaultColor = "\u001b[39m";
+ red = "\u001b[31m";
+ }
+ }
+ // Prevent the error stack from being visible by duplicating the error
+ // in a very close way to the original in case both sides are actually
+ // instances of Error.
+ if (
+ typeof actual === "object" && actual !== null &&
+ typeof expected === "object" && expected !== null &&
+ "stack" in actual && actual instanceof Error &&
+ "stack" in expected && expected instanceof Error
+ ) {
+ actual = copyError(actual);
+ expected = copyError(expected);
+ }
+
+ if (operator === "deepStrictEqual" || operator === "strictEqual") {
+ super(createErrDiff(actual, expected, operator));
+ } else if (
+ operator === "notDeepStrictEqual" ||
+ operator === "notStrictEqual"
+ ) {
+ // In case the objects are equal but the operator requires unequal, show
+ // the first object and say A equals B
+ let base = kReadableOperator[operator];
+ const res = inspectValue(actual).split("\n");
+
+ // In case "actual" is an object or a function, it should not be
+ // reference equal.
+ if (
+ operator === "notStrictEqual" &&
+ ((typeof actual === "object" && actual !== null) ||
+ typeof actual === "function")
+ ) {
+ base = kReadableOperator.notStrictEqualObject;
+ }
+
+ // Only remove lines in case it makes sense to collapse those.
+ if (res.length > 50) {
+ res[46] = `${blue}...${defaultColor}`;
+ while (res.length > 47) {
+ res.pop();
+ }
+ }
+
+ // Only print a single input.
+ if (res.length === 1) {
+ super(`${base}${res[0].length > 5 ? "\n\n" : " "}${res[0]}`);
+ } else {
+ super(`${base}\n\n${res.join("\n")}\n`);
+ }
+ } else {
+ let res = inspectValue(actual);
+ let other = inspectValue(expected);
+ const knownOperator = kReadableOperator[operator ?? ""];
+ if (operator === "notDeepEqual" && res === other) {
+ res = `${knownOperator}\n\n${res}`;
+ if (res.length > 1024) {
+ res = `${res.slice(0, 1021)}...`;
+ }
+ super(res);
+ } else {
+ if (res.length > 512) {
+ res = `${res.slice(0, 509)}...`;
+ }
+ if (other.length > 512) {
+ other = `${other.slice(0, 509)}...`;
+ }
+ if (operator === "deepEqual") {
+ res = `${knownOperator}\n\n${res}\n\nshould loosely deep-equal\n\n`;
+ } else {
+ const newOp = kReadableOperator[`${operator}Unequal`];
+ if (newOp) {
+ res = `${newOp}\n\n${res}\n\nshould not loosely deep-equal\n\n`;
+ } else {
+ other = ` ${operator} ${other}`;
+ }
+ }
+ super(`${res}${other}`);
+ }
+ }
+ }
+
+ (Error as ErrorWithStackTraceLimit).stackTraceLimit = limit;
+
+ this.generatedMessage = !message;
+ ObjectDefineProperty(this, "name", {
+ value: "AssertionError [ERR_ASSERTION]",
+ enumerable: false,
+ writable: true,
+ configurable: true,
+ });
+ this.code = "ERR_ASSERTION";
+
+ if (details) {
+ this.actual = undefined;
+ this.expected = undefined;
+ this.operator = undefined;
+
+ for (let i = 0; i < details.length; i++) {
+ this["message " + i] = details[i].message;
+ this["actual " + i] = details[i].actual;
+ this["expected " + i] = details[i].expected;
+ this["operator " + i] = details[i].operator;
+ this["stack trace " + i] = details[i].stack;
+ }
+ } else {
+ this.actual = actual;
+ this.expected = expected;
+ this.operator = operator;
+ }
+
+ Error.captureStackTrace(this, stackStartFn || stackStartFunction);
+ // Create error message including the error code in the name.
+ this.stack;
+ // Reset the name.
+ this.name = "AssertionError";
+ }
+
+ toString() {
+ return `${this.name} [${this.code}]: ${this.message}`;
+ }
+
+ [inspect.custom](recurseTimes: number, ctx: Record<string, unknown>) {
+ // Long strings should not be fully inspected.
+ const tmpActual = this.actual;
+ const tmpExpected = this.expected;
+
+ for (const name of ["actual", "expected"]) {
+ if (typeof this[name] === "string") {
+ const value = (this[name] as string);
+ const lines = value.split("\n");
+ if (lines.length > 10) {
+ lines.length = 10;
+ this[name] = `${lines.join("\n")}\n...`;
+ } else if (value.length > 512) {
+ this[name] = `${value.slice(512)}...`;
+ }
+ }
+ }
+
+ // This limits the `actual` and `expected` property default inspection to
+ // the minimum depth. Otherwise those values would be too verbose compared
+ // to the actual error message which contains a combined view of these two
+ // input values.
+ const result = inspect(this, {
+ ...ctx,
+ customInspect: false,
+ depth: 0,
+ });
+
+ // Reset the properties after inspection.
+ this.actual = tmpActual;
+ this.expected = tmpExpected;
+
+ return result;
+ }
+}
+
+export default AssertionError;
diff --git a/std/node/assertion_error_test.ts b/std/node/assertion_error_test.ts
new file mode 100644
index 000000000..b0457fe5f
--- /dev/null
+++ b/std/node/assertion_error_test.ts
@@ -0,0 +1,175 @@
+import { stripColor } from "../fmt/colors.ts";
+import {
+ assert,
+ assertEquals,
+ assertNotStrictEquals,
+ assertStrictEquals,
+} from "../testing/asserts.ts";
+import {
+ AssertionError,
+ copyError,
+ inspectValue,
+ createErrDiff,
+} from "./assertion_error.ts";
+
+Deno.test({
+ name: "copyError()",
+ fn() {
+ class TestError extends Error {}
+ const err = new TestError("this is a test");
+ const copy = copyError(err);
+
+ assert(copy instanceof Error, "Copy should inherit from Error.");
+ assert(copy instanceof TestError, "Copy should inherit from TestError.");
+ assertEquals(copy, err, "Copy should be equal to the original error.");
+ assertNotStrictEquals(
+ copy,
+ err,
+ "Copy should not be strictly equal to the original error.",
+ );
+ },
+});
+
+Deno.test({
+ name: "inspectValue()",
+ fn() {
+ const obj = { a: 1, b: [2] };
+ Object.defineProperty(obj, "c", { value: 3, enumerable: false });
+ assertStrictEquals(
+ stripColor(inspectValue(obj)),
+ "{ a: 1, b: [ 2 ] }",
+ );
+ },
+});
+
+Deno.test({
+ name: "createErrDiff()",
+ fn() {
+ assertStrictEquals(
+ stripColor(
+ createErrDiff({ a: 1, b: 2 }, { a: 2, b: 2 }, "strictEqual"),
+ ),
+ stripColor(
+ 'Expected "actual" to be reference-equal to "expected":' + "\n" +
+ "+ actual - expected" + "\n" +
+ "\n" +
+ "+ { a: 1, b: 2 }" + "\n" +
+ "- { a: 2, b: 2 }",
+ ),
+ );
+ },
+});
+
+Deno.test({
+ name: "construct AssertionError() with given message",
+ fn() {
+ const err = new AssertionError(
+ {
+ message: "answer",
+ actual: "42",
+ expected: "42",
+ operator: "notStrictEqual",
+ },
+ );
+ assertStrictEquals(err.name, "AssertionError");
+ assertStrictEquals(err.message, "answer");
+ assertStrictEquals(err.generatedMessage, false);
+ assertStrictEquals(err.code, "ERR_ASSERTION");
+ assertStrictEquals(err.actual, "42");
+ assertStrictEquals(err.expected, "42");
+ assertStrictEquals(err.operator, "notStrictEqual");
+ },
+});
+
+Deno.test({
+ name: "construct AssertionError() with generated message",
+ fn() {
+ const err = new AssertionError(
+ { actual: 1, expected: 2, operator: "equal" },
+ );
+ assertStrictEquals(err.name, "AssertionError");
+ assertStrictEquals(stripColor(err.message), "1 equal 2");
+ assertStrictEquals(err.generatedMessage, true);
+ assertStrictEquals(err.code, "ERR_ASSERTION");
+ assertStrictEquals(err.actual, 1);
+ assertStrictEquals(err.expected, 2);
+ assertStrictEquals(err.operator, "equal");
+ },
+});
+
+Deno.test({
+ name: "construct AssertionError() with stackStartFn",
+ fn: function stackStartFn() {
+ const expected = /node/;
+ const err = new AssertionError({
+ actual: "deno",
+ expected,
+ operator: "match",
+ stackStartFn,
+ });
+ assertStrictEquals(err.name, "AssertionError");
+ assertStrictEquals(stripColor(err.message), "deno match /node/");
+ assertStrictEquals(err.generatedMessage, true);
+ assertStrictEquals(err.code, "ERR_ASSERTION");
+ assertStrictEquals(err.actual, "deno");
+ assertStrictEquals(err.expected, expected);
+ assertStrictEquals(err.operator, "match");
+ assert(err.stack, "error should have a stack");
+ assert(
+ !err.stack?.includes("stackStartFn"),
+ "stackStartFn() should not present in stack trace",
+ );
+ },
+});
+
+Deno.test({
+ name: "error details",
+ fn() {
+ const stack0 = new Error();
+ const stack1 = new Error();
+ const err = new AssertionError({
+ message: "Function(s) were not called the expected number of times",
+ details: [
+ {
+ message:
+ "Expected the calls function to be executed 2 time(s) but was executed 3 time(s).",
+ actual: 3,
+ expected: 2,
+ operator: "calls",
+ stack: stack0,
+ },
+ {
+ message:
+ "Expected the fn function to be executed 1 time(s) but was executed 0 time(s).",
+ actual: 0,
+ expected: 1,
+ operator: "fn",
+ stack: stack1,
+ },
+ ],
+ });
+
+ assertStrictEquals(
+ err.message,
+ "Function(s) were not called the expected number of times",
+ );
+
+ assertStrictEquals(
+ err["message 0"],
+ "Expected the calls function to be executed 2 time(s) but was executed 3 time(s).",
+ );
+ assertStrictEquals(err["actual 0"], 3);
+ assertStrictEquals(err["expected 0"], 2);
+ assertStrictEquals(err["operator 0"], "calls");
+ assertStrictEquals(err["stack trace 0"], stack0);
+
+ assertStrictEquals(
+ err["message 1"],
+ "Expected the fn function to be executed 1 time(s) but was executed 0 time(s).",
+ );
+ assertStrictEquals(err["actual 1"], 0);
+ assertStrictEquals(err["expected 1"], 1);
+ assertStrictEquals(err["operator 1"], "fn");
+ assertStrictEquals(err["stack trace 1"], stack1);
+ },
+});
diff --git a/std/node/util.ts b/std/node/util.ts
index ce1c06b7c..9cca3e835 100644
--- a/std/node/util.ts
+++ b/std/node/util.ts
@@ -4,13 +4,34 @@ import * as types from "./_util/_util_types.ts";
export { types };
+const DEFAULT_INSPECT_OPTIONS = {
+ showHidden: false,
+ depth: 2,
+ colors: false,
+ customInspect: true,
+ showProxy: false,
+ maxArrayLength: 100,
+ maxStringLength: Infinity,
+ breakLength: 80,
+ compact: 3,
+ sorted: false,
+ getters: false,
+};
+
+inspect.defaultOptions = DEFAULT_INSPECT_OPTIONS;
+inspect.custom = Deno.customInspect;
+
+// TODO(schwarzkopfb): make it in-line with Node's implementation
+// Ref: https://nodejs.org/dist/latest-v14.x/docs/api/util.html#util_util_inspect_object_options
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function inspect(object: unknown, ...opts: any): string {
+ opts = { ...DEFAULT_INSPECT_OPTIONS, ...opts };
return Deno.inspect(object, {
- depth: opts.depth ?? 4,
- iterableLimit: opts.iterableLimit ?? 100,
- compact: !!(opts.compact ?? true),
- sorted: !!(opts.sorted ?? false),
+ depth: opts.depth,
+ iterableLimit: opts.maxArrayLength,
+ compact: !!opts.compact,
+ sorted: !!opts.sorted,
+ showProxy: !!opts.showProxy,
});
}