2024-01-01 14:58:21 -05:00
|
|
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
2023-12-01 21:20:06 -05:00
|
|
|
// vendored from std/assert/mod.ts
|
2023-02-14 11:38:45 -05:00
|
|
|
|
2023-12-08 04:00:03 -05:00
|
|
|
import { primordials } from "ext:core/mod.js";
|
|
|
|
const {
|
|
|
|
DatePrototype,
|
|
|
|
ArrayPrototypeJoin,
|
|
|
|
ArrayPrototypeMap,
|
|
|
|
DatePrototypeGetTime,
|
|
|
|
Error,
|
|
|
|
NumberIsNaN,
|
|
|
|
Object,
|
|
|
|
ObjectIs,
|
|
|
|
ObjectKeys,
|
|
|
|
ObjectPrototypeIsPrototypeOf,
|
|
|
|
ReflectHas,
|
|
|
|
ReflectOwnKeys,
|
|
|
|
RegExpPrototype,
|
|
|
|
RegExpPrototypeTest,
|
|
|
|
SafeMap,
|
|
|
|
SafeRegExp,
|
|
|
|
String,
|
|
|
|
StringPrototypeReplace,
|
|
|
|
StringPrototypeSplit,
|
|
|
|
SymbolIterator,
|
|
|
|
TypeError,
|
|
|
|
WeakMapPrototype,
|
|
|
|
WeakSetPrototype,
|
|
|
|
WeakRefPrototype,
|
|
|
|
WeakRefPrototypeDeref,
|
|
|
|
} = primordials;
|
|
|
|
|
2024-01-10 17:37:25 -05:00
|
|
|
import { URLPrototype } from "ext:deno_url/00_url.js";
|
|
|
|
import { red } from "ext:deno_node/_util/std_fmt_colors.ts";
|
|
|
|
import {
|
|
|
|
buildMessage,
|
|
|
|
diff,
|
|
|
|
diffstr,
|
|
|
|
} from "ext:deno_node/_util/std_testing_diff.ts";
|
|
|
|
|
2023-12-08 04:00:03 -05:00
|
|
|
const FORMAT_PATTERN = new SafeRegExp(/(?=["\\])/g);
|
2023-02-14 11:38:45 -05:00
|
|
|
|
|
|
|
/** Converts the input into a string. Objects, Sets and Maps are sorted so as to
|
|
|
|
* make tests less flaky */
|
|
|
|
export function format(v: unknown): string {
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
|
|
const { Deno } = globalThis as any;
|
|
|
|
return typeof Deno?.inspect === "function"
|
|
|
|
? Deno.inspect(v, {
|
|
|
|
depth: Infinity,
|
|
|
|
sorted: true,
|
|
|
|
trailingComma: true,
|
|
|
|
compact: false,
|
|
|
|
iterableLimit: Infinity,
|
|
|
|
// getters should be true in assertEquals.
|
|
|
|
getters: true,
|
|
|
|
})
|
2023-12-08 04:00:03 -05:00
|
|
|
: `"${StringPrototypeReplace(String(v), FORMAT_PATTERN, "\\")}"`;
|
2023-02-14 11:38:45 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
const CAN_NOT_DISPLAY = "[Cannot display]";
|
|
|
|
|
|
|
|
export class AssertionError extends Error {
|
|
|
|
override name = "AssertionError";
|
|
|
|
constructor(message: string) {
|
|
|
|
super(message);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-08 04:00:03 -05:00
|
|
|
function isKeyedCollection(
|
|
|
|
x: unknown,
|
|
|
|
): x is { size: number; entries(): Iterable<[unknown, unknown]> } {
|
|
|
|
return ReflectHas(x, SymbolIterator) && ReflectHas(x, "size");
|
2023-02-14 11:38:45 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Deep equality comparison used in assertions */
|
|
|
|
export function equal(c: unknown, d: unknown): boolean {
|
2023-12-08 04:00:03 -05:00
|
|
|
const seen = new SafeMap();
|
2023-02-14 11:38:45 -05:00
|
|
|
return (function compare(a: unknown, b: unknown): boolean {
|
|
|
|
// Have to render RegExp & Date for string comparison
|
|
|
|
// unless it's mistreated as object
|
|
|
|
if (
|
|
|
|
a &&
|
|
|
|
b &&
|
2023-12-08 04:00:03 -05:00
|
|
|
((ObjectPrototypeIsPrototypeOf(RegExpPrototype, a) &&
|
|
|
|
ObjectPrototypeIsPrototypeOf(RegExpPrototype, b)) ||
|
|
|
|
(ObjectPrototypeIsPrototypeOf(URLPrototype, a) &&
|
|
|
|
ObjectPrototypeIsPrototypeOf(URLPrototype, b)))
|
2023-02-14 11:38:45 -05:00
|
|
|
) {
|
|
|
|
return String(a) === String(b);
|
|
|
|
}
|
2023-12-08 04:00:03 -05:00
|
|
|
if (
|
|
|
|
ObjectPrototypeIsPrototypeOf(DatePrototype, a) &&
|
|
|
|
ObjectPrototypeIsPrototypeOf(DatePrototype, b)
|
|
|
|
) {
|
|
|
|
const aTime = DatePrototypeGetTime(a);
|
|
|
|
const bTime = DatePrototypeGetTime(b);
|
2023-02-14 11:38:45 -05:00
|
|
|
// Check for NaN equality manually since NaN is not
|
|
|
|
// equal to itself.
|
2023-12-08 04:00:03 -05:00
|
|
|
if (NumberIsNaN(aTime) && NumberIsNaN(bTime)) {
|
2023-02-14 11:38:45 -05:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return aTime === bTime;
|
|
|
|
}
|
|
|
|
if (typeof a === "number" && typeof b === "number") {
|
2023-12-08 04:00:03 -05:00
|
|
|
return NumberIsNaN(a) && NumberIsNaN(b) || a === b;
|
2023-02-14 11:38:45 -05:00
|
|
|
}
|
2023-12-08 04:00:03 -05:00
|
|
|
if (ObjectIs(a, b)) {
|
2023-02-14 11:38:45 -05:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (a && typeof a === "object" && b && typeof b === "object") {
|
|
|
|
if (a && b && !constructorsEqual(a, b)) {
|
|
|
|
return false;
|
|
|
|
}
|
2023-12-08 04:00:03 -05:00
|
|
|
if (
|
|
|
|
ObjectPrototypeIsPrototypeOf(WeakMapPrototype, a) ||
|
|
|
|
ObjectPrototypeIsPrototypeOf(WeakMapPrototype, b)
|
|
|
|
) {
|
|
|
|
if (
|
|
|
|
!(ObjectPrototypeIsPrototypeOf(WeakMapPrototype, a) &&
|
|
|
|
ObjectPrototypeIsPrototypeOf(WeakMapPrototype, b))
|
|
|
|
) return false;
|
2023-02-14 11:38:45 -05:00
|
|
|
throw new TypeError("cannot compare WeakMap instances");
|
|
|
|
}
|
2023-12-08 04:00:03 -05:00
|
|
|
if (
|
|
|
|
ObjectPrototypeIsPrototypeOf(WeakSetPrototype, a) ||
|
|
|
|
ObjectPrototypeIsPrototypeOf(WeakSetPrototype, b)
|
|
|
|
) {
|
|
|
|
if (
|
|
|
|
!(ObjectPrototypeIsPrototypeOf(WeakSetPrototype, a) &&
|
|
|
|
ObjectPrototypeIsPrototypeOf(WeakSetPrototype, b))
|
|
|
|
) return false;
|
2023-02-14 11:38:45 -05:00
|
|
|
throw new TypeError("cannot compare WeakSet instances");
|
|
|
|
}
|
|
|
|
if (seen.get(a) === b) {
|
|
|
|
return true;
|
|
|
|
}
|
2023-12-08 04:00:03 -05:00
|
|
|
if (ObjectKeys(a || {}).length !== ObjectKeys(b || {}).length) {
|
2023-02-14 11:38:45 -05:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
seen.set(a, b);
|
|
|
|
if (isKeyedCollection(a) && isKeyedCollection(b)) {
|
|
|
|
if (a.size !== b.size) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let unmatchedEntries = a.size;
|
|
|
|
|
2023-12-08 04:00:03 -05:00
|
|
|
// TODO(petamoriken): use primordials
|
|
|
|
// deno-lint-ignore prefer-primordials
|
2023-02-14 11:38:45 -05:00
|
|
|
for (const [aKey, aValue] of a.entries()) {
|
2023-12-08 04:00:03 -05:00
|
|
|
// deno-lint-ignore prefer-primordials
|
2023-02-14 11:38:45 -05:00
|
|
|
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--;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return unmatchedEntries === 0;
|
|
|
|
}
|
2023-12-08 04:00:03 -05:00
|
|
|
|
2023-02-14 11:38:45 -05:00
|
|
|
const merged = { ...a, ...b };
|
2023-12-08 04:00:03 -05:00
|
|
|
const keys = ReflectOwnKeys(merged);
|
|
|
|
for (let i = 0; i < keys.length; ++i) {
|
|
|
|
const key = keys[i];
|
2023-02-14 11:38:45 -05:00
|
|
|
type Key = keyof typeof merged;
|
|
|
|
if (!compare(a && a[key as Key], b && b[key as Key])) {
|
|
|
|
return false;
|
|
|
|
}
|
2023-12-08 04:00:03 -05:00
|
|
|
if (
|
|
|
|
(ReflectHas(a, key) && !ReflectHas(b, key)) ||
|
|
|
|
(ReflectHas(b, key) && !ReflectHas(a, key))
|
|
|
|
) {
|
2023-02-14 11:38:45 -05:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2023-12-08 04:00:03 -05:00
|
|
|
|
|
|
|
if (
|
|
|
|
ObjectPrototypeIsPrototypeOf(WeakRefPrototype, a) ||
|
|
|
|
ObjectPrototypeIsPrototypeOf(WeakRefPrototype, b)
|
|
|
|
) {
|
|
|
|
if (
|
|
|
|
!(ObjectPrototypeIsPrototypeOf(WeakRefPrototype, a) &&
|
|
|
|
ObjectPrototypeIsPrototypeOf(WeakRefPrototype, b))
|
|
|
|
) return false;
|
|
|
|
return compare(WeakRefPrototypeDeref(a), WeakRefPrototypeDeref(b));
|
2023-02-14 11:38:45 -05:00
|
|
|
}
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Make an assertion, error will be thrown if `expr` does not have truthy value. */
|
|
|
|
export function assert(expr: unknown, msg = ""): asserts expr {
|
|
|
|
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<T>(actual: T, expected: T, msg?: string) {
|
|
|
|
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)
|
2023-12-08 04:00:03 -05:00
|
|
|
: diff(
|
|
|
|
StringPrototypeSplit(actualString, "\n"),
|
|
|
|
StringPrototypeSplit(expectedString, "\n"),
|
|
|
|
);
|
|
|
|
const diffMsg = ArrayPrototypeJoin(
|
|
|
|
buildMessage(diffResult, { stringDiff }),
|
|
|
|
"\n",
|
|
|
|
);
|
2023-02-14 11:38:45 -05:00
|
|
|
message = `Values are not equal:\n${diffMsg}`;
|
|
|
|
} catch {
|
|
|
|
message = `\n${red(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<T>(actual: T, expected: T, msg?: string) {
|
|
|
|
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 not to be: ${expectedString}`;
|
|
|
|
}
|
|
|
|
throw new AssertionError(msg);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Make an assertion that `actual` and `expected` are strictly equal. If
|
|
|
|
* not then throw. */
|
|
|
|
export function assertStrictEquals<T>(
|
|
|
|
actual: unknown,
|
|
|
|
expected: T,
|
|
|
|
msg?: string,
|
|
|
|
): asserts actual is T {
|
2023-12-08 04:00:03 -05:00
|
|
|
if (ObjectIs(actual, expected)) {
|
2023-02-14 11:38:45 -05:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let message: string;
|
|
|
|
|
|
|
|
if (msg) {
|
|
|
|
message = msg;
|
|
|
|
} else {
|
|
|
|
const actualString = format(actual);
|
|
|
|
const expectedString = format(expected);
|
|
|
|
|
|
|
|
if (actualString === expectedString) {
|
2023-12-08 04:00:03 -05:00
|
|
|
const withOffset = ArrayPrototypeJoin(
|
|
|
|
ArrayPrototypeMap(
|
|
|
|
StringPrototypeSplit(actualString, "\n"),
|
|
|
|
(l: string) => ` ${l}`,
|
|
|
|
),
|
|
|
|
"\n",
|
|
|
|
);
|
2023-02-14 11:38:45 -05:00
|
|
|
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)
|
2023-12-08 04:00:03 -05:00
|
|
|
: diff(
|
|
|
|
StringPrototypeSplit(actualString, "\n"),
|
|
|
|
StringPrototypeSplit(expectedString, "\n"),
|
|
|
|
);
|
|
|
|
const diffMsg = ArrayPrototypeJoin(
|
|
|
|
buildMessage(diffResult, { stringDiff }),
|
|
|
|
"\n",
|
|
|
|
);
|
2023-02-14 11:38:45 -05:00
|
|
|
message = `Values are not strictly equal:\n${diffMsg}`;
|
|
|
|
} catch {
|
|
|
|
message = `\n${CAN_NOT_DISPLAY} + \n\n`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new AssertionError(message);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Make an assertion that `actual` and `expected` are not strictly equal.
|
|
|
|
* If the values are strictly equal then throw. */
|
|
|
|
export function assertNotStrictEquals<T>(
|
|
|
|
actual: T,
|
|
|
|
expected: T,
|
|
|
|
msg?: string,
|
|
|
|
) {
|
2023-12-08 04:00:03 -05:00
|
|
|
if (!ObjectIs(actual, expected)) {
|
2023-02-14 11:38:45 -05:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new AssertionError(
|
|
|
|
msg ?? `Expected "actual" to be strictly unequal to: ${format(actual)}\n`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Make an assertion that `actual` match RegExp `expected`. If not
|
|
|
|
* then throw. */
|
|
|
|
export function assertMatch(
|
|
|
|
actual: string,
|
|
|
|
expected: RegExp,
|
|
|
|
msg?: string,
|
|
|
|
) {
|
2023-12-08 04:00:03 -05:00
|
|
|
if (!RegExpPrototypeTest(expected, actual)) {
|
2023-02-14 11:38:45 -05:00
|
|
|
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 throw. */
|
|
|
|
export function assertNotMatch(
|
|
|
|
actual: string,
|
|
|
|
expected: RegExp,
|
|
|
|
msg?: string,
|
|
|
|
) {
|
2023-12-08 04:00:03 -05:00
|
|
|
if (RegExpPrototypeTest(expected, actual)) {
|
2023-02-14 11:38:45 -05:00
|
|
|
if (!msg) {
|
|
|
|
msg = `actual: "${actual}" expected to not match: "${expected}"`;
|
|
|
|
}
|
|
|
|
throw new AssertionError(msg);
|
|
|
|
}
|
|
|
|
}
|