From f6bfdd66a69fb01077e488b5a67a38ca3d43610e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Schwarzkopf=20Bal=C3=A1zs?= Date: Mon, 14 Sep 2020 16:22:07 +0200 Subject: [PATCH] feat(std/node): Add AssertionError class (#7210) --- std/node/_errors.ts | 48 +++ std/node/assert.ts | 2 + std/node/assert_test.ts | 10 +- std/node/assertion_error.ts | 577 +++++++++++++++++++++++++++++++ std/node/assertion_error_test.ts | 175 ++++++++++ std/node/util.ts | 29 +- 6 files changed, 835 insertions(+), 6 deletions(-) create mode 100644 std/node/_errors.ts create mode 100644 std/node/assertion_error.ts create mode 100644 std/node/assertion_error_test.ts diff --git a/std/node/_errors.ts b/std/node/_errors.ts new file mode 100644 index 0000000000..2321bb24f4 --- /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 3d738cbbf3..2a4f8a0b57 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 8df13187bb..9d38ca56b5 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 0000000000..46122b658f --- /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) { + // 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 0000000000..b0457fe5f5 --- /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 ce1c06b7c3..9cca3e8358 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, }); }