2019-03-05 14:58:28 -05:00
|
|
|
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
2019-08-24 13:38:18 -04:00
|
|
|
import { red, green, white, gray, bold } from "../fmt/colors.ts";
|
2019-08-14 10:22:31 -04:00
|
|
|
import diff, { DiffType, DiffResult } from "./diff.ts";
|
|
|
|
import { format } from "./format.ts";
|
|
|
|
|
|
|
|
const CAN_NOT_DISPLAY = "[Cannot display]";
|
2019-03-05 14:58:28 -05:00
|
|
|
|
|
|
|
interface Constructor {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
new (...args: any[]): any;
|
|
|
|
}
|
|
|
|
|
2019-03-08 16:04:43 -05:00
|
|
|
export class AssertionError extends Error {
|
|
|
|
constructor(message: string) {
|
|
|
|
super(message);
|
|
|
|
this.name = "AssertionError";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-14 10:22:31 -04:00
|
|
|
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("");
|
2019-10-09 17:22:22 -04:00
|
|
|
diffResult.forEach(
|
|
|
|
(result: DiffResult<string>): void => {
|
|
|
|
const c = createColor(result.type);
|
|
|
|
messages.push(c(`${createSign(result.type)}${result.value}`));
|
|
|
|
}
|
|
|
|
);
|
2019-08-14 10:22:31 -04:00
|
|
|
messages.push("");
|
|
|
|
|
|
|
|
return messages;
|
|
|
|
}
|
|
|
|
|
2019-03-06 16:39:50 -05:00
|
|
|
export function equal(c: unknown, d: unknown): boolean {
|
|
|
|
const seen = new Map();
|
2019-04-24 07:41:23 -04:00
|
|
|
return (function compare(a: unknown, b: unknown): boolean {
|
2019-04-22 09:39:33 -04:00
|
|
|
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;
|
|
|
|
}
|
2019-10-29 20:23:54 -04:00
|
|
|
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;
|
|
|
|
}
|
2019-03-26 08:15:16 -04:00
|
|
|
// 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);
|
|
|
|
}
|
2019-03-06 16:39:50 -05:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2019-03-05 14:58:28 -05:00
|
|
|
/** Make an assertion, if not `true`, then throw. */
|
|
|
|
export function assert(expr: boolean, msg = ""): void {
|
|
|
|
if (!expr) {
|
2019-03-08 16:04:43 -05:00
|
|
|
throw new AssertionError(msg);
|
2019-03-05 14:58:28 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Make an assertion that `actual` and `expected` are equal, deeply. If not
|
|
|
|
* deeply equal, then throw.
|
|
|
|
*/
|
2019-03-06 19:42:24 -05:00
|
|
|
export function assertEquals(
|
2019-03-06 16:39:50 -05:00
|
|
|
actual: unknown,
|
|
|
|
expected: unknown,
|
|
|
|
msg?: string
|
|
|
|
): void {
|
2019-08-14 10:22:31 -04:00
|
|
|
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);
|
2019-03-05 14:58:28 -05:00
|
|
|
}
|
|
|
|
|
2019-03-07 09:08:19 -05:00
|
|
|
/**
|
|
|
|
* 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}`;
|
|
|
|
}
|
2019-03-08 16:04:43 -05:00
|
|
|
throw new AssertionError(msg);
|
2019-03-07 09:08:19 -05:00
|
|
|
}
|
|
|
|
|
2019-03-05 14:58:28 -05:00
|
|
|
/**
|
|
|
|
* 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}`;
|
|
|
|
}
|
2019-03-08 16:04:43 -05:00
|
|
|
throw new AssertionError(msg);
|
2019-03-05 14:58:28 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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}"`;
|
|
|
|
}
|
2019-03-08 16:04:43 -05:00
|
|
|
throw new AssertionError(msg);
|
2019-03-05 14:58:28 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-07 09:08:19 -05:00
|
|
|
/**
|
|
|
|
* Make an assertion that `actual` contains the `expected` values
|
|
|
|
* If not then thrown.
|
|
|
|
*/
|
|
|
|
export function assertArrayContains(
|
|
|
|
actual: unknown[],
|
|
|
|
expected: unknown[],
|
|
|
|
msg?: string
|
2019-03-12 01:51:51 -04:00
|
|
|
): void {
|
2019-10-05 12:02:34 -04:00
|
|
|
const missing: unknown[] = [];
|
2019-03-07 09:08:19 -05:00
|
|
|
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}`;
|
|
|
|
}
|
2019-03-08 16:04:43 -05:00
|
|
|
throw new AssertionError(msg);
|
2019-03-07 09:08:19 -05:00
|
|
|
}
|
|
|
|
|
2019-03-05 14:58:28 -05:00
|
|
|
/**
|
|
|
|
* 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}"`;
|
|
|
|
}
|
2019-03-08 16:04:43 -05:00
|
|
|
throw new AssertionError(msg);
|
2019-03-05 14:58:28 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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}` : "."
|
|
|
|
}`;
|
2019-03-08 16:04:43 -05:00
|
|
|
throw new AssertionError(msg);
|
2019-03-05 14:58:28 -05:00
|
|
|
}
|
2019-03-08 16:04:43 -05:00
|
|
|
if (msgIncludes && !e.message.includes(msgIncludes)) {
|
|
|
|
msg = `Expected error message to include "${msgIncludes}", but got "${
|
|
|
|
e.message
|
|
|
|
}"${msg ? `: ${msg}` : "."}`;
|
|
|
|
throw new AssertionError(msg);
|
2019-03-05 14:58:28 -05:00
|
|
|
}
|
|
|
|
doesThrow = true;
|
|
|
|
}
|
|
|
|
if (!doesThrow) {
|
|
|
|
msg = `Expected function to throw${msg ? `: ${msg}` : "."}`;
|
2019-03-08 16:04:43 -05:00
|
|
|
throw new AssertionError(msg);
|
2019-03-05 14:58:28 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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}` : "."
|
|
|
|
}`;
|
2019-03-08 16:04:43 -05:00
|
|
|
throw new AssertionError(msg);
|
2019-03-05 14:58:28 -05:00
|
|
|
}
|
2019-03-08 16:04:43 -05:00
|
|
|
if (msgIncludes && !e.message.includes(msgIncludes)) {
|
|
|
|
msg = `Expected error message to include "${msgIncludes}", but got "${
|
|
|
|
e.message
|
|
|
|
}"${msg ? `: ${msg}` : "."}`;
|
|
|
|
throw new AssertionError(msg);
|
2019-03-05 14:58:28 -05:00
|
|
|
}
|
|
|
|
doesThrow = true;
|
|
|
|
}
|
|
|
|
if (!doesThrow) {
|
|
|
|
msg = `Expected function to throw${msg ? `: ${msg}` : "."}`;
|
2019-03-08 16:04:43 -05:00
|
|
|
throw new AssertionError(msg);
|
2019-03-05 14:58:28 -05:00
|
|
|
}
|
|
|
|
}
|
2019-03-08 02:32:46 -05:00
|
|
|
|
|
|
|
/** Use this to stub out methods that will throw when invoked. */
|
|
|
|
export function unimplemented(msg?: string): never {
|
2019-03-08 16:04:43 -05:00
|
|
|
throw new AssertionError(msg || "unimplemented");
|
2019-03-08 02:32:46 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Use this to assert unreachable code. */
|
|
|
|
export function unreachable(): never {
|
2019-03-08 16:04:43 -05:00
|
|
|
throw new AssertionError("unreachable");
|
2019-03-08 02:32:46 -05:00
|
|
|
}
|