Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions assert/equals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,41 @@ import { format } from "@std/internal/format";

import { AssertionError } from "./assertion_error.ts";

// Walks `value` (avoiding cycles) and returns true if any own property,
// array element, Map value, or Set element is a function. Used to surface
// a hint when assertEquals throws on inputs whose only difference is a
// function property, which prints as an identical-looking `[Function: …]`
// in the diff but compares by reference.
function containsFunction(value: unknown, seen: WeakSet<object>): boolean {
if (typeof value === "function") return true;
if (value === null || typeof value !== "object") return false;
if (seen.has(value as object)) return false;
seen.add(value as object);
if (value instanceof Map) {
for (const v of value.values()) {
if (containsFunction(v, seen)) return true;
}
return false;
}
if (value instanceof Set) {
for (const v of value.values()) {
if (containsFunction(v, seen)) return true;
}
return false;
}
for (const k of Reflect.ownKeys(value as object)) {
if (
containsFunction(
(value as Record<string | symbol, unknown>)[k],
seen,
)
) {
return true;
}
}
return false;
}

/**
* Make an assertion that `actual` and `expected` are equal, deeply. If not
* deeply equal, then throw.
Expand Down Expand Up @@ -54,6 +89,14 @@ export function assertEquals<T>(
const msgSuffix = msg ? `: ${msg}` : ".";
let message = `Values are not equal${msgSuffix}`;

if (
containsFunction(actual, new WeakSet()) ||
containsFunction(expected, new WeakSet())
) {
message +=
"\n Note: function properties are compared by reference, so two distinct functions print the same as `[Function: name]` but are not equal.";
}

const actualString = format(actual);
const expectedString = format(expected);
const stringDiff = (typeof actual === "string") &&
Expand Down
51 changes: 51 additions & 0 deletions assert/equals_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,57 @@ Deno.test({
},
});

Deno.test({
name:
"assertEquals() adds a function-by-reference hint when functions are present",
fn() {
// https://github.com/denoland/std/issues/6878
// Identical-looking function properties print as `[Function: y]` on
// both sides, which makes the failure confusing. The hint clarifies
// that functions compare by reference.
const error = assertThrows(
() =>
assertEquals(
{ x: 1, y: () => 2 },
{ x: 1, y: () => 2 },
),
AssertionError,
);
const message = stripAnsiCode((error as AssertionError).message);
if (!message.includes("function properties are compared by reference")) {
throw new Error(
`expected message to include the function-reference hint, got:\n${message}`,
);
}

// Also fires for function values nested in arrays and Maps.
const arrayError = assertThrows(
() => assertEquals([() => 1], [() => 1]),
AssertionError,
);
if (
!stripAnsiCode((arrayError as AssertionError).message).includes(
"function properties are compared by reference",
)
) {
throw new Error("expected hint for array of functions");
}

// Does NOT fire when neither side has any function values.
const plainError = assertThrows(
() => assertEquals({ x: 1 }, { x: 2 }),
AssertionError,
);
if (
stripAnsiCode((plainError as AssertionError).message).includes(
"function properties are compared by reference",
)
) {
throw new Error("hint must not appear when there are no functions");
}
},
});

Deno.test({
name: "assertEquals() matches same Set with object keys",
fn() {
Expand Down
Loading