jest icon indicating copy to clipboard operation
jest copied to clipboard

Circular references hang jest when assertions fail on node 14

Open voces opened this issue 5 years ago β€’ 52 comments

πŸ› Bug Report

When an assertion fails where either the expected or actual value is circular, and both values are objects, jest encounters an error stating it failed to convert a circular structure to JSON, resulting in the test run not completing.

To Reproduce

it("test", () => {
  const foo = {};
  foo.ref = foo;

  expect(foo).toEqual({});
});

Running jest gives me the following error:

(node:11685) UnhandledPromiseRejectionWarning: TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    --- property 'ref' closes the circle
    at stringify (<anonymous>)
    at writeChannelMessage (internal/child_process/serialization.js:117:20)
    at process.target._send (internal/child_process.js:804:17)
    at process.target.send (internal/child_process.js:702:19)
    at reportSuccess (/Users/verit/basic-jsx/node_modules/jest-worker/build/workers/processChild.js:67:11)

Jest continues running indefinitely (I only tested up to ten minutes) and reports nothing regarding the test suite.

I traced this to the added failureDetails property on error messages, landed in 26.3.0.

Expected behavior

I'd expect the test to fail and jest to complete running.

envinfo

I only tested two versions. The above error occurs on 14.9.0, but does not on 12.16.1.

  System:
    OS: macOS 10.15.6
    CPU: (8) x64 Intel(R) Core(TM) i7-7920HQ CPU @ 3.10GHz
  Binaries:
    Node: 14.9.0 - ~/.nodenv/versions/14.9.0/bin/node
    npm: 6.14.8 - ~/.nodenv/versions/14.9.0/bin/npm
  npmPackages:
    jest: ^26.4.2 => 26.4.2 

voces avatar Oct 04 '20 00:10 voces

Is there any workaround?

Lonli-Lokli avatar Nov 10 '20 12:11 Lonli-Lokli

@Lonli-Lokli Looks like the only workaround for now is --detectOpenHandles

However, this causes a massive decrease in performance.

I hope this can be fixed soon. I have migrated several of our projects to jest... and now it causes hiccups all throughout our build system when one test breaks.

It would be nice if --testTimeout worked in this scenario... but it still allows the test to just hang until Jenkins or circle ci times out.

joelcoxokc avatar Nov 11 '20 21:11 joelcoxokc

I can reproduce this when I have two such tests (in separate files - but not sure if this matters) and I have to run it with --watch. Here, however, it can be reproduced even without --watch: https://repl.it/@Frantiekiaik/jest-playground-1

ziacik avatar Nov 21 '20 17:11 ziacik

I just ran into what I assume is the same issue on Node 10.16.0 and Jest 26.6.2. I can't reproduce it as written, but if I make a file containing two copies of @voces's example test and run it in watch mode, I get the same error.

The test:

it('test', () => {
  const foo = {};
  foo.ref = foo;

  expect(foo).toEqual({});
});
it('test 2', () => {
  const foo = {};
  foo.ref = foo;

  expect(foo).toEqual({});
});

The error:

(node:58951) ExperimentalWarning: The fs.promises API is experimental
(node:58951) UnhandledPromiseRejectionWarning: TypeError: Converting circular structure to JSON
    at JSON.stringify (<anonymous>)
    at process.target._send (internal/child_process.js:735:23)
    at process.target.send (internal/child_process.js:634:19)
    at reportSuccess (/Users/richardmunroe/analytics_ui/node_modules/jest-worker/build/workers/processChild.js:67:11)
(node:58951) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:58951) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Running with --detectOpenHandles makes the problem go away for me too.

rimunroe avatar Nov 23 '20 19:11 rimunroe

This is happening since #9496 Commit 918636142791a3dd4ddfe9367149a90437bd6da9

And it seems to happen because of some inter-process serialization. After adding failureDetails property, the serialization fails on cyclic references as it contains the objects being under test. Maybe some sanitization of the failureDetails property would help.

ziacik avatar Nov 25 '20 22:11 ziacik

Note that after commit 5f6f2ec8e17555b695d65ab68824926c77730216 which changes default runner to circus, the error message is

"messageParent" can only be used inside a worker

      at messageParent (packages/jest-worker/build/workers/messageParent.js:46:11)

with JEST_JASMINE=1 the error is as before.

ziacik avatar Nov 27 '20 18:11 ziacik

Added a PR as an attempt to fix this.

Also, please note that the messageParent error mentioned above is due to this line which swallows the real error message which is also about circular references.

ziacik avatar Nov 27 '20 21:11 ziacik

Added a PR as an attempt to fix this.

πŸŽ‰

Also, please note that the messageParent error mentioned above is due to this line which swallows the real error message which is also about circular references.

We should not swallow errors like that...

SimenB avatar Nov 28 '20 09:11 SimenB

We confirm it happens in Node 12, and it’s more common when using Angular Dependency Injection (I think they have cyclic structures in some error-states).

The process hangs in such scenario, but this can be improved slightly be applying --unhandled-rejection=strict to nodejs script, instead running jest as separate binary. It helps jest to recover and fail suite (but it does not resolve cyclic reference of course).

piotrl avatar Dec 01 '20 08:12 piotrl

I can confirm this problem as very common with Angular DI with Node 12 as @piotrl mentioned. Using Jest 26.6.3

How can I run it with --unhandled-rejection=strict flag?

    //package.json > scripts
    "test:unit": "node --unhandled-rejections=strict $(npm bin)/jest --env=jest-environment-jsdom-sixteen ",

this is actually stopping the execution, is that the right workaround?

daton89 avatar Dec 28 '20 10:12 daton89

@SimenB from angular issue "jest": "26.6.3", "jest-canvas-mock": "2.3.0", "jest-preset-angular": "8.3.2", Now, the correct error is displayed in the console. However, the test runner process still does not stop. i.e. Process does not end when error is found.

Lonli-Lokli avatar Jan 15 '21 07:01 Lonli-Lokli

@Lonli-Lokli I have the exact same versions as you, still getting meaningless errors:

(node:6657) UnhandledPromiseRejectionWarning: TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    |     property 'element' -> object with constructor 'Object'
    |     property 'componentProvider' -> object with constructor 'Object'
    --- property 'parent' closes the circle
    at stringify (<anonymous>)
    at writeChannelMessage (internal/child_process/serialization.js:117:20)
    at process.target._send (internal/child_process.js:805:17)
    at process.target.send (internal/child_process.js:703:19)
    at reportSuccess (/builds/smallstack/products/cloud/node_modules/jest-runner/node_modules/jest-worker/build/workers/processChild.js:67:11)
(Use `node --trace-warnings ...` to show where the warning was created)

The error should have been a NullInjectorError: NullInjectorError: No provider for CloudApiClient

maxfriedmann avatar Jan 23 '21 07:01 maxfriedmann

I have debugged a bit why this happens, and it happens because Angular TestBed configure its compiler to add some additional meta-info to the errors. That configuration is getted from the same file, calling to initServicesIfNeeded function from some place into TestBed.

I don't know if it should be fixed by jest or Angular (maybe implementing a toJSON method to its errors, to avoid to include this meta-properties to serialization). But it's a problem with jest-workers and Angular TestBed errors

tonivj5 avatar Jan 29 '21 11:01 tonivj5

Why not just use https://github.com/WebReflection/circular-json or https://www.npmjs.com/package/json-stringify-safe or similar libraries to be safe with Object JSON stringify?

CSchulz avatar Feb 18 '21 14:02 CSchulz

The fix for this will be reverted (#11172) as it caused other serialization issues. Might need to use a custom json serializer, yeah

SimenB avatar Mar 08 '21 12:03 SimenB

What is the recommended way of handling this?

robertleeplummerjr avatar Apr 12 '21 14:04 robertleeplummerjr

Personally, I recommend rolling back the jest-jasmine2 (or jest-circus) version to one that isn't broken. I haven't tried it for a while yet, so there might be mysterious failures if you use try to use features introduced by the newer jest versions; you can do that if you don't directly depend on jest-jasmine2 by adding this to your package.json:

    "resolutions": {
        "jest-jasmine2": "26.0.1"
    }

samsieber avatar Apr 29 '21 20:04 samsieber

In our case this ended up being an uncaught exception. We eventually were able to log out the exception, and fix the unit test. Then it worked fine. Just FYI if anyone else runs into this. Scary stuff.

robertleeplummerjr avatar Apr 29 '21 20:04 robertleeplummerjr

For Angular users this might be the fix/workaround:

  1. Error reads something like

TypeError: Converting circular structure to JSON --> starting at object with constructor 'Object' | property 'element' -> object with constructor 'Object' | property 'componentProvider' -> object with constructor 'Object' --- property 'parent' closes the circle at stringify ()

  1. Run npm run test:detectOpenHandles or the equivalent jest --detectOpenHandles
  2. Error now reads

Found the synthetic listener @transform.start. Please include either "BrowserAnimationsModule" or "NoopAnimationsModule" in your application.

OR

Found the synthetic property @transitionMessages. Please include[...]

  1. To fix, import BrowserAnimationsModule or NoopAnimationsModule in your test

I don't know what the above errors are caused by though. Hopefully this helps.

EDIT: Formatting

BingeCode avatar Sep 20 '21 09:09 BingeCode

I was creating a repro case for a similar issue (see #11958) and I noticed that the issue of the jest runner hanging only appears to affect jest-circus on Node 12.x and 14.x.

If you look at this GitHub actions run: https://github.com/blimmer/jest-issue-repro/actions/runs/1339553822 you'll see that 12.x and 14.x get killed by a timeout, while 16.x completes normally.

So, for those who are able to upgrade to Node 16.x, that's another possible workaround to this issue.

blimmer avatar Oct 13 '21 23:10 blimmer

I've faced this issue recently in a React-Native project. The was due to declaring ref on a TouchableOpacity element. Which created a huge snapshot I guess, and the expectSnapshot failed. I removed from it and used on a wrapper view to get around this error. Btw, do not forget to use collapsable on the view, else it will cause App crash. Screen Shot 2021-11-16 at 1 54 05 PM

sparachi avatar Nov 16 '21 18:11 sparachi

same here, any solution ?

node: v14.17.6 npm: 6.14.15 jest: 24.8.0

gabsmprocha avatar Feb 11 '22 19:02 gabsmprocha

@SimenB I know you might not have time for this work but do you still remember about this issue? ;)

Lonli-Lokli avatar Feb 11 '22 19:02 Lonli-Lokli

@SimenB very tentative looking back into this (I just can't help myself sometimes πŸ™ˆ) and can't comment on my PR as it's now locked.

You outlined what you think the high level solution for this should be here: https://github.com/facebook/jest/pull/11467#issuecomment-1040878924

I'm wondering if you had any thoughts on how to do that? From what I remember when I the failureDetails property it's structure was very ad-hoc (starting with "a bunch of arrays or a single error", iirc) so I'm concerned about how this could actually be solved robustly.

It could be that I'm misremembering or that ultimately the types boil down to a stable union of complex but well defined types. (I want to say the type of that property is unknown but I'm on mobile so can't confirm - if it is I'd say a good first step is probably to get that property's type completely defined)

G-Rath avatar May 29 '22 04:05 G-Rath

cc @segrey I imagine you're probably pretty familiar with the different values the failureDetails property can be expected to hold - since it's likely that fixing this will result in less details, it would be good if you could detail what information you're actually using in WebStorm so we can keep them.

G-Rath avatar May 29 '22 04:05 G-Rath

I think we can fix this by patching the jest-worker, when detecting a circular error, just fail the case.

diff --git a/node_modules/jest-worker/build/workers/processChild.js b/node_modules/jest-worker/build/workers/processChild.js
index a4f5acb..4c2a018 100644
--- a/node_modules/jest-worker/build/workers/processChild.js
+++ b/node_modules/jest-worker/build/workers/processChild.js
@@ -64,7 +64,13 @@ function reportSuccess(result) {
     throw new Error('Child can only be used on a forked process');
   }
 
-  process.send([_types().PARENT_MESSAGE_OK, result]);
+  try {
+    process.send([_types().PARENT_MESSAGE_OK, result]);
+  } catch (e) {
+    if (e.message.indexOf("circular") > -1) {
+      reportClientError(e);
+    }
+  }
 }
 
 function reportClientError(error) {

maple-leaf avatar Jun 29 '22 08:06 maple-leaf

Same issue on node 18

vitalyster avatar Jul 08 '22 00:07 vitalyster

@SimenB I've dug into this and confirmed that if I comment out this line the issue goes away, and also that this is because test.errors holds both Jest errors and user errors.

So to ensure that that is JSON serializable I think we've got three options:

  1. Only hold Jest-based errors (e.g. AssertionResult), which internally would be ensured are JSON serializable
    • From what I can tell AssertionResult is only represented as a type so we'd have to come up with a stable way to check if an error is of that type (which could be as simple as matcherResult in error, but it'd have to be stable)
    • I don't know if there are other Jest errors that end up here that we'd also want to preserve
    • This would mean that reporters could not have any special handling on non-jest errors (e.g. pretty handling of ESLint errors)
  2. Use a library like flatted to serialize the content
    • My understanding is there is not a single standard for serializing circular json, so this would mean consumers would be required to use whatever Jest decides to use, which could be an annoying overhead
    • This'd mean any tools currently using this property would require updating since they'll currently be expecting the content as a standard object rather than a string that needs to be parsed by an external library
  3. Require downstream users to ensure their errors are serializable as JSON
    • This would have huge overhead and based on #11958 there might even be conflicts in Jest that makes it impossible for users to do this

I think 1. is probably the option to go with at least for - afaik JetBrains are actually the only people using this property and they're definitely only interested in the AssertionResult errors from Jest; I think that should also be easier to extend afterwards if someone did want to preserve user errors e.g. we could implement a config option for providing a handler to do the serialization and only apply that to non AssertionResult errors.

G-Rath avatar Jul 16 '22 21:07 G-Rath

After digging further into this I think I've come to what you were outlining here @SimenB, which is that AssertionError is not being properly serialized.

The bigger problem with this is that it's the actual and expected properties that are not being properly serialized which feels to me like it almost puts us back to where we started with #11467 just higher up πŸ€” I've not looked into how reporters actually fit into Jest, but I'm wondering if maybe the best solution here would be to accept a parse/stringify pair that gets called here + before the final data gets passed to whatever reporters are being used - that way people can opt-in to handling this how they want and hopefully we don't have to do a breaking change for all existing reporters...

That'd also fix the issue for both AssertionError & external errors. @SimenB what do you think?

G-Rath avatar Jul 16 '22 22:07 G-Rath

I get this issue with mocking await functions in many tests in different describes in the same file which all fail when I have a mockReturnValueOnce in one describe, but doesn't appear with mockReturnValue in the describe block, it's as if the mocks are shared between describe blocks because the return value from an upper describe was being returned in my other describe block mock instead of its own return value...

node:internal/child_process/serialization:127
    const string = JSONStringify(message) + '\n';
                   ^

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    |     property '_httpMessage' -> object with constructor 'Object'
    --- property 'socket' closes the circle
    at stringify (<anonymous>)
    at writeChannelMessage (node:internal/child_process/serialization:127:20)
    at process.target._send (node:internal/child_process:819:17)
    at process.target.send (node:internal/child_process:719:19)
    at reportSuccess (C:\node-projects\telus\tps\cdo-tps-quote-mgmt-api\node_modules\jest-worker\build\workers\processChild.js:59:11)

If I do jest --detectOpenHandles or jest --runInBand the errors don't appear, and I don't have a bunch of tests fail anymore.

KrisBNelson avatar Sep 29 '22 16:09 KrisBNelson