0

I've had my share of obscure error messages in Ember tests with no useful stack trace or any other information to figure out the underlying cause, but this case takes the prize for being the most obscure of them all.

I have this Ember app where tests are failing in a very bizarre way:

$ ember test -m "Acceptance | main report"                                                                                                                                                                1 ↵
cleaning up...
Built project successfully. Stored in "/home/username/work/my-project/frontend/tmp/core_object-tests_dist-HinFNKHW.tmp".
ok 1 PhantomJS 2.1 - Acceptance | main report: visiting main home
not ok 2 PhantomJS 2.1 - Acceptance | main report: it changes structure
    ---
        actual: >
            false
        expected: >
            true
        stack: >
            exception@http://localhost:7357/!/assets/test-support.js:7199:49
            adapterDispatch@http://localhost:7357/!/assets/js/vendor.js:54101:22
            dispatchError@http://localhost:7357/!/assets/js/vendor.js:32467:23
            onerrorDefault@http://localhost:7357/!/assets/js/vendor.js:46036:32
            trigger@http://localhost:7357/!/assets/js/vendor.js:73035:19
            http://localhost:7357/!/assets/js/vendor.js:73935:28
            invoke@http://localhost:7357/!/assets/js/vendor.js:15192:18
            flush@http://localhost:7357/!/assets/js/vendor.js:15260:15
            flush@http://localhost:7357/!/assets/js/vendor.js:15384:20
            end@http://localhost:7357/!/assets/js/vendor.js:15454:28
            run@http://localhost:7357/!/assets/js/vendor.js:15577:19
            http://localhost:7357/!/assets/js/vendor.js:15873:29
        message: >
            Error: Assertion Failed: Cannot call writableChainWatchers after the object is destroyed.
        Log: |
    ...

1..2
# tests 2
# pass  1
# skip  0
# fail  1
Not all tests passed.
Error: Not all tests passed.
    at EventEmitter.App.getExitCode (/home/username/work/my-project/frontend/node_modules/testem/lib/app.js:434:15)
    at EventEmitter.App.exit (/home/username/work/my-project/frontend/node_modules/testem/lib/app.js:189:23)
    at /home/username/work/my-project/frontend/node_modules/testem/lib/app.js:103:14
    at tryCatcher (/home/username/work/my-project/frontend/node_modules/testem/node_modules/bluebird/js/release/util.js:16:23)
    at Promise._settlePromiseFromHandler (/home/username/work/my-project/frontend/node_modules/testem/node_modules/bluebird/js/release/promise.js:510:31)
    at Promise._settlePromise (/home/username/work/my-project/frontend/node_modules/testem/node_modules/bluebird/js/release/promise.js:567:18)
    at Promise._settlePromise0 (/home/username/work/my-project/frontend/node_modules/testem/node_modules/bluebird/js/release/promise.js:612:10)
    at Promise._settlePromises (/home/username/work/my-project/frontend/node_modules/testem/node_modules/bluebird/js/release/promise.js:691:18)
    at Async._drainQueue (/home/username/work/my-project/frontend/node_modules/testem/node_modules/bluebird/js/release/async.js:133:16)
    at Async._drainQueues (/home/username/work/my-project/frontend/node_modules/testem/node_modules/bluebird/js/release/async.js:143:10)
    at Immediate.Async.drainQueues [as _onImmediate] (/home/username/work/my-project/frontend/node_modules/testem/node_modules/bluebird/js/release/async.js:17:14)
    at processImmediate [as _immediateCallback] (timers.js:383:17)

Note the error message:

Cannot call writableChainWatchers after the object is destroyed.

My first question is, how can I figure out what's the cause of this weird error, when the stack trace I get is anything but helpful. Not only is it unhelpful to get these insane line numbers of the built vendor.js, but also nothing in the stack trace comes from my app itself. So no line number that could help me here.

Also, this error situation does not occur when running the app in dev or production environment. It's just happening in the test environment.

And finally, and perhaps more important: I realized that the cause of the error has something to do with state leaking from one test to another. In that test file you can see there are two tests. The first one passes, but the second one fails. When I commented the first one, to focus in the only failing test, it turns out it does not fail. When I swap the order of these two tests in the file, then the one that was failing, that used to be second in order, now that it is the first one it does not fail. And conversely, the one that was passing when it was first, it's now failing when it comes last.

Update

I have more information about the conditions under which this error occurs.

I pinpointed the line of code in my app that raises it in the tests. It looks like this:

Ember.run(() => this.set('value', value));

This line of code is inside a compute method in a Ember.Helper my app uses. This helper has an observer that calls this.recompute() when some properties in a service change its values. Much like is shown here.

The actual code raising this error is not the this.set call. I removed it and the code still occurs. It's the mere invocation of Ember.run the culprit. However, when I invoked this.set directly without wrapping it in Ember.run, the error also occurs. To be fully clear, any of the following three lines of code raise the same error:

Ember.run(() => this.set('value', value));
Ember.run(() => {});
this.set('value', value);

The only way I have right now to fix it is to wrap that line within a try/catch with an empty catch block, silently ignoring the error.

I also tracked down the line of code in ember.js that is raising this error. You can take a look at it here. I still hope someone will be able to figure this out.

Ernesto
  • 3,837
  • 6
  • 35
  • 56

1 Answers1

3

This is likely due to part of your app doing something asynchronously, then then the page being rerouted before it is finished. The classic example would be to have a component that loads a record and sets a property on itself when that is done. Since network calls are often quick, you usually won't see this on production/local. However, if you are fast enough to click a link to a different page, your ember app will complain that the component was destroyed before it could be modified (the callback is hit after the component is destroyed during the component lifecycle). The reason this appears in tests more frequently is the app generally transitions very quickly between pages, and it's fast enough to create a common race condition.

The best tool to fix this is the addon "ember-concurrency". Your alternative is to check whether the component is destroyed before you set values on it, but this is considered to be an anti-pattern. It can be tricky to track down what is causing the specific tests failures, but generally putting debugger statements in callbacks can help; if the debugger is hit during the wrong test, you have your culprit.

Ember concurrency

Useful post explaining why ember concurrency exists

AlexMA
  • 9,842
  • 7
  • 42
  • 64
  • Thanks AlexMA. I will dig more into this add-on you mention. I am aware of this problems you mentioned in the first paragraph, however in this case is not quite the same for two reasons: first, when it is about setting values in a destroyed object, the error would be something like "yo called 'set' on a destroyed object". But this "writableChainWatchers" thing makes it more weird and low level. And second, no matter how fast things go, it is unacceptable that some state leaks from one test to the next. If a test passes when run in isolation, it should pass when run in a suite. – Ernesto Mar 09 '17 at 03:17
  • I added more relevant information above, in case is useful to diagnose this situation. – Ernesto Mar 09 '17 at 20:39
  • What if you try wrapping the code in `if (!this.isDestroyed) {...}`? Or try wrapping the `recompute` call in that? At this point I am a bit out of my depth, and would turn on "pause on caught exceptions" in my dev console or try to find some help in the Ember slack channel. – AlexMA Mar 09 '17 at 20:52
  • I found out the point in the code causing this precisely by turning on "pause on caught exception", and no, checking for `this.isDestroyed` does not help in this case. Anyway, thanks for your help and for pointing me out to `ember-concurrency`. At this point I temporarily fixed the issue by swallowing the exception silently, but my next step is creating a ticket to bullet-proof all my concurrency with `ember-concurrency`. – Ernesto Mar 10 '17 at 01:42