Sunday, September 14, 2014

NPM and Polymer Puzzles for Great Coding Fun


I love coding because of the puzzles.

I confess that I am not much of a 1000-piece picture-puzzle person. That just seems like so much tedium. Every piece has one exact place to go. There is only one solution. And you can get to that single solution if you mindlessly try to find pieces that fit together for an hour or two. There is no challenge, just diversion.

Which is why I like coding so much. There is never a single solution. Heck there might not even be a solution. But it sure is fun trying to find these solutions. Maybe that is why I like testing my code so much. As much as I appreciate the assurance and maintainability that testing gives me, it is yet another challenge. It also presents challenges of setting up tests properly, writing tests that actually test something and, of course, getting the actual code to work.

I am working through the project chapters in Patterns in Polymer tonight, hoping to get as much of it covered by tests as possible. This is aided my new eee-polymer-tests test generator / common setup NPM package. Except when it does not work.

I am working on the AngularJS chapter which describes using custom Polymer elements in Angular projects. I have some tests in there, but I have the feeling that they are rather old. So my thinking is to run the test generator on the chapter project and overwrite everything (it is all in Git, so I can always get a test back if it proves useful).

The problem? When I run the generator with the --force flag, it seems to overwrite my existing test files and karma configuration, but... they are not affected:
$ ./node_modules/eee-polymer-tests/generator.js x-pizza --force

Generating test setup for: x-pizza
Force overwrite: karma.conf.js.
Force overwrite: test/PolymerSetup.js.
Force overwrite: test/XPizzaSpec.js.
Force overwrite: bower.json.
Done!

$ git status
On branch master
Changes not staged for commit:
  (use "git add ..." to update what will be committed)
  (use "git checkout -- ..." to discard changes in working directory)

        modified:   bower.json
        modified:   karma.conf.js
        modified:   package.json
So what gives? Also, I know this was working in my other projects. What is it about this project that is preventing the force-overwrite option from behaving as desired?

Since this is working for some of the generators, but not others, it seems likely that there is a difference in implementation between the different generators. That or it's a permissions issue. It's usually a permissions issue. But not this time.

In this case, the problem is my Node.js code—it is too Node.js-y. Sidenote: it is hard adding an “-y” to the end of a proper noun that includes punctuation. The difference between the generators is that some are synchronized file system operations while the problem ones are asynchronous—as is typical in Node.js-land (hrm… that doesn't seem any better). The Nodey (bleh) version looks like:
function generatePolymerSetup() {
  var template = __dirname + '/templates/PolymerSetup.js';
  fs.readFile(template, 'utf8', function (err, data) {
    if (err) {
      console.log(err);
      process.exit(1);
    }

    var content = data.replace(/\{\{element-name\}\}/g, element);
    fs.writeFile(POLYMER_SETUP, content);
  });
}
The lack of the word “sync” in there is the giveaway that this is asynchronous code. Node opens a stream to read then merrily executes whatever other code is ready to be evaluated / run outside of this function. Eventually the filesystem responds with the contents of the file and, if Node is not busily executing code elsewhere, it will invoke the callback function with any file content or error information that it might have. And again, when Node tries to write the content to the POLYMER_SETUP file, it will open a write file descriptor then move on to some other business until the filesystem responds that writing is ready. Unless it never gets the chance.

And herein lies my problem. While Node is waiting on the filesystem, it continues to work through the other generator business with which it is tasked until there is no more work. And unlike a Node.js server, this generator script just stops without giving the filesystem operations a chance to complete.

Once the problem has been identified as too much async, the solution is to use the synchronous versions of file system operations:
function generatePolymerSetup() {
  var template = __dirname + '/templates/PolymerSetup.js';
  var content = fs.
    readFileSync(template, 'utf8').
    replace(/\{\{element-name\}\}/g, element);

  fs.writeFileSync(POLYMER_SETUP, content);
}
Node.js will block (not do anything else) until the readFileSync() operation returns. Similarly, it will not do anything else until the new setup file is written and writeFileSync() returns. And once both are done—and only once both are done—Node.js is free it move onto the next generator. In other words, there is no way to accidentally skip any generated files.

Is this the best solution? For a command-line operation, it probably is. There might a more Node.js solution (that kind of works)—especially if I am more careful about tracking open file descriptors. But this seems a good enough solution.

I am a little unsure why I am seeing this problem now. It may be that the filesystem has more work to do before signaling back to Node that streams are ready. When initially testing, I may have manually deleted some of these files before running the generator.

Solving that little puzzle is a nice win, but I am not rewarded with a passing test. Even the skeleton Jasmine test created by my generator is failing when run with Karma:
$ karma start --single-run
INFO [karma]: Karma v0.12.23 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 37.0.2062 (Linux)]: Connected on socket K5_WrlH4x_H2n31y3Iz6 with id 85813120
Chrome 37.0.2062 (Linux) <x-pizza> element content has a shadow DOM FAILED
        Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
Chrome 37.0.2062 (Linux): Executed 1 of 1 (1 FAILED) ERROR (5.072 secs / 5.066 secs)
I would not unhappy if all of this just worked, but the harder it is to solve, the more satisfying the solution.

In this case, my PolymerSetup.js is failing, not the actual test. In the setup, I poll for Polymer to be fully initialized before allowing the tests to run. In Jasmine (2.0+), this is done by calling the done() callback:
// Delay Jasmine specs until Polymer is ready
var POLYMER_READY = false;
beforeEach(function(done) {
  function waitForPolymer() {
    if (Polymer) {
      Polymer.whenReady(done);
      return;
    }
    setTimeout(waitForPolymer, 1000);
  }
  waitForPolymer();

  if (POLYMER_READY) done();
});
The problem turns out to be that whenReady() has been renamed as whenPolymerReady() in the Polymer project. Fun. It takes me a bit to track that one down, including a trip into the Karma debugger and the Chrome debugger:



So, even though Polymer.whenPolymerReady() seems a little redundant, that would seem to be the solution du jour:
// Delay Jasmine specs until Polymer is ready
var POLYMER_READY = false;
beforeEach(function(done) {
  function waitForPolymer() {
    if (Polymer && Polymer.whenReady) {
      Polymer.whenReady(done);
      return;
    }
    if (Polymer && Polymer.whenPolymerReady) {
      Polymer.whenPolymerReady(done);
      return;
    }
    setTimeout(waitForPolymer, 100);
  }
  waitForPolymer();

  if (POLYMER_READY) done();
});
Yeah, I think I'll hedge my bets on that one by including both whenReady() and whenPolymerReady(). Unless the name is changed in another unique way, I should be covered.

And, thankfully, this time I am rewarded. Not only does the generated test pass, but so does the test that used to be in there:
$ karma start
INFO [karma]: Karma v0.12.23 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [watcher]: Changed file "/home/chris/repos/polymer-book/book/code-js/angular/test/XPizzaSpec.js".
Chrome 37.0.2062 (Linux): Executed 2 of 2 SUCCESS (0.063 secs / 0.057 secs)
So I got to solve fun puzzles and have as my reward some code that is more maintainable and future proofed. For a coder like me, that is a good day.


Day #183

No comments:

Post a Comment