Wednesday, May 15, 2013

One Main() for Great Good of Dart Tests

‹prev | My Chain | next›

Thanks largely to Damon Douglas, my #pairwithme pair, I have the test suite for the Dart version of the ICE Code Editor in much better shape.

When we started last night all the tests passed, but there was lots of red and other weird error-like output when I ran the test suite:



When we finished, all of that was gone. Well, almost all of it:



It was only one red line, but we gave it a good try to eliminate that one red line. Unfortunately, it was late and everything we did only made things worse. Compounding the problems was that the single red line often grew into a full stack trace dripping with js-interop influence:
Exception: Non unique ID: dart-0
Stack Trace: #0      Proxy._forward (file:///home/chris/repos/ice-code-editor/test/packages/js/js.dart:1043:22)
#1      Proxy.noSuchMethod (file:///home/chris/repos/ice-code-editor/test/packages/js/js.dart:1033:20)
#2      Ace.edit (file:///home/chris/repos/ice-code-editor/test/packages/ice_code_editor/editor.dart:238:65)
#3      Editor._startJsAce (file:///home/chris/repos/ice-code-editor/test/packages/ice_code_editor/editor.dart:180:20)
#4      Editor._startAce.<anonymous closure> (file:///home/chris/repos/ice-code-editor/test/packages/ice_code_editor/editor.dart:171:52)
Last night, we tried to tweak the code to prevent the problem. Fresh eyes today make me realize that I am probably going to have to re-organize my test suite to eliminate this problem.

What leads me to believe that a code re-organization is in the cards is that the various specs—for the core editor, the storage, the full-screen editor—all run fine in isolation. This particular problem only pops up when two different test groups try to run the same js-interop code. I do not think this is a limitation of js-interop or of Dart. I think it is more a case that the organization scheme that I have chosen was, in hindsight, poor.

So far, all of my tests have needed to run in the browser. Since I have multiple classes in ICE, I have been creating a new file to test each of these classes. I think that is OK, but what is not OK—at least not when there is certain kinds of js-interop work involved—is how I included the tests. On the web page that provided the test suite context, I had included the various test files as:
<head>
  <title>ICE Test Suite</title>
  <script type="application/dart" src="editor_test.dart"></script>
  <script type="application/dart" src="store_test.dart"></script>
  <script type="application/dart" src="gzip_test.dart"></script>
  <script type="application/dart" src="full_test.dart"></script>
</head>
Each of those _test.dart files had their own main() entry point. For normal Dart code (and even certain classes of tests), this is perfectly OK. What makes it not OK is that the editor.dart pulled JS into its own isolate:
import 'package:unittest/unittest.dart';
import 'dart:html';
import 'package:ice_code_editor/ice.dart';

main() {
  // tests that create js-interop proxies
}
And then the full_test.dart did the exact same thing:
import 'package:unittest/unittest.dart';
import 'dart:html';
import 'package:ice_code_editor/ice.dart';

main() {
  // tests that create js-interop proxies
}
So both tests have their own main isolate, but share libraries and ultimately try to share js-interop proxies into the same JavaScript code (the ACE code editor in this case). The first time through, everything is OK. The second time through, js-interop tries to setup the first proxy in the new isolate, only to find that it has already started a zeroeth proxy. Even if that succeeds (and I'm not sure how it could), there is still a problem caused by double-loading ACE JavaScript script files that result in the cannot read property 'ace/ace' of undefined. Some of what Damon and I did last night ensured that the ACE JavaScript source would only load once. But obviously that only works in a single isolate.

So it seems clear that I have no choice but to run all of my tests in the same main() isolate. If I want to retain the same separation of testing concerns, this means that I need to move my test files into Dart “parts.”

Starting from the top, I load a single (new) ice_test.dart source file into my testing web page context:
<head>
  <title>ICE Test Suite</title>
  <script type="application/dart" src="ice_test.dart"></script>
  <script src="packages/browser/dart.js"></script>
</head>
In ice_test.dart, I import the packages necessary to run all of my tests: unittest, dart:html and, of course, ice. Then I declare the various parts that make up this testing library:
library ice_test;

import 'package:unittest/unittest.dart';
import 'package:ice_code_editor/ice.dart';
import 'dart:html';

part 'editor_test.dart';
part 'store_test.dart';
part 'gzip_test.dart';
part 'full_test.dart';

main(){
  editor_tests();
  store_tests();
  gzip_tests();
  full_tests();
}
I have to give separate names to each of the functions that run the tests in editor_test.dart, full_test.dart, etc. so that the names do not clash. I start with the convention of using the plural of the file name.

The last thing that I need to do is declare each of the _test.dart files as parts of the ice_test library. The editor_test.dart file then becomes:
part of ice_test;

editor_tests() {
  // test here
}
After doing the same in the other test files, I am ready to run my test suite and... it works!



Yay! No red. All in all, I am pretty happy with that solution. I might prefer not to have to follow the convention of popularizing the the test functions, but if that is the only bother that I have in my test organization, I can live with it.


Day #752

2 comments:

  1. I can't wait for our next adventure. So if the unit testing didn't involve interop-ing with external libraries, we wouldn't have run into the error - even if we had multiple main methods?

    ReplyDelete
    Replies
    1. Yup. The two variations of the last error were either js-interop being confused about its proxies or the same thing that we fixed (but happening cross isolates).

      Even so, I _think_ it's probably a good idea to put all tests in a single isolate. The reporting works better. It's also possible to mark any test in any of the _test.dart files as a solo_test to get only one test to run (before it would still run all tests in the other isolates).

      Delete