Sunday, September 7, 2014

Failure: Expected Throw, Which Threw


I got started with the errata for Beta 1.0 of Dart for Hipsters er… Dart 1 for Everyone (not sure I'll ever get used to that). For the most part, it is pretty OK. There are issues, but nothing insurmountable. Except…

One of my tests is failing. Or maybe not.

I had all of my tests passing with Dart 1.5.8, but with 1.6.0, I find:
CONSOLE MESSAGE: FAIL: [bad invocation from noSuchMethod] cannot supply invocation mirror arguments
  Expected: throws 'Yikes _ajaxSave'
    Actual: <Closure: () => dynamic>
     Which: threw 'Yikes _ajaxSave")'

  package:unittest/src/simple_configuration.dart 119:7                         SimpleConfiguration.onExpectFailure
  package:unittest/src/simple_configuration.dart 15:28                         _ExpectFailureHandler.fail
  package:matcher/src/expect.dart 113:9                                        DefaultFailureHandler.failMatch
  package:matcher/src/expect.dart 73:29                                        expect
  ../varying_the_behavior/test/calling_methods_from_no_such_method.dart 38:13  run.<fn>.<fn>
  package:unittest/src/test_case.dart 102:37                                   _run.<fn>
Which is just weird, right? I expect this code throws 'Yikes _ajaxSave'. I find that, when run under test, it threw 'Yikes _ajaxSave". And this is counted as a test failure?!

I actually first noticed this error back when Dart 1.6 was still in beta. I spent a little time investigating any changes to throwing errors in 1.6, but could not find any. In the end, I thought to wait on the possibility that the language had a bug that would get sorted. That turns out not to be the case.

Since 1.6.0 arrived, I have been ignoring this as best I can so that I can focus on changes to the text of the book. It helps knowing that I will always have time to investigate in depth during these chain posts. So I get down to it.

The test is for some of Dart's dynamic code capabilities. In this test case, I have a method that reaches noSuchMethod() in a call. In this particular noSuchMethod(), I handle calls to #save, but any other calls should result in an error:
class HipsterModel {
  noSuchMethod(args) {
    if (args.memberName != #save) {
      var method_name = args.memberName.toString().
        replaceAll('Symbol("', '').
        replaceAll(new RegExp(r'@.+'), '');
      throw "Yikes ${method_name}";
    }
  }
}
The problem turns out not to be a bug, but a change to the way that symbols are converted to strings. Had I looked closer at the failure, I might have noticed that the message includes trailing double quotes. In other words, the output of toString() on a Symbol is different and I am no longer handling it properly.

I believe that the old format of Symbol toString() looked like Symbol("_ajaxSave@XXX"). I don't recall what information was embedded in the XXX part of the string, but the following code would try to strip away the data so that a “clean” failure could be thrown (and ultimately tested):
    if (args.memberName != #save) {
      var method_name = args.memberName.toString().
        replaceAll('Symbol("', '').
        replaceAll(new RegExp(r'@.+'), '');
      throw "Yikes ${method_name}";
    }
In Dart 1.6, calling toString() on a Symbol results in something like: Symbol("_ajaxSave"). I cannot argue with the change—this seems cleaner. And easier to convert into a string. In fact, I can use replaceAllMapped() to extract a regular expression placeholder to do it:
    if (args.memberName != #save) {
      var method_name = args.memberName.toString().
        replaceAllMapped(
          new RegExp(r'Symbol\("(.+)"\)'),
          (m)=> m[1]
        );
      throw "Yikes ${method_name}";
    }
The language designers make converting Symbol objects to strings intentionally awkward so that developers do not break symbol lookup when code is converted to JavaScript. If this were production code, I might just embed the full Symbol#toString() output in the error. Or allow Dart's normal noSuchMethod() handling to kick in. But, since this helps to test book code, I leave it.

With that, I have all my tests passing and dartanalyzer static analysis passing as well. So I can back to fixing errata.

Day #176

2 comments:

  1. You should use MirrorSystem.getName(), which will work when compiled with dart2js (though it will bloat your output by including the symbol map).

    ReplyDelete
    Replies
    1. Ah, good point. I had meant to point to the SO article on this (http://stackoverflow.com/questions/16058505/converting-a-symbol-into-a-string) which recommends MirrorSystem.getName(), but just forgot. So thanks for reminding me.

      The only reason I didn't use that solution was fear that it would make my book acceptance tests slow (~2 seconds wall time currently). I didn't even try it -- I just assume that any mirrors code will automatically make things slow (bad developer). I just tried this and my fears were unfounded. The tests run just as fast and now this one is future proofed against future changes to Symbol's string representation. Someday, I won't assume.

      Delete