Repository navigation
Conversation
…odule export mock.module() of a loaded ES module and spyOn() of a namespace export store their values with JSModuleNamespaceObject::layExportOverride() and end with removeExportOverride(). The engine keeps the owners of each export variable and what each of them found there. So two mocks of one variable (`export * from`), or a mock and a spy, end in any order: an owner that ends under a newer one hands over what it found, and stores nothing. patchOrder and the newest-first pass at the end of a test file go. A preload's mock that a test file displaces is set aside, and comes back where it stood.
Collaborator
Author
|
Updated 9:55 AM PT - Nov 11th, 2026
⏳ @robobun, your commit bc8b142 is still building in
|
Collaborator
Author
|
Status Reproduced with five modules. Fix: #48621, with the engine half in oven-sh/WebKit#1033. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
export * from). If the older mock ends first, a mock value stays: the next test file reads"mock of inner"where it expects"real".restoreOverwrittenExports,src/jsc/bindings/BunPlugin.cpp:3579). The copy of the newer mock is the value of the older one. A namespacespyOn(JSMockFunction.cpp:1002) andbun test --aotdo the same.Fix
mock.module()and a namespacespyOnstore as owners,unmockModuleandmockRestoreend by owner. A displaced preload's mock comes back in place when the file ends.test/js/bun/test/mock/mock-module.test.ts(19 new tests, main fails 16), the other mock and spy suites,test/aot.Background
--aotstay wrong.Downsides
spyOnplusmockRestorecosts 5,683, from 4,813..textgrows by 14,080 bytes.bun test --aotstays wrong for a builtin module behind a compiled barrel.Notes
Pin.
WEBKIT_VERSIONpoints at the preview build of oven-sh/WebKit#1033. It moves to the merged commit before this PR merges. That WebKit branch starts at the commit that main pins.Repro.
variable.jsexportsa.inner.jsandouter.jsexport it again withexport *. A test mocks./inner.js, then./outer.js, then callsvi.doUnmock("./inner.js"). The next test file readsa.doUnmock(inner)mock of outerrealmock of innermock of outermock of outerrealSame cause, fixed here.
vi.doMock()or a secondmock.module()over the older patch.usinghandle of a mock of a builtin module under the mock of a barrel.node:pathstayed mocked in the next file.{ spy: true }mocks of two barrels of one class.spyOn(namespace)and a module mock of one export, in either order. A spy that stands over a mock calls the real function once the mock ends.export * as. The variable kept a mock value after the file.bun test --aot: the mocks of two compiled files that export one variable, and a spy over one.The behaviour change.
spyOn(ns, "a"), thenmock.module()of a module that exportsa, thenspy.mockRestore(): the export keeps the value of the mock until the mock ends. Before, the restore stored the real function and the standing mock showed nothing.docs/test/mocks.mdxsays thatmock.restore()doesn't remove module mocks.The end of a test file keeps its newest-first order (
patchOrder). The order no longer decides the values. It is the cheap order: the owner that stored last ends without a walk.Left for later.
Bun.plugin.clearAll()drops every mock and puts nothing back.vm.SyntheticModulesetExportstays a store with no owner.vi.unstubAllGlobals(), put back what a restored spy replaced, not the spy #48249).vi.importActual()can read the value of another mock as the original while two mocks stand.bun test --aot: a builtin module behind a compiled barrel, and a preload's mock that returns in place.test/aot/runner-suites.known.txtlists both.Earlier work. #48121 (a base plus owner layers per property) and #35669 (an undo record keyed on the leaf binding) keep the record in Bun, above the store. This PR keeps it where every store meets.
Checks.
test/js/bun/test/mock/mock-module.test.ts: 332 pass, 1 todo on a debug build with ASAN.mock-hoist,mock-module-non-string,mock-module-resolve-log,mock-fn,mock-disposable,spyMatchers,vi-utils,vitest-apis,vitest-flavor,jest-extended: pass.BUN_JSC_validateExceptionChecks=1on the repros: clean.test/aot: thefixed-runs,mock-*andspy-*files pass on a release build.Numbers. Release builds with the same flags, base against this change, each with its side of WebKit. They are from before the rebase onto #48478 (Bun 89452d2, WebKit 873ab3a4). Instructions are user-mode counts (
perf_event_open), the minimum of 3 runs. Two runs of the base differ by up to 0.7%.sizeofAbstractModuleRecord/JSModuleRecord/JSModuleNamespaceObject/VM/Zig::GlobalObjectsizeof(JSModuleMock).textresolveExportcallsvi.doUnmock()vm.SyntheticModulesetExport, per callspyOnplusmockRestoreof a plain object property (no JIT)spyOnplusmockRestoreof a namespace export (no JIT)