Sunday, August 11, 2013

Extracting Working Dart Code into a Pub Package


I think that I have some pretty useful keyboard event handling code for Dart on my hands. I would like to think that it is useful enough that others might have some use for and, more importantly, might be able to help me improve. So tonight, I am going to extract it out of the ICE Code Editor codebase into its own library.

Dart boasts nothing short of freaking amazing library support. In some respects, breaking this code out into a library is trivial. I can create a repository, a pubspec.yaml file for my library, add the code to the lib sub-directory and I am done. But this is Dart. This is a new language and a new paradigm for reusable browser code. As such, I am not going to half-ass this.

Development of this new code was heavily driven by some pretty solid tests. I am not going to lose those tests. Also, since Dart, even at this early stage, has a pretty excellent package repository at Dart Pub, I will end this exercise by pushing my new package.

Getting started, I create a new repository. I choose to call it ctrl-alt-foo since handling keyboard shortcuts largely drove this effort:
➜  repos  mkdir ctrl-alt-foo
➜  repos  cd !$
➜  repos  cd ctrl-alt-foo
➜  ctrl-alt-foo  git init .
Initialized empty Git repository in /home/chris/repos/ctrl-alt-foo/.git/
➜  ctrl-alt-foo git:(master) mkdir lib test
➜  ctrl-alt-foo git:(master) touch README.md LICENSE pubspec.yaml
I add basic documentation to README.md and add the MIT license to LICENSE. The pubspec.yaml file needs only the most basic information at this point in order to make this a real Dart package:
name: ctrl_alt_foo
version: 0.0.1
description: Keyboard library to make keyboard events in Dart a pleasure.
authors:
- Chris Strom <chris@eeecomputes.com>
homepage: https://github.com/eee-c/ctrl-alt-foo
dependencies:
  unittest: any
I add those files to Git and am ready for the next step: installing dependencies.

Actually there are not many dependencies in this case—just unittest and its dependencies (I add js later). Dart's pub command makes this easy enough:
➜  ctrl-alt-foo git:(master) pub install
Resolving dependencies............
Downloading unittest 0.6.15+3 from hosted...
Downloading path 0.6.15+3 from hosted...
Downloading meta 0.6.15+3 from hosted...
Downloading stack_trace 0.6.15+3 from hosted...
Dependencies installed!
This reminds me that I need to dot-git-ignore the packages directory, along with a few other items:
➜  ctrl-alt-foo git:(master) ✗ cat <<IGNORE > .gitignore
packages
pubspec.lock
out
docs
IGNORE
With the preliminaries out of the way, I am ready to start extracting code and tests out of the ICE repository and into this new repository. I have 150+ passing tests in ICE, many of which cover this functionality. So the approach that I take is to leave those tests in place while I move the code. Once I have verified that everything is still working, I will move the tests as well.

I move the key_event_x.dart directly into the new package repository:
➜  ice-code-editor git:(ctrl-alt-foo) mv lib/key_event_x.dart ../ctrl-alt-foo/lib/
I want to verify my package locally before pushing it out to the public, so I update ICE's dependencies to a local path:
name: ice_code_editor
#...
dependencies:
  # ...
  ctrl_alt_foo:
    path: /home/chris/repos/ctrl-alt-foo
This will resolve as a legitimate Dart package just the way that a pub.dartlang.org or a github repository would but has the advantage of working locally without needing to pull and changes. Any work that I do in my local ctrl-alt-foo repository is immediately available in ICE thanks to the path dependency. After a quick pub install:
➜  ice-code-editor git:(ctrl-alt-foo) ✗ pub install
Resolving dependencies........
Dependencies installed!
➜  ice-code-editor git:(ctrl-alt-foo) ✗ ls -l packages 
total 20
lrwxrwxrwx 1 chris chris 34 Aug 11 22:55 ctrl_alt_foo -> /home/chris/repos/ctrl-alt-foo/lib
...
I see my new package which should contain the recently relocated key_event_x.dart file. To use this package file, I need to change the import statement in the ICE library file from the in-package version:
library ice;
// ...
import 'key_event_x.dart';
To use my new package:
library ice;
// ...
import 'package:ctrl_alt_foo/key_event_x.dart';
With that, I am no longer using a local class file, but one in a new repository that is completely ready for publishing to pub.dartlang.org. A quick test run reveals that all of my tests still pass:
CONSOLE MESSAGE: unittest-suite-wait-for-done
...
CONSOLE MESSAGE: PASS: Keyboard Shortcuts can open the new dialog
CONSOLE MESSAGE: PASS: Keyboard Shortcuts Open Projects Dialog can open the project dialog
CONSOLE MESSAGE: PASS: Keyboard Shortcuts Open Projects Dialog hitting enter with no filter text does nothing
CONSOLE MESSAGE: PASS: Keyboard Shortcuts Open Projects Dialog enter opens the top project
CONSOLE MESSAGE: PASS: Keyboard Shortcuts Open Projects Dialog tab key moves forward in list
CONSOLE MESSAGE: PASS: Keyboard Shortcuts Open Projects Dialog down arrow key moves forward in list
CONSOLE MESSAGE: PASS: Keyboard Shortcuts Open Projects Dialog up arrow key moves backward in list
CONSOLE MESSAGE: PASS: Keyboard Shortcuts Open Projects Dialog up arrow at top of list moves back into filter
CONSOLE MESSAGE: PASS: Keyboard Shortcuts Open Projects Dialog With Less Than 10 Projects first project has keyboard focus
CONSOLE MESSAGE: PASS: Keyboard Shortcuts Open Projects Dialog With Less Than 10 Projects up arrow at top of list stays at top of list
CONSOLE MESSAGE: PASS: Keyboard Shortcuts Open Projects Dialog With Less Than 10 Projects down arrow at bottom of list stays at bottom of list
CONSOLE MESSAGE: PASS: Toggling the code editor with hotkey
CONSOLE MESSAGE: PASS: Toggling the code editor with post message
CONSOLE MESSAGE: All 154 tests passed.
CONSOLE MESSAGE: unittest-suite-success
Even though the ctrl_alt_foo package code is ready to go, it lacks something vitally important: tests. To be sure, the ICE tests cover the functionality, but I have no tests in my new package to describe how it works. That is simply not cool.

Unfortunately, I now realize that most of the tests in ICE do not directly apply to ctrl_alt_foo code. The ICE tests are higher-level, verifying things like Ctrl+N opens a new project dialog. I am exercising all of the functionality in ctrl_alt_foo, but the expectation can only be verified with a full installation of ICE. I could make ICE a dependency of ctrl_alt_foo, but since the latter depends on the former, that seems a bad idea. Besides, it should not take too much effort to write a few tests, based on the ICE tests, for my new library.

To run browser tests in Dart, a bit of boilerplate is needed. The purposes of each are documented elsewhere, but I need a shell script to run (for continuous integration), a web page to supply browser context and some test harness code, and a test file that runs the tests and interacts with the browser harness code.

The test/run.sh Bash script runs the code through dartanalyzer and runs the tests under Chrome's content_shell:
#!/bin/bash

# Static type analysis
results=$(dartanalyzer test/test.dart 2>&1)
echo "$results"
if [[ "$results" == *"warnings found"* || "$results" == *"error"* ]]
then
  exit 1
fi

results=$(content_shell --dump-render-tree test/index.html 2>&1)
echo -e "$results"

if [[ "$results" == *"Some tests failed"* ]]
then
  exit 1
fi
The test/index.html browser context file loads the test.dart and then does a lot of work setting up the testing environment:
<script type="application/dart" src="test.dart"></script>
<script type='text/javascript'>
  var testRunner = window.testRunner || window.layoutTestController;
  if (testRunner) {
    function handleMessage(m) {
      if (m.data == 'done') {
        testRunner.notifyDone();
      }
    }
    testRunner.waitUntilDone();
    window.addEventListener("message", handleMessage, false);
  }
</script>
<script src="packages/browser/dart.js"></script>
<script src="packages/browser/interop.js"></script>
Lastly, the test/test.dart file imports the necessary packages (including the ctrl_alt_foo files that will be tested) runs the tests and starts polling for the asynchronous tests to complete:
library ctrl_alt_foo_test;

import 'package:ctrl_alt_foo/key_event_x.dart';

import 'package:unittest/unittest.dart';
import 'dart:html';
import 'dart:async';

import 'package:ctrl_alt_foo/helpers.dart';

main(){

  pollForDone(testCases);
}

pollForDone(List tests) {
  if (tests.every((t)=> t.isComplete)) {
    window.postMessage('done', window.location.href);
    return;
  }

  var wait = new Duration(milliseconds: 100);
  new Timer(wait, ()=> pollForDone(tests));
}
The pollForDone function in here and the testRunner code in index.html are intertwined and so boilerplate at this point that I ought to extract them into a separate package. Something for another day, perhaps.

I write a few basic tests inside that main() entry point:
  // ...
  test("can listen for key events", (){
    KeyboardEventStreamX.onKeyDown(document).listen(expectAsync1((e) {
      expect(e.isKey('A'), true);
    }));

    type('A');
  });

  test("can listen for Ctrl shortcuts", (){
    KeyboardEventStreamX.onKeyDown(document).listen(expectAsync1((e) {
      expect(e.isCtrl('A'), true);
    }));

    typeCtrl('A');
  });

  test("can listen for Ctrl-Shift shortcuts", (){
    KeyboardEventStreamX.onKeyDown(document).listen(expectAsync1((e) {
      expect(e.isCtrlShift('A'), true);
    }));

    typeCtrlShift('A');
  });
  // ...
And run the test runner:
➜  ctrl-alt-foo git:(master) ✗ ./test/run.sh
Analyzing test/test.dart...
No issues found.
CONSOLE MESSAGE: unittest-suite-wait-for-done
CONSOLE MESSAGE: PASS: can listen for key events
CONSOLE MESSAGE: PASS: can listen for Ctrl shortcuts
CONSOLE MESSAGE: PASS: can listen for Ctrl-Shift shortcuts
CONSOLE MESSAGE: 
CONSOLE MESSAGE: All 3 tests passed.
CONSOLE MESSAGE: unittest-suite-success
Content-Type: text/plain
layer at (0,0) size 800x600
  RenderView at (0,0) size 800x600
layer at (0,0) size 800x600
  RenderBlock {HTML} at (0,0) size 800x600
    RenderBody {BODY} at (8,8) size 784x584
#EOF
#EOF
#EOF
➜  ctrl-alt-foo git:(master) ✗ echo $?
0
That looks solid for a first pass.

The last thing that I need to do tonight is make the code available on GitHub: https://github.com/eee-c/ctrl-alt-foo and “pub lish” it to Dart Pub. As easy as it is to get it on GitHub, pub makes publihsing my new package even easier:
➜  ctrl-alt-foo git:(master) pub lish
Publishing "ctrl_alt_foo" 0.0.1:
|-- .gitignore
|-- LICENSE
|-- README.md
|-- lib
|   |-- helpers.dart
|   '-- key_event_x.dart
|-- pubspec.yaml
'-- test
    |-- index.html
    |-- run.sh
    '-- test.dart

Looks great! Are you ready to upload your package (y/n)? y
Uploading.........
ctrl_alt_foo 0.0.1 uploaded successfully.
And, just like that, I am done: http://pub.dartlang.org/packages/ctrl_alt_foo! I extracted perfectly usable code out into a separate, tested library and published that library as a package to Dart's pub.dartlang.org in a single night. On top of that, my running code never once experienced a hiccup during this process. ICE continues to work exactly as it had, only now I have pulled nearly 100 lines of non-domain code out into a library. Nice!

About the only thing that went wrong tonight was setting up the drone.io continuous integration process (I was forbidden for some reason). I will pick back up with that tomorrow.


Day #840

No comments:

Post a Comment