Sunday, July 11, 2010

Code Should Only Exist to Make a Test Pass

‹prev | My Chain | next›

Over the past few days, I have worked through the library of fab.js apps that support my (fab) game. I have gotten testing coverage in place with vows.js and cleaned up some of my code while I was at it (always good sign for a testing framework).

But I have made code changes. Ideally everything is still working, but well... it is not. When I first enter the room everything stops. Specifically, the comet session drops immediately.

Sure enough, when I check the comet_view resource via curl, it drops right back to the command line prompt rather than blocking for more output:
cstrom@whitefall:~/repos/my_fab_game$ curl http://localhost:4011/comet_view?player=foo\&x=250\&y=350
<html><body>
// comet initialization
<script type="text/javascript">window.parent.player_list.new_player({"id":"foo","x":250,"y":350,"uniq_id":"ef41a5af47ae84ba00fe36e602fbef85"})</script>
<script type="text/javascript">window.parent.player_list.new_player({"id":"foo","x":250,"y":350,"uniq_id":"ef41a5af47ae84ba00fe36e602fbef85"})</script>
cstrom@whitefall:~/repos/my_fab_game$
In fab.js, HTTP connections are terminated by calling a downstream listener with no arguments. So I work through my comet_view stack looking for things like out():
  ( /^\/comet_view/ )
( broadcast_new )
( store_player )
( init_comet )
( player_from_querystring )
Both store_player and init_comet have downstream calls with no arguments. To figure out which one is the cause, I add some print STDERR calls to each:
function init_comet (app) {
return function () {
var out = this;

return app.call( function listener(obj) {
if (obj && obj.body && obj.body.uniq_id) {
// take the upstream body response and send
// it back down downstream
}
else {
puts("[init_comet] terminating");
out();
}


return listener;
});
};
}
Indeed, that does get called:
cstrom@whitefall:~/repos/my_fab_game$ ./game.js
[store_player] adding: foo
[add_player] broadcasting about: foo
...

[init_comet] terminating
Dang. How is that possible? Well, it must be coming from downstream because it is in the listener that is sent to the upstream app.

There is only one app that is upstream of init_comet, player_from_querystring:
function player_from_querystring() {
var out = this;
return function(head) {
if (head && head.url && head.url.search) {
var uniq_id;
// calculate the uniq_id

var search = head.url.search.substring(1);
var q = require('querystring').parse(search);

var app = out({ body: {id: q.player, x: q.x || 0, y: q.y || 0, uniq_id: uniq_id} });
if ( app ) app();
}
else {
out();
}
};
}
Ah, there is an out() call in there. But, print STDERR proves that it is not being called, nor should it—that code is only reached when query strings are not supplied. There is a query string:
cstrom@whitefall:~/repos/my_fab_game$ curl http://localhost:4011/comet_view?player=foo\&x=250\&y=350
So where is the trouble coming from?

Aw nuts! There is more than one out() call in there! The other is disguised as app():
function player_from_querystring() {
var out = this;
return function(head) {
if (head && head.url && head.url.search) {
var uniq_id;
// calculate the uniq_id

var search = head.url.search.substring(1);
var q = require('querystring').parse(search);

var app = out({ body: {id: q.player, x: q.x || 0, y: q.y || 0, uniq_id: uniq_id} });
if ( app ) app();
}
else {
out();
}
};
}
Dang! That is another out() call. In the init_comet middleware / binary app, the last thing that the listener sent to player_from_querystring does is return itself:
function init_comet (app) {
return function () {
var out = this;

return app.call( function listener(obj) {
// Lots of cool comet code

return listener;
});
};
}
That listener then gets assigned to the app local variable, which is then invoked with no arguments, terminating the HTTP connection.

There is nothing wrong with any of that code. Both the middleware app and the upstream app behave in a very typical fab.js way. In fact, I only added that app() call because it is a typical fab.js thing to do. But I clearly did not add that to make a test pass. My only defense is that I did that before I had any comfort level with vows.js or any Javascript testing.

Aw well, lesson learned. Hopefully. And a lesson learned is a good place to stop for the night.


Day #161

3 comments:

  1. Hehe. I couldn't help but think of you when I tracked that one down.

    ReplyDelete
  2. Don't look at Firetower then... no tests at all, purely exploratory coding :-P

    ReplyDelete