Repository navigation
Conversation
…lue in toMatchObject and Bun.deepMatch Subset matching becomes an isPartial mode of Bun__deepEquals. The expected value's type selects the comparison, so a Date, Error, Map, Set, RegExp, typed array, boxed primitive or function on the expected side goes through the same typed arm toEqual uses, and only a plain object or array is walked key by key. The untyped Bun__deepMatch walk is removed.
…ng never checked toMatchObject now applies an asymmetric matcher passed as the whole expected value, so the IPv6 spot checks ran for the first time and three of them asserted values node does not produce.
…ms in both directions
Subset matching dispatched on JSFunctionType only, so an expected native
constructor, mock function or callable Proxy was still walked key by key:
`expect({ f: jest.fn() }).toMatchObject({ f: jest.fn() })` and
`Bun.deepMatch(Set, Map)` passed. Any callable expected value now matches
only itself.
The URLSearchParams arm checks one direction and relies on the second call
with the sides swapped. Subset matching calls an arm once, so
`a=1&a=1` matched `a=1&b=1`. The arm now checks both directions in that
mode.
Add the cases of the per type pull requests for functions, Headers,
URLSearchParams and Temporal values.
The subset walk called Bun__deepEquals for every property. For two primitives that call only runs sameValue. Do that in place.
The isPartial mode of Bun__deepEquals took over the cycle rule and the typed arms of toEqual. A received object that refers to itself, a float array with NaN and Headers with two Set-Cookie values no longer matched an equal pattern. The mode is removed and the subset walk is as on main again. The walk lists the enumerable keys of the expected value. A Date and an Error have none, so any object matched. Before the walk, an expected Date now needs a Date with the same time value, and an expected Error needs an Error with the same message. The enumerable properties of the expected value are matched after that, as before. The comparison of Map, Set, functions and the other typed values is not part of this change.
…-match-date-error
…instantiation The helper sat directly above the comment of the explicit instantiation of Bun__deepEquals. A change to that comment and this helper then touch the same lines. It now sits between the instantiation and Bun__deepMatch. No behaviour changes.
…and Bun.deepMatch Bun__deepMatch walks the enumerable keys of the expected value. A Map, a Set and a function keep what they hold elsewhere, so an expected one matched any object. An asymmetric matcher that was the whole expected value was walked as a plain object too. - An expected Set or Map needs one of the same size. Each received member or entry must match an expected one as a subset. A candidate that does not match leaves nothing behind in the state of the walk. - An expected function matches only itself. A method that the expected object inherits is not compared. - A matcher that is the whole expected or received value is applied. The property loop, the members of a Set and the entries of a Map share one pair function, subsetPairMatches.
…d Bun.deepMatch Bun__deepMatch listed the keys of the expected value with PrivateSymbolMode::Include. So it compared the #fields of a class, which Jest, Vitest and toEqual do not see, and for each array it walked the builtins that Array.prototype keeps under private names. Both lists now leave private names out, as Bun__deepEquals does. Those builtins were also what made an expected array refuse an object that is not an array. That rule is now written out: an expected array needs an array.
# Conflicts: # test/js/bun/test/expect.test.js
…private-name change Merge of the new base: #47736 (private names leave the key walk) and the branch of #44497 on top of it. After a review, this branch keeps the Set and Map rules only: - Function identity and the whole-pattern matcher are taken out. They stay in their own pull requests. - The tests of server.spec.ts and url-parse-ipv6.test.ts and the elysia entry of test/vendor.json go back to what main has. - matchEitherAsymmetricMatcher is shared with Bun__deepEquals. A value that only has the type of a built-in matcher, such as a Response, no longer hides a matcher on the other side. - What is compared inside a Set or a Map is recorded again, and a candidate that fails takes its records back. A matcher under a property is shown whichever of the two was compared first. - A received Proxy is not read as a Set or a Map.
…nction # Conflicts: # src/jsc/bindings/bindings.cpp
… a Set or a Map The scan for a received member that is not in the expected Set or Map started at the first expected member each time, so two Sets of N cloned objects in the same order took N squared over 2 tries. The scan now keeps its iterator. It first tries the expected member behind the one that matched last. If that try fails, it starts at the first expected member as before and leaves that one member out. Members that are not found by identity and follow each other in the same order take one try each: 50 tries for 50 members, against 1,275. Another order keeps the old number of tries plus at most one for each member, and so does a Set with members found by identity between the others. One try is now the static helper subsetCandidateMatches. Tests: each order of four members for a Set and a Map, a failed try in each order, a matcher that throws, the Map path where the value under the same key differs, and the number of tries.
…-match-map-set-function
… toMatchObject and Bun.deepMatch The array gate of Bun__deepMatch compared getArrayLength() of both operands. That reads the indexed storage of the cell, and a Proxy has none. So a Proxy of an array that is not empty never had the length of the array it wraps, and the match failed. A Vue reactive() array and a Pinia store array are such Proxies. Two arrays with indexed storage keep the storage compare. Any other pair reads `length` with [[Get]] from each Proxy operand and compares the two lengths with ===, which is the rule the gate already has for two arrays. Part of #9103.
The matcher cast the sample and the value to JSArray. A Proxy of an array is not one, so it never matched, and expect.not.arrayContaining passed for every Proxy. The constructor rejected a Proxy sample with "Expected a array". When the sample or the value is a Proxy, both are classified with JSC::isArray and read with [[Get]]. Two real arrays, and a value that is not an array, take the path they took before. The two paths share the containment loop. The two assertions that #27529 added for this matcher in isArray-proxy-crash.test.ts asserted the failed match that replaced the crash. They now assert the match.
…-match-proxy-array
…ey after the matcher, drain stdout - The two assertions for a Proxy of an object as the sample of expect.arrayContaining now pin the error of the sample check. - The rows for a trap that throws under expect.not.arrayContaining in toMatchObject have a second key after the matcher, so the match reads on after the matcher. - The snapshot subprocess test reads stdout, and the two subprocess tests are concurrent.
…y-array' into robobun/6e0587e4/deep-match-served-set-map
…t-map # Conflicts: # test/js/bun/test/expect.test.js
…t or a Map by the members it serves toMatchObject and Bun.deepMatch did not read a Proxy around a Set or a Map. A received Proxy matched each expected Set and each expected Map. An expected Proxy was only a pattern of properties. They now ask the Proxy what script asks it, as Jest and Vitest do. They read its iterator method and its `size`, and they run its iterator. They call `has` and `get` of a Proxy on the expected side. They read the value of a Map entry after its key is found. The target of a Proxy only tells if the Proxy stands for a Set or for a Map. A Proxy that serves no iterator method has no rule. Its properties are matched. A Proxy that does not serve its Set or Map throws its TypeError. The comparison uses the reader of toEqual, ServedCollection. ServedSubsetSide gives the reader the names of the iterators that the scans of a Set and of a Map call. The scans are templates. Two Sets and two Maps compile to the code that they had. The scan of a Map leaves out the key of the entry that it tried first. The iterator of a Proxy can give one key in more than one entry. For a Proxy, the scan leaves out that key only if the key did not match. The table of built-ins for ahead-of-time builds tells what Bun.deepMatch does to a Proxy. It runs the iterators of the two operands. It calls `has` and `get` of the subset with the members and the keys that the value holds. Before, a compiled program read an old field of a member that `has` wrote. The tests of toEqual and of toMatchObject share one stand-in for reactive() of Vue.
|
Status: draft, waits for #47702, #47858 and #47888. Reproduced on a debug build of #47702 with #47858 and #47888 merged, with The question for a maintainer is in the PR body: compare what the Proxy serves, or keep "no verdict" for a Proxy. The branch: since #47942 it conflicts with main in one file, The update is one merge of main and of #47888, the entry written again for the new table, and a new run of the three ahead-of-time programs. It comes when #47702 and #47858 are on the new main, or sooner if a maintainer asks. CI: no test of this PR failed on build 137556 at 44cc6e5. 28 test files are red there: 26 under |
…e constructors by identity under the vitest import Two sizes that are undefined do not say that the second value has no other member, so each value must have the members of the other. Under the vitest import the constructors of the two values must be one value, as in Vitest. Jest also takes two native functions of one name. The row for expect.not.arrayContaining() against a served array is a test.todo with the answer of Jest and Vitest.
…) from another error "fails" is a failed toEqual() only. A TypeError from a Proxy prints as it is, so it cannot pass for a mismatch.
…t-map # Conflicts: # test/js/bun/test/expect.test.js # test/js/bun/test/vitest-flavor.test.ts
…h have the members of the other Two sizes that are undefined do not say that the expected value has no other member. The received members are walked again for each expected member. That walk asks has and get of neither value, and it takes back what it puts in the state.
…re on main The arms again take a Proxy for unequal to a Set or a Map. Bun__deepEquals keeps that pair for servedCollectionsDequal after the arm has answered. With the test inside the arms the compiler laid out the loop over the members in another way: toEqual of two Sets of 3 objects took 32 more instructions.
The pass compared the members with the two values swapped. It asked has and get of the first value, which threw for a first value that serves neither. A Proxy with no size that holds itself ended in a RangeError, and an equality tester got (expected, received). Two sizes that are undefined are equal sizes again, as before 304cabf. The test of the differences has the rows in which the answer is not that of Jest and Vitest.
…must each have the members of the other" This reverts commit 54fa1c3.
…n.deepMatch The rows that hold with one pass are back in the tests that also run on Jest and Vitest. The rows in which the answer is not that of Jest and Vitest are in the test of the differences.
Depends on #47702, #47858 and #47888. Their commits are in this diff.
Problem
expect({ s: reactive(new Set([1])) }).toMatchObject({ s: new Set([2]) })passes with Vue'sreactive(), a Proxy around the Set. Jest 30.5.2 and Vitest 4.1.12 fail it.subsetSpecialObjectMatches,src/jsc/bindings/bindings.cpp). An expected Proxy has no keys.Fix
subsetServedCollectionMatchesasks each Proxy what script asks:Symbol.iterator,size,has,getand its members, through the reader of bun:test: toEqual reads a Proxy around a Set or a Map through the Proxy #47888.Bun.deepMatchsays thathasandgetof the subset get members ofvalue.expect.test.js(16 tests fail without it, 10 pass on Jest and Vitest),deep-match.spec.ts,vitest-flavor.test.ts. Two self-reviews: 10 concerns addressed, then one walk removed (Notes).Background
reactive(member), whosegettrap unwraps aref.Downsides
new Proxy(new Set(), {})throws a TypeError, as in Jest and Vitest.Notes
No issue reports this. It rests on measurement against Jest and Vitest, as #47888 does.
The rule (measured against
expect30.5.2 and@vitest/expect4.1.12 on Node 26.3.0, with@vue/reactivity3.5.44)Symbol.iteratorfirst. A Proxy that serves none has no rule here, and the key walk decides, as before. Thensizeof both sides is compared with===, before an iterator is opened.undefinedare the same size, so such a pair takes this rule too. Jest and Vitest walk such a pair in step. bun:test: toEqual reads a Proxy around a Set or a Map through the Proxy #47888 names that walk under what is left out: no library that I found serves a Proxy with nosize.hasmatches, for a Map if the value undergetmatches. Each other member must match an expected member as a subset.size, the iterator,hasorgetthrows is thrown. A walk that is left early gets itsreturn(). A revoked Proxy keeps its TypeError.Rows (232 recorded rows on which Jest and Vitest agree. "Base" is #47702 with #47858 and #47888 merged.)
toStrictEqualrows. bun:test: toEqual reads a Proxy around a Set or a Map through the Proxy #47888 leavestoStrictEqualas on main.{}that serves a Set or a Map interface, and a Proxy around a Set whoseSymbol.toStringTagsays Map. These are right on the base by accident and wrong here.expect.objectContaining, or in anexpect.arrayContainingelement, inside a Proxy collection pass: 24 of 24. Without bun:test: toEqual reads a Proxy around a Set or a Map through the Proxy #47888 beneath this PR, 20 of them fail.Where the answer is not that of Jest and Vitest (pinned in the test "where toMatchObject() ... is not that of Jest 30.5.2 and Vitest 4.1.12")
sizethat isundefined: 5 rows. The references walk such a pair in step. Here a member that only the expected value has is not seen, an expected value with nohasthrows, and such a Proxy does not match a real Set.Ahead-of-time builds
valuetohasandgetof the Proxy. The old entry did not say so, andbun build --compile --aotgave another result thanbun:[1,true,1,null]against[1,true,2,"added"]for a program whosehaswrites to the member it gets.valueas kept in native memory, which is how the table writes a value that is handed to code.test/aot/parallel/builtins/deep-match-hands-*are three such programs. They print the same both ways with the entry and differ without it.hasandgetas subset, an iterable class as value). Two literals that hold a Set do not grow.expect().toMatchObjectneeds no entry: its generated entry allows any code to run.Cost (release, linux x64,
sizeandperf_event_openuser instructions, JIT off, 5,000 calls minus an empty loop, median of 5, at 44cc6e5. Base as above, with #47888 at 8d7af31.)toMatchObject, 4 key object, 2 key patterntoMatchObject, 3 levels and an arraytoMatchObject,[1,2,3]against[1,2,3]toMatchObject, object with 100 number propertiestoMatchObject, array of 10 class instancesBun.deepMatch, 3 levels and an arrayexpect.objectContaining, 4 key objecttoMatchObject, Set of 5 numberstoMatchObject, Set of 1,000 numberstoMatchObject, Map of 3 entries with object valuestoMatchObject, Map of 100 entries with object valuestoMatchObject, Map of 1,000 string keysBun.deepMatch, Map of 1,000 string keystoEqual, 3 levelssubsetSpecialObjectMatches, which this PR changes, and its loop over the entries is 3 instructions longer there. Two real Sets do not change.Tests
describe("toMatchObject() with a Proxy around a Set or a Map")intest/js/bun/test/expect.test.js: 10 tests that also pass on Jest and Vitest, with a hand-written stand-in forreactive(). 6 more tests underif (isBun): what a Proxy is asked and when, what it throws, a member that stays alive during a collection,has is not a function, the rows that differ from the references, and the exception checks in a subprocess.expect.test.js16 fail,deep-match.spec.ts13 fail,vitest-flavor.test.ts1 fail.sizethat is no number (undefined,"1",1n,null) on which Jest and Vitest agree: the base differs on 70, this PR on 18. 16 are passes that the references fail. 2 are right on the base and wrong here: a Proxy with nosizeagainst a real Set with the same members.BUN_JSC_validateExceptionChecks=1: the 79 tests ofexpect.test.jsthat name a Proxy ortoMatchObjectpass, and so doesdeep-match.spec.ts. No abort.bun-snapshots.test.ts,vitest-apis.test.ts,jest-extended.test.js,isArray-proxy-crash.test.ts,test/js/node/assert/deep-equal.test.ts.describe("expect()"), so both describes use one copy.Between the PRs of this stack
test.todoforexpect.not.arrayContainingagainst a served array, which needs bun:test: read a Proxy of an array through the Proxy in toMatchObject, Bun.deepMatch and expect.arrayContaining #47858. On this branch it is a plain test.size. 79634ea takes it out, as 8d7af31 does in bun:test: toEqual reads a Proxy around a Set or a Map through the Proxy #47888: a self-review found that no library serves such a pair and that the walk was not the rule of Jest and Vitest.no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/regression/issue/isArray-proxy-crash.test.ts