mirror of
https://github.com/denoland/deno.git
synced 2025-03-04 01:44:26 -05:00
feat(std/node): Add AssertionError class (#7210)
This commit is contained in:
parent
a6f34d4722
commit
f6bfdd66a6
6 changed files with 835 additions and 6 deletions
48
std/node/_errors.ts
Normal file
48
std/node/_errors.ts
Normal file
|
@ -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,
|
||||||
|
};
|
|
@ -7,6 +7,8 @@ import {
|
||||||
assertThrows,
|
assertThrows,
|
||||||
} from "../testing/asserts.ts";
|
} from "../testing/asserts.ts";
|
||||||
|
|
||||||
|
export { AssertionError } from "./assertion_error.ts";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
assert as default,
|
assert as default,
|
||||||
assert as ok,
|
assert as ok,
|
||||||
|
|
|
@ -9,9 +9,9 @@ import {
|
||||||
fail as denoFail,
|
fail as denoFail,
|
||||||
} from "../testing/asserts.ts";
|
} from "../testing/asserts.ts";
|
||||||
|
|
||||||
import assert from "./assert.ts";
|
import AssertionError from "./assertion_error.ts";
|
||||||
|
|
||||||
import {
|
import assert, {
|
||||||
ok,
|
ok,
|
||||||
assert as assert_,
|
assert as assert_,
|
||||||
deepStrictEqual,
|
deepStrictEqual,
|
||||||
|
@ -21,6 +21,7 @@ import {
|
||||||
match,
|
match,
|
||||||
throws,
|
throws,
|
||||||
fail,
|
fail,
|
||||||
|
AssertionError as AssertionError_,
|
||||||
} from "./assert.ts";
|
} from "./assert.ts";
|
||||||
|
|
||||||
Deno.test("API should be exposed", () => {
|
Deno.test("API should be exposed", () => {
|
||||||
|
@ -62,4 +63,9 @@ Deno.test("API should be exposed", () => {
|
||||||
"`assertThrows()` should be exposed as `throws()`",
|
"`assertThrows()` should be exposed as `throws()`",
|
||||||
);
|
);
|
||||||
assertStrictEquals(fail, denoFail, "`fail()` should be exposed");
|
assertStrictEquals(fail, denoFail, "`fail()` should be exposed");
|
||||||
|
assertStrictEquals(
|
||||||
|
AssertionError,
|
||||||
|
AssertionError_,
|
||||||
|
"`AssertionError()` constructor should be exposed",
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
577
std/node/assertion_error.ts
Normal file
577
std/node/assertion_error.ts
Normal file
|
@ -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;
|
175
std/node/assertion_error_test.ts
Normal file
175
std/node/assertion_error_test.ts
Normal file
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
|
@ -4,13 +4,34 @@ import * as types from "./_util/_util_types.ts";
|
||||||
|
|
||||||
export { types };
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function inspect(object: unknown, ...opts: any): string {
|
export function inspect(object: unknown, ...opts: any): string {
|
||||||
|
opts = { ...DEFAULT_INSPECT_OPTIONS, ...opts };
|
||||||
return Deno.inspect(object, {
|
return Deno.inspect(object, {
|
||||||
depth: opts.depth ?? 4,
|
depth: opts.depth,
|
||||||
iterableLimit: opts.iterableLimit ?? 100,
|
iterableLimit: opts.maxArrayLength,
|
||||||
compact: !!(opts.compact ?? true),
|
compact: !!opts.compact,
|
||||||
sorted: !!(opts.sorted ?? false),
|
sorted: !!opts.sorted,
|
||||||
|
showProxy: !!opts.showProxy,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue