-
-
Notifications
You must be signed in to change notification settings - Fork 6.6k
Description
🐛 Bug Report
Calling require.main can go into an infinite loop in certain situations, causing tests to hang forever. I'd be happy to PR a fix, but there are at least 2 different ways to approach this and I don't know which one is better from an overall design point of view.
To Reproduce
Steps to reproduce the behavior:
- I've added an
e2e/test that reproduces the behaviour in my clone (linked below). tl;dr the setup is the following:
modulerequiresmockmockrequiresmodule, causing a cyclic import.jest.mock('./mock')- Call
require.mainfrom withinmodule.
The last step causes the require.main property getter to go into an infinite loop because of a weird interaction between require.parent and moduleRegistry.
Expected behavior
I would expect the test to either fail with a clear error message ("import loop detected" or similar), or succeed.
Link to repl or repo (highly encouraged)
e2e test that reproduces the behavior. Running the test causes an infinite loop.
I root caused this to a weird interaction between require.parent and moduleRegistry when mocking a module. It's a bit tricky, but the situation is roughly as follows:
- When
mockis first loaded, it goes throughRuntime._generateMocksince I'm mocking it. Runtime._generateMockclears the_moduleRegistryand_mockRegistryinstance variables for the duration of the mock generation.- At this point,
require.parent, which is defined as follows:
Object.defineProperty(localModule, 'parent', {
enumerable: true,
get() {
const key = from || '';
return moduleRegistry.get(key) || null;
},
});
returns null for mock.ts, since moduleRegistry is empty. However, from === module.ts, which is relevant at a later point.
4. Since mock requires module, and moduleRegistry is empty at this point, jest starts loading module again, and sets moduleRegistry['module'] accordingly.
5. When module tries to call require.main, we now have a cyclic dependency, and the while loop that determines main goes into an infinite loop. This is because moduleRegistry.get(key) above is now set, since we've loaded module.ts and populated it in the moduleRegistry.
I'm attaching a diagram where I've tried to explain this visually.
Here are some ways I see to fix this:
- Keep a
Setof visitedmoduleIds when computingrequire.main, and throw if we detect a loop. - Set
require.parentdirectly instead of using a getter. This will ensure that it always has the same value and doesn't change when the underlyingmoduleRegistrychanges.
I tried both of these and they both avoid the infinite loops. 2 is probably nicer since it leads to a successful test, but I don't know if it could have adverse side effects. I saw that the original PR introducing module.parent has a discussion about the getter.
envinfo
I don't think the issue is env-specific since I managed to root-cause it to specific code, but posting for completeness:
System:
OS: macOS 10.15.6
Binaries:
Node: 10.15.3 - ~/.nvm/versions/node/v10.15.3/bin/node
Yarn: 1.22.5 - /usr/local/bin/yarn
npm: 6.10.3 - ~/.nvm/versions/node/v10.15.3/bin/npm
npmPackages:
jest: ^25.5.4 => 25.5.4
