// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. import { red, green, white, gray, bold } from "../fmt/colors.ts"; import diff, { DiffType, DiffResult } from "./diff.ts"; import { format } from "./format.ts"; const CAN_NOT_DISPLAY = "[Cannot display]"; interface Constructor { // eslint-disable-next-line @typescript-eslint/no-explicit-any new (...args: any[]): any; } export class AssertionError extends Error { constructor(message: string) { super(message); this.name = "AssertionError"; } } function createStr(v: unknown): string { try { return format(v); } catch (e) { return red(CAN_NOT_DISPLAY); } } function createColor(diffType: DiffType): (s: string) => string { switch (diffType) { case DiffType.added: return (s: string): string => green(bold(s)); case DiffType.removed: return (s: string): string => 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>): string[] { const messages: string[] = []; messages.push(""); messages.push(""); messages.push( ` ${gray(bold("[Diff]"))} ${red(bold("Left"))} / ${green(bold("Right"))}` ); messages.push(""); messages.push(""); diffResult.forEach( (result: DiffResult): void => { const c = createColor(result.type); messages.push(c(`${createSign(result.type)}${result.value}`)); } ); messages.push(""); return messages; } export function equal(c: unknown, d: unknown): boolean { const seen = new Map(); return (function compare(a: unknown, b: unknown): boolean { if (a && a instanceof Set && b && b instanceof Set) { if (a.size !== b.size) { return false; } for (const item of b) { if (!a.has(item)) { return false; } } return true; } if (a && b && a instanceof Map && b instanceof Map) { if (a.size !== b.size) { return false; } for (const [key, value] of a) { if (!compare(value, b.get(key))) { return false; } } return true; } // Have to render RegExp & Date for string comparison // unless it's mistreated as object if ( a && b && ((a instanceof RegExp && b instanceof RegExp) || (a instanceof Date && b instanceof Date)) ) { return String(a) === String(b); } if (Object.is(a, b)) { return true; } if (a && typeof a === "object" && b && typeof b === "object") { if (seen.get(a) === b) { return true; } if (Object.keys(a || {}).length !== Object.keys(b || {}).length) { return false; } const merged = { ...a, ...b }; for (const key in merged) { type Key = keyof typeof merged; if (!compare(a && a[key as Key], b && b[key as Key])) { return false; } } seen.set(a, b); return true; } return false; })(c, d); } /** Make an assertion, if not `true`, then throw. */ export function assert(expr: boolean, msg = ""): void { if (!expr) { throw new AssertionError(msg); } } /** * Make an assertion that `actual` and `expected` are equal, deeply. If not * deeply equal, then throw. */ export function assertEquals( actual: unknown, expected: unknown, msg?: string ): void { if (equal(actual, expected)) { return; } let message = ""; const actualString = createStr(actual); const expectedString = createStr(expected); try { const diffResult = diff( actualString.split("\n"), expectedString.split("\n") ); message = buildMessage(diffResult).join("\n"); } catch (e) { message = `\n${red(CAN_NOT_DISPLAY)} + \n\n`; } if (msg) { message = msg; } throw new AssertionError(message); } /** * Make an assertion that `actual` and `expected` are not equal, deeply. * If not then throw. */ 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 (e) { actualString = "[Cannot display]"; } try { expectedString = String(expected); } catch (e) { expectedString = "[Cannot display]"; } if (!msg) { msg = `actual: ${actualString} expected: ${expectedString}`; } throw new AssertionError(msg); } /** * Make an assertion that `actual` and `expected` are strictly equal. If * not then throw. */ export function assertStrictEq( actual: unknown, expected: unknown, msg?: string ): void { if (actual !== expected) { let actualString: string; let expectedString: string; try { actualString = String(actual); } catch (e) { actualString = "[Cannot display]"; } try { expectedString = String(expected); } catch (e) { expectedString = "[Cannot display]"; } if (!msg) { msg = `actual: ${actualString} expected: ${expectedString}`; } throw new AssertionError(msg); } } /** * Make an assertion that actual contains expected. If not * then thrown. */ export function assertStrContains( actual: string, expected: string, msg?: string ): void { if (!actual.includes(expected)) { if (!msg) { msg = `actual: "${actual}" expected to contains: "${expected}"`; } throw new AssertionError(msg); } } /** * Make an assertion that `actual` contains the `expected` values * If not then thrown. */ export function assertArrayContains( actual: unknown[], expected: 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: "${actual}" expected to contains: "${expected}"`; msg += "\n"; msg += `missing: ${missing}`; } throw new AssertionError(msg); } /** * Make an assertion that `actual` match RegExp `expected`. If not * then thrown */ 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); } } /** * Forcefully throws a failed assertion */ export function fail(msg?: string): void { // eslint-disable-next-line @typescript-eslint/no-use-before-define assert(false, `Failed assertion${msg ? `: ${msg}` : "."}`); } /** Executes a function, expecting it to throw. If it does not, then it * throws. An error class and a string that should be included in the * error message can also be asserted. */ export function assertThrows( fn: () => void, ErrorClass?: Constructor, msgIncludes = "", msg?: string ): void { let doesThrow = false; try { fn(); } catch (e) { if (ErrorClass && !(Object.getPrototypeOf(e) === ErrorClass.prototype)) { msg = `Expected error to be instance of "${ErrorClass.name}"${ msg ? `: ${msg}` : "." }`; throw new AssertionError(msg); } if (msgIncludes && !e.message.includes(msgIncludes)) { msg = `Expected error message to include "${msgIncludes}", but got "${ e.message }"${msg ? `: ${msg}` : "."}`; throw new AssertionError(msg); } doesThrow = true; } if (!doesThrow) { msg = `Expected function to throw${msg ? `: ${msg}` : "."}`; throw new AssertionError(msg); } } export async function assertThrowsAsync( fn: () => Promise, ErrorClass?: Constructor, msgIncludes = "", msg?: string ): Promise { let doesThrow = false; try { await fn(); } catch (e) { if (ErrorClass && !(Object.getPrototypeOf(e) === ErrorClass.prototype)) { msg = `Expected error to be instance of "${ErrorClass.name}"${ msg ? `: ${msg}` : "." }`; throw new AssertionError(msg); } if (msgIncludes && !e.message.includes(msgIncludes)) { msg = `Expected error message to include "${msgIncludes}", but got "${ e.message }"${msg ? `: ${msg}` : "."}`; throw new AssertionError(msg); } doesThrow = true; } if (!doesThrow) { msg = `Expected function to throw${msg ? `: ${msg}` : "."}`; throw new AssertionError(msg); } } /** Use this to stub out methods that will throw when invoked. */ export function unimplemented(msg?: string): never { throw new AssertionError(msg || "unimplemented"); } /** Use this to assert unreachable code. */ export function unreachable(): never { throw new AssertionError("unreachable"); }