mirror of
https://github.com/Start9Labs/start-sdk.git
synced 2026-03-30 12:21:57 +00:00
440 lines
14 KiB
JavaScript
440 lines
14 KiB
JavaScript
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
|
// This module is browser compatible. Do not rely on good formatting of values
|
|
// for AssertionError messages in browsers.
|
|
import * as dntShim from "../../../../_dnt.test_shims.js";
|
|
import { bold, gray, green, red, stripColor, white } from "../fmt/colors.js";
|
|
import { diff, DiffType } from "./_diff.js";
|
|
const CAN_NOT_DISPLAY = "[Cannot display]";
|
|
export class AssertionError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = "AssertionError";
|
|
}
|
|
}
|
|
/**
|
|
* Converts the input into a string. Objects, Sets and Maps are sorted so as to
|
|
* make tests less flaky
|
|
* @param v Value to be formatted
|
|
*/
|
|
export function _format(v) {
|
|
return dntShim.dntGlobalThis.Deno
|
|
? dntShim.Deno.inspect(v, {
|
|
depth: Infinity,
|
|
sorted: true,
|
|
trailingComma: true,
|
|
compact: false,
|
|
iterableLimit: Infinity,
|
|
})
|
|
: `"${String(v).replace(/(?=["\\])/g, "\\")}"`;
|
|
}
|
|
/**
|
|
* Colors the output of assertion diffs
|
|
* @param diffType Difference type, either added or removed
|
|
*/
|
|
function createColor(diffType) {
|
|
switch (diffType) {
|
|
case DiffType.added:
|
|
return (s) => green(bold(s));
|
|
case DiffType.removed:
|
|
return (s) => red(bold(s));
|
|
default:
|
|
return white;
|
|
}
|
|
}
|
|
/**
|
|
* Prefixes `+` or `-` in diff output
|
|
* @param diffType Difference type, either added or removed
|
|
*/
|
|
function createSign(diffType) {
|
|
switch (diffType) {
|
|
case DiffType.added:
|
|
return "+ ";
|
|
case DiffType.removed:
|
|
return "- ";
|
|
default:
|
|
return " ";
|
|
}
|
|
}
|
|
function buildMessage(diffResult) {
|
|
const messages = [];
|
|
messages.push("");
|
|
messages.push("");
|
|
messages.push(` ${gray(bold("[Diff]"))} ${red(bold("Actual"))} / ${green(bold("Expected"))}`);
|
|
messages.push("");
|
|
messages.push("");
|
|
diffResult.forEach((result) => {
|
|
const c = createColor(result.type);
|
|
messages.push(c(`${createSign(result.type)}${result.value}`));
|
|
});
|
|
messages.push("");
|
|
return messages;
|
|
}
|
|
function isKeyedCollection(x) {
|
|
return [Symbol.iterator, "size"].every((k) => k in x);
|
|
}
|
|
/**
|
|
* Deep equality comparison used in assertions
|
|
* @param c actual value
|
|
* @param d expected value
|
|
*/
|
|
export function equal(c, d) {
|
|
const seen = new Map();
|
|
return (function compare(a, b) {
|
|
// 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 URL && b instanceof URL))) {
|
|
return String(a) === String(b);
|
|
}
|
|
if (a instanceof Date && b instanceof Date) {
|
|
const aTime = a.getTime();
|
|
const bTime = b.getTime();
|
|
// Check for NaN equality manually since NaN is not
|
|
// equal to itself.
|
|
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 (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()) {
|
|
/* Given that Map keys can be references, we need
|
|
* to ensure that they are also deeply equal */
|
|
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),
|
|
]) {
|
|
if (!compare(a && a[key], b && b[key])) {
|
|
return false;
|
|
}
|
|
if (((key in a) && (!(key in b))) || ((key in b) && (!(key in a)))) {
|
|
return false;
|
|
}
|
|
}
|
|
seen.set(a, b);
|
|
return true;
|
|
}
|
|
return false;
|
|
})(c, d);
|
|
}
|
|
/** Make an assertion, error will be thrown if `expr` does not have truthy value. */
|
|
export function assert(expr, msg = "") {
|
|
if (!expr) {
|
|
throw new AssertionError(msg);
|
|
}
|
|
}
|
|
export function assertEquals(actual, expected, msg) {
|
|
if (equal(actual, expected)) {
|
|
return;
|
|
}
|
|
let message = "";
|
|
const actualString = _format(actual);
|
|
const expectedString = _format(expected);
|
|
try {
|
|
const diffResult = diff(actualString.split("\n"), expectedString.split("\n"));
|
|
const diffMsg = buildMessage(diffResult).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, expected, msg) {
|
|
if (!equal(actual, expected)) {
|
|
return;
|
|
}
|
|
let actualString;
|
|
let expectedString;
|
|
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, expected, msg) {
|
|
if (actual === expected) {
|
|
return;
|
|
}
|
|
let message;
|
|
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 diffResult = diff(actualString.split("\n"), expectedString.split("\n"));
|
|
const diffMsg = buildMessage(diffResult).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, expected, msg) {
|
|
if (actual !== expected) {
|
|
return;
|
|
}
|
|
throw new AssertionError(msg ?? `Expected "actual" to be strictly unequal to: ${_format(actual)}\n`);
|
|
}
|
|
/**
|
|
* Make an assertion that actual is not null or undefined. If not
|
|
* then thrown.
|
|
*/
|
|
export function assertExists(actual, msg) {
|
|
if (actual === undefined || actual === null) {
|
|
if (!msg) {
|
|
msg =
|
|
`actual: "${actual}" expected to match anything but null or undefined`;
|
|
}
|
|
throw new AssertionError(msg);
|
|
}
|
|
}
|
|
/**
|
|
* Make an assertion that actual includes expected. If not
|
|
* then thrown.
|
|
*/
|
|
export function assertStringIncludes(actual, expected, msg) {
|
|
if (!actual.includes(expected)) {
|
|
if (!msg) {
|
|
msg = `actual: "${actual}" expected to contain: "${expected}"`;
|
|
}
|
|
throw new AssertionError(msg);
|
|
}
|
|
}
|
|
export function assertArrayIncludes(actual, expected, msg) {
|
|
const missing = [];
|
|
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);
|
|
}
|
|
/**
|
|
* Make an assertion that `actual` match RegExp `expected`. If not
|
|
* then thrown
|
|
*/
|
|
export function assertMatch(actual, expected, msg) {
|
|
if (!expected.test(actual)) {
|
|
if (!msg) {
|
|
msg = `actual: "${actual}" expected to match: "${expected}"`;
|
|
}
|
|
throw new AssertionError(msg);
|
|
}
|
|
}
|
|
/**
|
|
* Make an assertion that `actual` not match RegExp `expected`. If match
|
|
* then thrown
|
|
*/
|
|
export function assertNotMatch(actual, expected, msg) {
|
|
if (expected.test(actual)) {
|
|
if (!msg) {
|
|
msg = `actual: "${actual}" expected to not match: "${expected}"`;
|
|
}
|
|
throw new AssertionError(msg);
|
|
}
|
|
}
|
|
/**
|
|
* Make an assertion that `actual` object is a subset of `expected` object, deeply.
|
|
* If not, then throw.
|
|
*/
|
|
export function assertObjectMatch(
|
|
// deno-lint-ignore no-explicit-any
|
|
actual, expected) {
|
|
const seen = new WeakMap();
|
|
return assertEquals((function filter(a, b) {
|
|
// Prevent infinite loop with circular references with same filter
|
|
if ((seen.has(a)) && (seen.get(a) === b)) {
|
|
return a;
|
|
}
|
|
seen.set(a, b);
|
|
// Filter keys and symbols which are present in both actual and expected
|
|
const filtered = {};
|
|
const entries = [
|
|
...Object.getOwnPropertyNames(a),
|
|
...Object.getOwnPropertySymbols(a),
|
|
]
|
|
.filter((key) => key in b)
|
|
.map((key) => [key, a[key]]);
|
|
for (const [key, value] of entries) {
|
|
// On array references, build a filtered array and filter nested objects inside
|
|
if (Array.isArray(value)) {
|
|
const subset = b[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;
|
|
}
|
|
} // On nested objects references, build a filtered object recursively
|
|
else if (typeof value === "object") {
|
|
const subset = b[key];
|
|
if ((typeof subset === "object") && (subset)) {
|
|
filtered[key] = filter(value, subset);
|
|
continue;
|
|
}
|
|
}
|
|
filtered[key] = value;
|
|
}
|
|
return filtered;
|
|
})(actual, expected), expected);
|
|
}
|
|
/**
|
|
* Forcefully throws a failed assertion
|
|
*/
|
|
export function fail(msg) {
|
|
// 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, ErrorClass, msgIncludes = "", msg) {
|
|
let doesThrow = false;
|
|
let error = null;
|
|
try {
|
|
fn();
|
|
}
|
|
catch (e) {
|
|
if (e instanceof Error === false) {
|
|
throw new AssertionError("A non-Error object was thrown.");
|
|
}
|
|
if (ErrorClass && !(e instanceof ErrorClass)) {
|
|
msg =
|
|
`Expected error to be instance of "${ErrorClass.name}", but was "${e.constructor.name}"${msg ? `: ${msg}` : "."}`;
|
|
throw new AssertionError(msg);
|
|
}
|
|
if (msgIncludes &&
|
|
!stripColor(e.message).includes(stripColor(msgIncludes))) {
|
|
msg =
|
|
`Expected error message to include "${msgIncludes}", but got "${e.message}"${msg ? `: ${msg}` : "."}`;
|
|
throw new AssertionError(msg);
|
|
}
|
|
doesThrow = true;
|
|
error = e;
|
|
}
|
|
if (!doesThrow) {
|
|
msg = `Expected function to throw${msg ? `: ${msg}` : "."}`;
|
|
throw new AssertionError(msg);
|
|
}
|
|
return error;
|
|
}
|
|
/**
|
|
* Executes a function which returns a promise, expecting it to throw or reject.
|
|
* 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 async function assertThrowsAsync(fn, ErrorClass, msgIncludes = "", msg) {
|
|
let doesThrow = false;
|
|
let error = null;
|
|
try {
|
|
await fn();
|
|
}
|
|
catch (e) {
|
|
if (e instanceof Error === false) {
|
|
throw new AssertionError("A non-Error object was thrown or rejected.");
|
|
}
|
|
if (ErrorClass && !(e instanceof ErrorClass)) {
|
|
msg =
|
|
`Expected error to be instance of "${ErrorClass.name}", but got "${e.name}"${msg ? `: ${msg}` : "."}`;
|
|
throw new AssertionError(msg);
|
|
}
|
|
if (msgIncludes &&
|
|
!stripColor(e.message).includes(stripColor(msgIncludes))) {
|
|
msg =
|
|
`Expected error message to include "${msgIncludes}", but got "${e.message}"${msg ? `: ${msg}` : "."}`;
|
|
throw new AssertionError(msg);
|
|
}
|
|
doesThrow = true;
|
|
error = e;
|
|
}
|
|
if (!doesThrow) {
|
|
msg = `Expected function to throw${msg ? `: ${msg}` : "."}`;
|
|
throw new AssertionError(msg);
|
|
}
|
|
return error;
|
|
}
|
|
/** Use this to stub out methods that will throw when invoked. */
|
|
export function unimplemented(msg) {
|
|
throw new AssertionError(msg || "unimplemented");
|
|
}
|
|
/** Use this to assert unreachable code. */
|
|
export function unreachable() {
|
|
throw new AssertionError("unreachable");
|
|
}
|