std/testing/asserts.ts
2019-09-11 23:31:23 -04:00

360 lines
8.6 KiB
TypeScript

// 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<DiffResult<string>>): 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<string>): 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;
}
// 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 {
let 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<void>,
ErrorClass?: Constructor,
msgIncludes = "",
msg?: string
): Promise<void> {
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");
}