Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(assert): assertObjectMatch doesn't print whole object #5498

Merged
merged 3 commits into from
Jul 21, 2024
Merged
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
161 changes: 93 additions & 68 deletions assert/object_match.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,82 +32,107 @@ export function assertObjectMatch(
expected: Record<PropertyKey, unknown>,
msg?: string,
): void {
type loose = Record<PropertyKey, unknown>;
return assertEquals(
// get the intersection of "actual" and "expected"
// side effect: all the instances' constructor field is "Object" now.
filter(actual, expected),
// set (nested) instances' constructor field to be "Object" without changing expected value.
// see https://github.com/denoland/deno_std/pull/1419
filter(expected, expected),
msg,
);
}

type loose = Record<PropertyKey, unknown>;

function isObject(val: unknown): boolean {
return typeof val === "object" && val !== null;
}

function filter(a: loose, b: loose) {
const seen = new WeakMap();
return filterObject(a, b);

function filterObject(a: loose, b: loose): loose {
// Prevent infinite loop with circular references with same filter
if ((seen.has(a)) && (seen.get(a) === b)) {
return a;
}

try {
seen.set(a, b);
} catch (err) {
if (err instanceof TypeError) {
throw new TypeError(
`Cannot assertObjectMatch ${a === null ? null : `type ${typeof a}`}`,
);
}
}

function filter(a: loose, b: loose) {
const seen = new WeakMap();
return fn(a, b);
// Filter keys and symbols which are present in both actual and expected
const filtered = {} as loose;
const keysA = Reflect.ownKeys(a);
const keysB = Reflect.ownKeys(b);
const entries = keysA.filter((key) => keysB.includes(key))
.map((key) => [key, a[key as string]]) as Array<[string, unknown]>;

function fn(a: loose, b: loose): loose {
// Prevent infinite loop with circular references with same filter
if ((seen.has(a)) && (seen.get(a) === b)) {
return a;
if (keysA.length && keysB.length && !entries.length) {
// If both objects are not empty but don't have the same keys or symbols,
// returns the entries in object a.
for (const key of keysA) {
filtered[key] = a[key];
}
try {
seen.set(a, b);
} catch (err) {
if (err instanceof TypeError) {
throw new TypeError(
`Cannot assertObjectMatch ${
a === null ? null : `type ${typeof a}`
}`,

return filtered;
}

for (const [key, value] of entries) {
// On regexp references, keep value as it to avoid loosing pattern and flags
if (value instanceof RegExp) {
filtered[key] = value;
continue;
}

const subset = (b as loose)[key];

// On array references, build a filtered array and filter nested objects inside
if (Array.isArray(value) && Array.isArray(subset)) {
filtered[key] = filterObject({ ...value }, { ...subset });
continue;
}

// On nested objects references, build a filtered object recursively
if (isObject(value) && isObject(subset)) {
// When both operands are maps, build a filtered map with common keys and filter nested objects inside
if ((value instanceof Map) && (subset instanceof Map)) {
filtered[key] = new Map(
[...value].filter(([k]) => subset.has(k)).map(
([k, v]) => {
const v2 = subset.get(k);
if (isObject(v) && isObject(v2)) {
return [k, filterObject(v as loose, v2 as loose)];
}

return [k, v];
},
),
);
continue;
}
}
// Filter keys and symbols which are present in both actual and expected
const filtered = {} as loose;
const entries = [
...Object.getOwnPropertyNames(a),
...Object.getOwnPropertySymbols(a),
]
.filter((key) => key in b)
.map((key) => [key, a[key as string]]) as Array<[string, unknown]>;
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 as loose)[key];
if (Array.isArray(subset)) {
filtered[key] = fn({ ...value }, { ...subset });
continue;
}
} // On regexp references, keep value as it to avoid loosing pattern and flags
else if (value instanceof RegExp) {
filtered[key] = value;

// When both operands are set, build a filtered set with common values
if ((value instanceof Set) && (subset instanceof Set)) {
filtered[key] = value.intersection(subset);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

continue;
} // On nested objects references, build a filtered object recursively
else if (typeof value === "object" && value !== null) {
const subset = (b as loose)[key];
if ((typeof subset === "object") && subset) {
// When both operands are maps, build a filtered map with common keys and filter nested objects inside
if ((value instanceof Map) && (subset instanceof Map)) {
filtered[key] = new Map(
[...value].filter(([k]) => subset.has(k)).map((
[k, v],
) => [k, typeof v === "object" ? fn(v, subset.get(k)) : v]),
);
continue;
}
// When both operands are set, build a filtered set with common values
if ((value instanceof Set) && (subset instanceof Set)) {
filtered[key] = new Set([...value].filter((v) => subset.has(v)));
continue;
}
filtered[key] = fn(value as loose, subset as loose);
continue;
}
}
filtered[key] = value;

filtered[key] = filterObject(value as loose, subset as loose);
continue;
}
return filtered;

filtered[key] = value;
}

return filtered;
}
return assertEquals(
// get the intersection of "actual" and "expected"
// side effect: all the instances' constructor field is "Object" now.
filter(actual, expected),
// set (nested) instances' constructor field to be "Object" without changing expected value.
// see https://github.com/denoland/deno_std/pull/1419
filter(expected, expected),
msg,
);
}
43 changes: 43 additions & 0 deletions assert/object_match_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,9 @@ Deno.test("assertObjectMatch() throws assertion error when in the first argument
() => assertObjectMatch({ foo: undefined, bar: null }, { foo: null }),
AssertionError,
);
assertThrows(
() => assertObjectMatch(n, { baz: new Map([["b", null]]) }),
);
});

Deno.test("assertObjectMatch() throws readable type error for non mappable primitive types", () => {
Expand All @@ -319,3 +322,43 @@ Deno.test("assertObjectMatch() throws readable type error for non mappable primi
"assertObjectMatch",
);
});

Deno.test("assertObjectMatch() prints inputs correctly", () => {
const x = {
command: "error",
payload: {
message: "NodeNotFound",
},
protocol: "graph",
};

const y = {
protocol: "graph",
command: "addgroup",
payload: {
graph: "foo",
metadata: {
description: "foo",
},
name: "somegroup",
},
};

assertThrows(
() => assertObjectMatch(x, y),
AssertionError,
` {
+ command: "addgroup",
- command: "error",
payload: {
+ graph: "foo",
+ metadata: {
+ description: "foo",
+ },
+ name: "somegroup",
- message: "NodeNotFound",
},
protocol: "graph",
}`,
);
});