Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d990694
bun:test: compare an expected Date, Error, Map, Set or function by va…
robobun Oct 2, 2026
9587d09
test: fix url.parse IPv6 expectations that a top level objectContaini…
robobun Oct 3, 2026
3a5802e
Merge branch 'main' into robobun/53ec321e/deep-match-date-error
robobun Oct 23, 2026
4164bc6
Match an expected function of any kind by identity, and URLSearchPara…
robobun Oct 23, 2026
8b34056
Compare two primitive property values in the subset walk without a call
robobun Oct 23, 2026
607886f
Format the new toMatchObject tests
robobun Oct 23, 2026
f7f5591
Keep Bun__deepMatch and check an expected Date and Error at its entry
robobun Oct 23, 2026
e8b8489
Merge remote-tracking branch 'origin/main' into robobun/53ec321e/deep…
robobun Nov 1, 2026
f2ef13e
bun:test: define subsetDateOrErrorMismatch below the Bun__deepEquals …
robobun Nov 1, 2026
abb3445
bun:test: compare an expected Map, Set and function in toMatchObject …
robobun Nov 1, 2026
020e2aa
bun:test: leave private names out of the key walk of toMatchObject an…
robobun Nov 2, 2026
d3917f7
Merge commit '020e2aab73' into HEAD
robobun Nov 2, 2026
b1b82d4
bun:test: reduce this branch to the Set and Map rules, on top of the …
robobun Nov 2, 2026
b97c159
test: a property matcher with a private field still compares its othe…
robobun Nov 2, 2026
484b909
Remove a comment that repeats PrivateSymbolMode::Exclude
robobun Nov 2, 2026
bbfff52
Shorten the comments of the Set and Map rules to one line each
robobun Nov 2, 2026
62bbe84
Merge commit '484b9092f2' into HEAD
robobun Nov 2, 2026
db32a16
Shorten the comment of subsetDateOrErrorMismatch to one line
robobun Nov 2, 2026
26b7806
test: toMatchObject compares an expected Set and Map under the vitest…
robobun Nov 2, 2026
08eceab
Merge commit 'db32a16b79' into robobun/fd48c6eb/deep-match-map-set-fu…
robobun Nov 2, 2026
25329d0
toMatchObject: try the expected member behind the last match first in…
robobun Nov 2, 2026
b0f58a7
Merge remote-tracking branch 'origin/main' into robobun/fd48c6eb/deep…
robobun Nov 2, 2026
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
5 changes: 5 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2937,6 +2937,11 @@ declare module "bun" {
* Returns true if all properties in the subset exist in the
* other and have equal values.
*
* A `Date` in the subset matches a `Date` with the same time value.
* An `Error` in the subset matches an `Error` with the same message.
* A `Set` or a `Map` in the subset matches one of the same size whose
* members match.
*
* This also powers expect().toMatchObject in `bun:test`
*/
function deepMatch(subset: unknown, a: unknown): boolean;
Expand Down
333 changes: 237 additions & 96 deletions src/jsc/bindings/bindings.cpp

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions src/jsc/bindings/headers-handwritten.h
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,52 @@ struct DeepMatchState {
recorded.append(patternOfValue);
recordedNames.append(name);
}

// False if this (value, pattern) pair already matched or is still being compared (a cycle). Both are in `gcBuffer` already.
bool markSeen(JSC::JSValue value, JSC::JSValue pattern)
{
std::pair<JSC::EncodedJSValue, JSC::EncodedJSValue> pair { JSC::JSValue::encode(value), JSC::JSValue::encode(pattern) };
if (!seen.insert(pair).second)
return false;
if (openTrials) [[unlikely]]
seenInTrials.append(pair);
return true;
}

// One try of a Set member or a Map entry against a candidate. A try that fails takes back what it put in `seen`, `gcBuffer` and the records.
unsigned openTrials { 0 };
WTF::Vector<std::pair<JSC::EncodedJSValue, JSC::EncodedJSValue>> seenInTrials;

struct Trial {
DeepMatchState& state;
size_t seenBegin;
size_t gcBegin;
size_t recordsBegin;
bool matched { false };

Trial(DeepMatchState& state)
: state(state)
, seenBegin(state.seenInTrials.size())
, gcBegin(state.gcBuffer.size())
, recordsBegin(state.recordedNames.size())
{
state.openTrials++;
}

~Trial()
{
if (!matched) {
for (size_t i = state.seenInTrials.size(); i-- > seenBegin;)
state.seen.erase(state.seenInTrials[i]);
state.seenInTrials.shrink(seenBegin);
state.gcBuffer.shrink(gcBegin);
state.recorded.shrink(recordsBegin * 4);
state.recordedNames.shrink(recordsBegin);
}
if (!--state.openTrials)
state.seenInTrials.shrink(0);
}
};
};
}

Expand Down
98 changes: 66 additions & 32 deletions test/js/bun/bun-object/deep-match.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,23 @@ describe("Bun.deepMatch", () => {
new Set(["a", "b", "c"]),
new Set(["a", "b", "c"]),
],
// a member or a value of `b` may be a superset of the one in `a`
[{ m: new Map([ ["foo", { a: 1 }] ]) }, { m: new Map([ ["foo", { a: 1, b: 2 }] ]) }],
[{ s: new Set([{ a: 1 }]) }, { s: new Set([{ a: 1, b: 2 }]) }],
[{ m: new Map([ ["foo", 1], ["bar", 2] ]) }, { m: new Map([ ["bar", 2], ["foo", 1] ]) }],

// Dates and Errors
[new Date(1), new Date(1)],
[{ d: new Date(1) }, { d: new Date(1), other: 1 }],
[{ d: [new Date(1)] }, { d: [new Date(1)] }],
[new Error("a"), new Error("a")],
[{ e: new Error("a") }, { e: new Error("a", { cause: 1 }) }],
[{ e: new Error("a") }, { e: Object.assign(new Error("a"), { code: 1 }) }],
// a plain object in the subset matches the properties of any object
[{ message: "a" }, new Error("a")],
[{ d: {} }, { d: new Date() }],
[{ m: {} }, { m: new Map([ ["foo", 1] ]) }],
[{ s: { size: 1 } }, { s: new Set([1]) }],
])("Bun.deepMatch(%p, %p) === true", (a, b) => {
expect(Bun.deepMatch(a, b)).toBe(true);
});
Expand All @@ -100,39 +117,56 @@ describe("Bun.deepMatch", () => {
[[], [undefined]],
[["a", "b", "c"], ["a", "b", "d"]],

// Maps
// FIXME: I assume this is incorrect but I need confirmation on expected behavior.
// [
// new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
// new Map<number, number>([ [1, 2], [2, 3] ]),
// ],
// [
// new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
// new Map<number, number>([ [1, 2], [2, 3], [3, 4], [4, 5] ]),
// ],
// [
// new Map<number, number>([ [1, 2], [2, 3], [3, 4], [4, 5] ]),
// new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
// ],
// Maps: the same size, and each entry of `b` matches an entry of `a`
[
new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
new Map<number, number>([ [1, 2], [2, 3] ]),
],
[
new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
new Map<number, number>([ [1, 2], [2, 3], [3, 4], [4, 5] ]),
],
[
new Map<number, number>([ [1, 2], [2, 3], [3, 4], [4, 5] ]),
new Map<number, number>([ [1, 2], [2, 3], [3, 4] ]),
],
[new Map([ ["foo", 1] ]), new Map([ ["foo", 2] ])],
[new Map([ ["foo", 1] ]), new Map([ ["bar", 1] ])],
[{ m: new Map([ ["foo", { a: 1, b: 2 }] ]) }, { m: new Map([ ["foo", { a: 1 }] ]) }],
[{ m: new Map() }, { m: {} }],
[{ m: new Map() }, { m: new Set() }],

// Sets
// FIXME: I assume this is incorrect but I need confirmation on expected behavior.
// [
// new Set([1, 2, 3]),
// new Set([4, 5, 6]),
// ],
// [
// new Set([1, 2, 3]),
// new Set([1, 2]),
// ],
// [
// new Set([1, 2]),
// new Set([1, 2, 3]),
// ],
// [
// new Set(["a", "b", "c"]),
// new Set(["a", "b", "d"]),
// ],
// Sets: the same size, and each member of `b` matches a member of `a`
[
new Set([1, 2, 3]),
new Set([4, 5, 6]),
],
[
new Set([1, 2, 3]),
new Set([1, 2]),
],
[
new Set([1, 2]),
new Set([1, 2, 3]),
],
[
new Set(["a", "b", "c"]),
new Set(["a", "b", "d"]),
],
[{ s: new Set([{ a: 1, b: 2 }]) }, { s: new Set([{ a: 1 }]) }],
[{ s: new Set() }, { s: {} }],
[{ s: new Set([1]) }, { s: [1] }],

// A Date is compared by its time value, an Error by its message
[new Date(1), new Date(2)],
[{ d: new Date(1) }, { d: new Date(2) }],
[{ d: new Date() }, { d: {} }],
[{ d: new Date(1) }, { d: [new Date(1)] }],
[new Error("a"), new Error("b")],
[{ e: new Error("a") }, { e: new Error("b") }],
[{ e: new Error("a") }, { e: { message: "a" } }],
[{ e: new Error("a") }, { e: {} }],
[{ e: Object.assign(new Error("a"), { code: 1 }) }, { e: new Error("a") }],
])("Bun.deepMatch(%p, %p) === false", (a, b) => {
expect(Bun.deepMatch(a, b)).toBe(false);
});
Expand Down
Loading
Loading