import { inspect } from "./util.ts";import { stripColor as removeColors } from "../fmt/colors.ts";
function getConsoleWidth(): number { try { return Deno.consoleSize().columns; } catch { return 80; }}
const MathMax = Math.max;const { Error } = globalThis;const { create: ObjectCreate, defineProperty: ObjectDefineProperty, getPrototypeOf: ObjectGetPrototypeOf, getOwnPropertyDescriptor: ObjectGetOwnPropertyDescriptor, keys: ObjectKeys,} = Object;
import { ERR_INVALID_ARG_TYPE } from "./internal/errors.ts";
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:",};
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 { return inspect( val, { compact: true, customInspect: false, depth: 1000, maxArrayLength: Infinity, showHidden: false, showProxy: false, sorted: true, 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 = "";
if ( operator === "strictEqual" && ((typeof actual === "object" && actual !== null && typeof expected === "object" && expected !== null) || (typeof actual === "function" && typeof expected === "function")) ) { operator = "strictEqualObject"; }
if ( actualLines.length === 1 && expectedLines.length === 1 && actualLines[0] !== expectedLines[0] ) { 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 (inputLength <= kMaxShortLength) { if ( (typeof actual !== "object" || actual === null) && (typeof expected !== "object" || expected === null) && (actual !== 0 || expected !== 0) ) { return `${kReadableOperator[operator]}\n\n` + `${actualLines[0]} !== ${expectedLines[0]}\n`; } } else if (operator !== "strictEqualObject") { const maxLength = Deno.isatty(Deno.stderr.rid) ? getConsoleWidth() : 80; if (inputLength < maxLength) { while (actualRaw[i] === expectedRaw[i]) { i++; } if (i > 2) { indicator = `\n ${" ".repeat(i)}^`; i = 0; } } } }
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); if (maxLines === 0) { const actualLines = actualInspected.split("\n");
if (actualLines.length > 50) { actualLines[46] = `${blue}...${defaultColor}`; while (actualLines.length > 47) { actualLines.pop(); } }
return `${kReadableOperator.notIdentical}\n\n${actualLines.join("\n")}\n`; }
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 (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++; } identical = 0; if (lines === actualLines) { res += `\n${plusMinus} ${lines[i]}`; } else { other += `\n${plusMinus} ${lines[i]}`; } printedLines++; } else { const expectedLine = expectedLines[i]; let actualLine = actualLines[i]; let divergingLines = actualLine !== expectedLine && (!actualLine.endsWith(",") || actualLine.slice(0, -1) !== expectedLine); if ( divergingLines && expectedLine.endsWith(",") && expectedLine.slice(0, -1) === actualLine ) { divergingLines = false; actualLine += ","; } if (divergingLines) { 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++; } identical = 0; res += `\n${green}+${defaultColor} ${actualLine}`; other += `\n${red}-${defaultColor} ${expectedLine}`; printedLines += 2; } else { res += other; other = ""; identical++; if (identical <= 2) { res += `\n ${actualLine}`; printedLines++; } } } 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[]; stackStartFn?: Function; stackStartFunction?: Function;}
interface ErrorWithStackTraceLimit extends ErrorConstructor { stackTraceLimit: number;}
export class AssertionError extends Error { [key: string]: unknown;
constructor(options: AssertionErrorConstructorOptions) { if (typeof options !== "object" || options === null) { throw new ERR_INVALID_ARG_TYPE("options", "Object", options); } const { message, operator, stackStartFn, details, stackStartFunction, } = options; let { actual, expected, } = options;
const limit = (Error as ErrorWithStackTraceLimit).stackTraceLimit; (Error as ErrorWithStackTraceLimit).stackTraceLimit = 0;
if (message != null) { super(String(message)); } else { if (Deno.isatty(Deno.stderr.rid)) { if (Deno.noColor) { blue = ""; green = ""; defaultColor = ""; red = ""; } else { blue = "\u001b[34m"; green = "\u001b[32m"; defaultColor = "\u001b[39m"; red = "\u001b[31m"; } } 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" ) { let base = kReadableOperator[operator]; const res = inspectValue(actual).split("\n");
if ( operator === "notStrictEqual" && ((typeof actual === "object" && actual !== null) || typeof actual === "function") ) { base = kReadableOperator.notStrictEqualObject; }
if (res.length > 50) { res[46] = `${blue}...${defaultColor}`; while (res.length > 47) { res.pop(); } }
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", { __proto__: null, value: "AssertionError [ERR_ASSERTION]", enumerable: false, writable: true, configurable: true, } as any); 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); this.stack; this.name = "AssertionError"; }
override toString() { return `${this.name} [${this.code}]: ${this.message}`; }
[inspect.custom](_recurseTimes: number, ctx: Record<string, unknown>) { 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)}...`; } } }
const result = inspect(this, { ...ctx, customInspect: false, depth: 0, });
this.actual = tmpActual; this.expected = tmpExpected;
return result; }}
export default AssertionError;