A couple of days ago we had an interesting case of our CI constantly failing on one particular branch pipeline. The code change was trivial. So trivial that we eventually replaced it with an empty commit. The pipeline was still failing.
We useember-exam to shuffle the test order with pseudorandom seed. The idea is:
branch_name=`git rev-parse--abbrev-ref HEAD`ember exam--random=${branch_name}
If you never encountered this, very good reasons for doing so are:
- Tests running in always the same order willprobably not reveal potential race conditions.
- Tests running in always the same order willprobably not reveal possible leaking state between test runs.
- By using seed generated from a branch name, we canprobably replicate problematic scenarios locally.
Why did I emphasiseprobably so much? Because as one greatDilbert comics says:
👨🏼💻 Are you sure that's random?
😈 That's the problem with randomness, you can never be sure.
-Scott Adams-
Situations like ours might signal that there isregistered test waiter that does not get cleaned up after thecomponent lifecycle ends.
This is hard to debug because the leak can originate inany tests that preceded the one that timed out. So how to find the offending code?
We had luck with usingChrome -> Dev Tools -> Performance
tool. We simply let the tests run till they started timing out. At that point, we hit "record" for few seconds and tried to dig through the results. Few moments going through the stack tree and there is a hint pointing atember-test-waiters.
So we dig into thewaiter-manager::hasPendingWaiters
code and found more details aboutwhich waiter, that was not expected, we were waiting for. After that, a careful examination of the relevant component revealed case when the waiter might not have been unregistered.
I like this technique because it records history and allows you to quickly jump between the things that already happened without worrying too much that you will miss something important. It's a great first approximation.
Code example
From the perf tool, one can simply jump to the relevant code line, put a breakpoint and check what is the code complaining about. The code we found could be simplified to:
exportdefaultComponent.extend({actions:{buttonClicked(){this.animate();}}animate(){waitForPromise(newPromise((resolve)=>{AnimationLibrary.animate({onComplete:()=>{resolve();}});});}});
There might be multiple problems with this. But for simplicity let's say that one test callsclick('button')
withoutawait
and immediately after that the test ends. There is a chance that component gets torn down whileAnimationLibrary.animate
is running. But since there is no DOM to interact with anymore, it will never callonComplete()
. Thus leaving the next test hanging with a waiter that will never resolve.
Or there might be other situations happening. Does not matter. The important thing is that you make sure you cleaned up after yourself when your component lifecycle ends:
exportdefaultComponent.extend({// Register fake cancel function_cancel(){},actions:{buttonClicked(){this.animate();}}animate(){waitForPromise(newPromise((resolve)=>{// Override _cancel functionthis._cancel=resolve;AnimationLibrary.animate({onComplete:()=>{resolve();}});});}willDestroyElement(){// Make sure that the promise is resolvedthis._cancel();}});
Async is hard.
-every developer ever-
Top comments(0)
For further actions, you may consider blocking this person and/orreporting abuse