import { bgGreen, bgRed, bold, gray, green, red, stripColor, white,} from "../fmt/colors.ts";import { diff, DiffResult, diffstr, DiffType } from "./_diff.ts";
const CAN_NOT_DISPLAY = "[Cannot display]";
export class AssertionError extends Error { name = "AssertionError"; constructor(message: string) { super(message); }}
export function _format(v: unknown): string { const { Deno } = globalThis as any; return typeof Deno?.inspect === "function" ? Deno.inspect(v, { depth: Infinity, sorted: true, trailingComma: true, compact: false, iterableLimit: Infinity, }) : `"${String(v).replace(/(?=["\\])/g, "\\")}"`;}
function createColor( diffType: DiffType, { background = false } = {},): (s: string) => string { switch (diffType) { case DiffType.added: return (s: string): string => background ? bgGreen(white(s)) : green(bold(s)); case DiffType.removed: return (s: string): string => background ? bgRed(white(s)) : red(bold(s)); default: return white; }}
function createSign(diffType: DiffType): string { switch (diffType) { case DiffType.added: return "+ "; case DiffType.removed: return "- "; default: return " "; }}
function buildMessage( diffResult: ReadonlyArray<DiffResult<string>>, { stringDiff = false } = {},): string[] { const messages: string[] = [], diffMessages: string[] = []; messages.push(""); messages.push(""); messages.push( ` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${ green(bold("Expected")) }`, ); messages.push(""); messages.push(""); diffResult.forEach((result: DiffResult<string>): void => { const c = createColor(result.type); const line = result.details?.map((detail) => detail.type !== DiffType.common ? createColor(detail.type, { background: true })(detail.value) : detail.value ).join("") ?? result.value; diffMessages.push(c(`${createSign(result.type)}${line}`)); }); messages.push(...(stringDiff ? [diffMessages.join("")] : diffMessages)); messages.push("");
return messages;}
function isKeyedCollection(x: unknown): x is Set<unknown> { return [Symbol.iterator, "size"].every((k) => k in (x as Set<unknown>));}
export function equal(c: unknown, d: unknown): boolean { const seen = new Map(); return (function compare(a: unknown, b: unknown): boolean { if ( a && b && ((a instanceof RegExp && b instanceof RegExp) || (a instanceof URL && b instanceof URL)) ) { return String(a) === String(b); } if (a instanceof Date && b instanceof Date) { const aTime = a.getTime(); const bTime = b.getTime(); if (Number.isNaN(aTime) && Number.isNaN(bTime)) { return true; } return a.getTime() === b.getTime(); } if (Object.is(a, b)) { return true; } if (a && typeof a === "object" && b && typeof b === "object") { if (a && b && !constructorsEqual(a, b)) { return false; } if (a instanceof WeakMap || b instanceof WeakMap) { if (!(a instanceof WeakMap && b instanceof WeakMap)) return false; throw new TypeError("cannot compare WeakMap instances"); } if (a instanceof WeakSet || b instanceof WeakSet) { if (!(a instanceof WeakSet && b instanceof WeakSet)) return false; throw new TypeError("cannot compare WeakSet instances"); } if (seen.get(a) === b) { return true; } if (Object.keys(a || {}).length !== Object.keys(b || {}).length) { return false; } if (isKeyedCollection(a) && isKeyedCollection(b)) { if (a.size !== b.size) { return false; }
let unmatchedEntries = a.size;
for (const [aKey, aValue] of a.entries()) { for (const [bKey, bValue] of b.entries()) { if ( (aKey === aValue && bKey === bValue && compare(aKey, bKey)) || (compare(aKey, bKey) && compare(aValue, bValue)) ) { unmatchedEntries--; } } }
return unmatchedEntries === 0; } const merged = { ...a, ...b }; for ( const key of [ ...Object.getOwnPropertyNames(merged), ...Object.getOwnPropertySymbols(merged), ] ) { type Key = keyof typeof merged; if (!compare(a && a[key as Key], b && b[key as Key])) { return false; } if (((key in a) && (!(key in b))) || ((key in b) && (!(key in a)))) { return false; } } seen.set(a, b); if (a instanceof WeakRef || b instanceof WeakRef) { if (!(a instanceof WeakRef && b instanceof WeakRef)) return false; return compare(a.deref(), b.deref()); } return true; } return false; })(c, d);}
function constructorsEqual(a: object, b: object) { return a.constructor === b.constructor || a.constructor === Object && !b.constructor || !a.constructor && b.constructor === Object;}
export function assert(expr: unknown, msg = ""): asserts expr { if (!expr) { throw new AssertionError(msg); }}
export function assertEquals( actual: unknown, expected: unknown, msg?: string,): void;export function assertEquals<T>(actual: T, expected: T, msg?: string): void;export function assertEquals( actual: unknown, expected: unknown, msg?: string,): void { if (equal(actual, expected)) { return; } let message = ""; const actualString = _format(actual); const expectedString = _format(expected); try { const stringDiff = (typeof actual === "string") && (typeof expected === "string"); const diffResult = stringDiff ? diffstr(actual as string, expected as string) : diff(actualString.split("\n"), expectedString.split("\n")); const diffMsg = buildMessage(diffResult, { stringDiff }).join("\n"); message = `Values are not equal:\n${diffMsg}`; } catch { message = `\n${red(CAN_NOT_DISPLAY)} + \n\n`; } if (msg) { message = msg; } throw new AssertionError(message);}
export function assertNotEquals( actual: unknown, expected: unknown, msg?: string,): void;export function assertNotEquals<T>(actual: T, expected: T, msg?: string): void;export function assertNotEquals( actual: unknown, expected: unknown, msg?: string,): void { if (!equal(actual, expected)) { return; } let actualString: string; let expectedString: string; try { actualString = String(actual); } catch { actualString = "[Cannot display]"; } try { expectedString = String(expected); } catch { expectedString = "[Cannot display]"; } if (!msg) { msg = `actual: ${actualString} expected: ${expectedString}`; } throw new AssertionError(msg);}
export function assertStrictEquals( actual: unknown, expected: unknown, msg?: string,): void;export function assertStrictEquals<T>( actual: T, expected: T, msg?: string,): void;export function assertStrictEquals( actual: unknown, expected: unknown, msg?: string,): void { if (actual === expected) { return; }
let message: string;
if (msg) { message = msg; } else { const actualString = _format(actual); const expectedString = _format(expected);
if (actualString === expectedString) { const withOffset = actualString .split("\n") .map((l) => ` ${l}`) .join("\n"); message = `Values have the same structure but are not reference-equal:\n\n${ red(withOffset) }\n`; } else { try { const stringDiff = (typeof actual === "string") && (typeof expected === "string"); const diffResult = stringDiff ? diffstr(actual as string, expected as string) : diff(actualString.split("\n"), expectedString.split("\n")); const diffMsg = buildMessage(diffResult, { stringDiff }).join("\n"); message = `Values are not strictly equal:\n${diffMsg}`; } catch { message = `\n${red(CAN_NOT_DISPLAY)} + \n\n`; } } }
throw new AssertionError(message);}
export function assertNotStrictEquals( actual: unknown, expected: unknown, msg?: string,): void;export function assertNotStrictEquals<T>( actual: T, expected: T, msg?: string,): void;export function assertNotStrictEquals( actual: unknown, expected: unknown, msg?: string,): void { if (actual !== expected) { return; }
throw new AssertionError( msg ?? `Expected "actual" to be strictly unequal to: ${_format(actual)}\n`, );}
export function assertExists<T>( actual: T, msg?: string,): asserts actual is NonNullable<T> { if (actual === undefined || actual === null) { if (!msg) { msg = `actual: "${actual}" expected to not be null or undefined`; } throw new AssertionError(msg); }}
export function assertStringIncludes( actual: string, expected: string, msg?: string,): void { if (!actual.includes(expected)) { if (!msg) { msg = `actual: "${actual}" expected to contain: "${expected}"`; } throw new AssertionError(msg); }}
export function assertArrayIncludes( actual: ArrayLike<unknown>, expected: ArrayLike<unknown>, msg?: string,): void;export function assertArrayIncludes<T>( actual: ArrayLike<T>, expected: ArrayLike<T>, msg?: string,): void;export function assertArrayIncludes( actual: ArrayLike<unknown>, expected: ArrayLike<unknown>, msg?: string,): void { const missing: unknown[] = []; for (let i = 0; i < expected.length; i++) { let found = false; for (let j = 0; j < actual.length; j++) { if (equal(expected[i], actual[j])) { found = true; break; } } if (!found) { missing.push(expected[i]); } } if (missing.length === 0) { return; } if (!msg) { msg = `actual: "${_format(actual)}" expected to include: "${ _format(expected) }"\nmissing: ${_format(missing)}`; } throw new AssertionError(msg);}
export function assertMatch( actual: string, expected: RegExp, msg?: string,): void { if (!expected.test(actual)) { if (!msg) { msg = `actual: "${actual}" expected to match: "${expected}"`; } throw new AssertionError(msg); }}
export function assertNotMatch( actual: string, expected: RegExp, msg?: string,): void { if (expected.test(actual)) { if (!msg) { msg = `actual: "${actual}" expected to not match: "${expected}"`; } throw new AssertionError(msg); }}
export function assertObjectMatch( actual: Record<PropertyKey, any>, expected: Record<PropertyKey, unknown>,): void { type loose = Record<PropertyKey, unknown>; const seen = new WeakMap(); function filter(a: loose, b: loose): loose { if (Array.isArray(a)) { return a; }
if ((seen.has(a)) && (seen.get(a) === b)) { return a; } seen.set(a, b); const filtered = {} as loose; const entries = [ ...Object.getOwnPropertyNames(a), ...Object.getOwnPropertySymbols(a), ] .filter((key) => key in b) .map((key) => [key, a[key as string]]) as Array<[string, unknown]>; for (const [key, value] of entries) { if (Array.isArray(value)) { const subset = (b as loose)[key]; if (Array.isArray(subset)) { filtered[key] = value .slice(0, subset.length) .map((element, index) => { const subsetElement = subset[index]; if ((typeof subsetElement === "object") && (subsetElement)) { return filter(element, subsetElement); } return element; }); continue; } } else if (typeof value === "object") { const subset = (b as loose)[key]; if ((typeof subset === "object") && (subset)) { filtered[key] = filter(value as loose, subset as loose); continue; } } filtered[key] = value; } return filtered; } return assertEquals( filter(actual, expected), filter(expected, expected), );}
export function fail(msg?: string): never { assert(false, `Failed assertion${msg ? `: ${msg}` : "."}`);}
export function assertIsError<E extends Error = Error>( error: unknown, ErrorClass?: new (...args: any[]) => E, msgIncludes?: string, msg?: string,): asserts error is E { if (error instanceof Error === false) { throw new AssertionError(`Expected "error" to be an Error object.`); } if (ErrorClass && !(error instanceof ErrorClass)) { msg = `Expected error to be instance of "${ErrorClass.name}", but was "${ typeof error === "object" ? error?.constructor?.name : "[not an object]" }"${msg ? `: ${msg}` : "."}`; throw new AssertionError(msg); } if ( msgIncludes && (!(error instanceof Error) || !stripColor(error.message).includes(stripColor(msgIncludes))) ) { msg = `Expected error message to include "${msgIncludes}", but got "${ error instanceof Error ? error.message : "[not an Error]" }"${msg ? `: ${msg}` : "."}`; throw new AssertionError(msg); }}
export function assertThrows<E extends Error = Error>( fn: () => unknown, ErrorClass?: new (...args: any[]) => E, msgIncludes?: string, msg?: string,): void;export function assertThrows( fn: () => unknown, errorCallback: (e: Error) => unknown, msg?: string,): void;export function assertThrows<E extends Error = Error>( fn: () => unknown, errorClassOrCallback?: | (new (...args: any[]) => E) | ((e: Error) => unknown), msgIncludesOrMsg?: string, msg?: string,): void { let ErrorClass: (new (...args: any[]) => E) | undefined = undefined; let msgIncludes: string | undefined = undefined; let errorCallback; if ( errorClassOrCallback == null || errorClassOrCallback.prototype instanceof Error || errorClassOrCallback.prototype === Error.prototype ) { ErrorClass = errorClassOrCallback as new (...args: any[]) => E; msgIncludes = msgIncludesOrMsg; errorCallback = null; } else { errorCallback = errorClassOrCallback as (e: Error) => unknown; msg = msgIncludesOrMsg; } let doesThrow = false; try { fn(); } catch (error) { if (error instanceof Error === false) { throw new AssertionError("A non-Error object was thrown."); } assertIsError( error, ErrorClass, msgIncludes, msg, ); if (typeof errorCallback == "function") { errorCallback(error); } doesThrow = true; } if (!doesThrow) { msg = `Expected function to throw${msg ? `: ${msg}` : "."}`; throw new AssertionError(msg); }}
export function assertRejects<E extends Error = Error>( fn: () => Promise<unknown>, ErrorClass?: new (...args: any[]) => E, msgIncludes?: string, msg?: string,): Promise<void>;export function assertRejects( fn: () => Promise<unknown>, errorCallback: (e: Error) => unknown, msg?: string,): Promise<void>;export async function assertRejects<E extends Error = Error>( fn: () => Promise<unknown>, errorClassOrCallback?: | (new (...args: any[]) => E) | ((e: Error) => unknown), msgIncludesOrMsg?: string, msg?: string,): Promise<void> { let ErrorClass: (new (...args: any[]) => E) | undefined = undefined; let msgIncludes: string | undefined = undefined; let errorCallback; if ( errorClassOrCallback == null || errorClassOrCallback.prototype instanceof Error || errorClassOrCallback.prototype === Error.prototype ) { ErrorClass = errorClassOrCallback as new (...args: any[]) => E; msgIncludes = msgIncludesOrMsg; errorCallback = null; } else { errorCallback = errorClassOrCallback as (e: Error) => unknown; msg = msgIncludesOrMsg; } let doesThrow = false; try { await fn(); } catch (error) { if (error instanceof Error === false) { throw new AssertionError("A non-Error object was thrown or rejected."); } assertIsError( error, ErrorClass, msgIncludes, msg, ); if (typeof errorCallback == "function") { errorCallback(error); } doesThrow = true; } if (!doesThrow) { msg = `Expected function to throw${msg ? `: ${msg}` : "."}`; throw new AssertionError(msg); }}
export const assertThrowsAsync = assertRejects;
export function unimplemented(msg?: string): never { throw new AssertionError(msg || "unimplemented");}
export function unreachable(): never { throw new AssertionError("unreachable");}