29 Jul 04

Staking Out File Changes

Michael McCracken has the automation spirit. He’s tired of doing anything repetitive, and he’s not gonna take it anymore! For starters, he went looking for a program or script that would run his unit tests whenever his Python source files changed. That search didn’t turn up anything, so he solved his problem the ol’ fashioned way — he wrote a program. From his blog:
Inspired by Pragmatic Project Automation, I just posted about getting my unit tests to run automatically whenever I change a file.

Since nobody volunteered, I went ahead and wrote what I wanted: stakeout.

If you want to run tests anytime a source file changes:

  stakeout testAll.py *.py

If you want to run make anytime a source file changes:

  stakeout make *.[c,h] dir/*.[c,h]

It uses kqueue to watch the files on the argument list. If a file is modified, stakeout prints out the name of the modified file and runs your script. You can leave this running as long as you want, since it’s just idle until a file changes.

I’ve been using this for a couple of hacking sessions, and it’s great. It’s really convenient - the tests run instantly when you save a watched file, and it doesn’t run if you didn’t change anything.

stakeout is an Objective-C application that runs on Mac OS X. It’s available under a Creative Commons attribution-sharealike license. Michael told me that he’d love to see patches, so if you make improvements please let him know.

Inspired by Michael’s work, I decided to adapt the filemonitor.rb Ruby script from the book (available in the example code download) to do something similar to stakeout:

  if ARGV.size < 2
    puts "Usage: stakeout.rb <command> [files to watch]+"
    exit 1
  end

  command = ARGV.shift
  files = {}

  ARGV.each do |arg|
    Dir[arg].each { |file|
      files[file] = File.mtime(file)
    }
  end

  loop do

    sleep 1

    changed_file, last_changed = files.find { |file, last_changed|
      File.mtime(file) > last_changed
    }

    if changed_file
      files[changed_file] = File.mtime(changed_file)
      puts "=> #{changed_file} changed, running #{command}"
      system(command)
      puts "=> done"
    end

  end

Given that Ruby script, if you want to run the test target of your Ant build file for every file that has changed time, for example, type this:

  $ stakeout.rb "ant test" *.java

Many thanks to Michael for sharing this automation trick!