Friday, July 12, 2013

Hop Test Server! Hop!


Probably my favorite feature of the Dart Weekly newsletter is the package update section. Chris Buckett does a nice job of culling a few interesting little packages that I would otherwise overlook. It is fun to see some of the interesting, smaller projects that are going on in Dart-land. And every now and then I come across one that seems like it might be of use.

In this week's issue, the phrase “runtime scripts integrated with unit tests” caught my eye in the Hop project. I have been running an HTTP server to support some of my Dart tests. Maybe it will be of some help? Heck, even if I can't use it now, it's always fun to try something new. To the Dart cave!

I start with the usual update of Hipster MVC's pubspec.yaml to depend on the Hop package:
name: hipster_mvc
version: 0.2.6
description: Dart-based MVC framework
author: Chris Strom <chris@eeecomputes.com>
homepage: https://github.com/eee-c/hipster-mvc
dependencies:
  unittest: any
  dirty: any
  uuid: any
  browser: any
  hop: any
Followed by a quick pub install:
➜  hipster-mvc git:(http-test-server) ✗ pub install
Resolving dependencies...............
Downloading bot 0.22.0 from hosted...
Downloading bot_io 0.21.2 from hosted...
Downloading hop 0.22.2 from hosted...
Dependencies installed!
The project wiki has some pretty nice documentation. Per the Setup section, I create bin sub-directory and copy Hop's hop command into it:
➜  hipster-mvc git:(http-test-server) ✗ mkdir bin
➜  hipster-mvc git:(http-test-server) ✗ cp /home/chris/.pub-cache/hosted/pub.dartlang.org/hop-0.22.2/bin/hop bin
I wonder if this might be better placed in tool as it is an internal script, but I stick with the convention for now.

Next up, I convert my existing test/test_server.dart script to a library so that it can be imported and called from another script:
library test_server;
// import stuff here..

Future<HttpServer> run() {
  var port = 31337;
  return HttpServer.bind('127.0.0.1', port)
    ..then((app) {
      app.listen((HttpRequest req) {
        // process requests like normal
      });

      print('Server started on port: ${port}');
    });
}
Starting a server is an asynchronous undertaking, so I return the Future from the HttpServer.bind() call. Hopefully, that is all that I need to do in test_server.dart.

Now, I create tool/hop_runner.dart and define my start and stop server tasks:
import 'package:hop/hop.dart';

import '../test/test_server.dart' as TestServer;
import 'dart:async';

void main() {
  addAsyncTask('test_server:start', _startTestServer);
  addTask('test_server:stop', _stopTestServer);
  runHop();
}
This should give me two hop tasks: test_server:start and test_server:stop. But first, I need to define the two associated task functions.

I am unsure how I will be able to stop the server, so I leave the _stopTestServer() function definition blank for now. In _startTestServer(), I build a started completer that I can use to mark the task completed once the server's future is marked complete:
Future<bool> _startTestServer(TaskContext content) {
  var started = new Completer();
  TestServer.run().then((_) {
    started.complete(true);
  });
  return started.future;
}

bool _stopTestServer(TaskContext context) {
  // Not sure how this will work...
  return true;
}
When I ask Hop for a list of tasks, however, I am greeted with:
➜  hipster-mvc git:(http-test-server) ✗ ./bin/hop
Unhandled exception:
Illegal argument: "name" -- The value "test_server:start" does not contain the pattern "JSRegExp: pattern=^[a-z]([a-z0-9_\-]*[a-z0-9])?$ flags="
Bother. I suppose colons are reserved for future nesting support. I prefer test_server:start mostly because I am used to that from the Rails world. A dash should suffice for now:
void main() {
  addAsyncTask('test_server-start', _startTestServer);
  addTask('test_server-stop', _stopTestServer);
  runHop();
}
With that, I am ready to try this from the command line:
➜  hipster-mvc git:(http-test-server) ✗ ./bin/hop test_server-start
Unhandled exception:
type '(TaskContext) => bool' is not a subtype of type 'Task' of 'task'.
#0      addTask (package:hop/hop.dart:54:32)
#1      main (file:///home/chris/repos/hipster-mvc/tool/hop_runner.dart:8:10)
Weird! I had expected that I might get a problem from my async task with actual code, but it is no-op addTask() that is causing trouble. It turns out that this should be addSyncTask(), not addTask(). Both addAsyncTask() and addSyncTask() convert their callbacks into Task objects that can be submitted to addTask(). I make the change:
void main() {
  addAsyncTask('test_server-start', _startTestServer);
  addSyncTask('test_server-stop', _stopTestServer);
  runHop();
}
And then re-run hop to find:
➜  hipster-mvc git:(http-test-server) ✗ ./bin/hop test_server-start
Server started on port: 31337
Yay! It worked. Except…

My server shuts down immediately. As soon as the Hop process exits, the Dart VM containing the server exits as well. In other words, running the server from inside Hop's hop_runner.dart ain't gonna work.

So instead, I revert all of my changes to my test_server.dart so that it needs to be run from the command line as dart test/test_server.dart. Then, in Hop's hop_runner.dart, I fork a shell process:
import 'package:hop/hop.dart';

import '../test/test_server.dart' as TestServer;
import 'dart:async';
import 'dart:io';

void main() {
  addAsyncTask('test_server-start', _startTestServer);
  addSyncTask('test_server-stop', _stopTestServer);
  runHop();
}

Future<bool> _startTestServer(TaskContext content) {
  var started = new Completer();

  Process.start('dart', ['test/test_server.dart'], runInShell: true).then((_) {
    started.complete(true);
  });

  return started.future;
}
With that, I am able to start my test web server from the command line:
➜  hipster-mvc git:(http-test-server) ✗ ./bin/hop test_server-start
➜  hipster-mvc git:(http-test-server) ✗ ps -ef | grep test_server  
chris      783     1  0 23:56 pts/23   00:00:00 /bin/sh -c 'dart' 'test/test_server.dart'
chris      784   783  3 23:56 pts/23   00:00:00 dart test/test_server.dart
➜  hipster-mvc git:(http-test-server) ✗ curl http://localhost:31337/test
{"foo":1}%   
I am still forced to manually kill the server:
➜  hipster-mvc git:(http-test-server) ✗ kill 784
But that is something that I ought to be able to improve upon another day.

For now, it seems that I can make use of hop in my test suite. I am unsure if it will prove a significant improvement on what I already have, but it ought to suffice as a suitable framework for building future improvements.


Day #810

No comments:

Post a Comment