Autotest notifications on Ubuntu using lib-notify

words by Brian Racer

At Jetpack we are big fans of Test Driven Development(TDD), and specifically Behavior Driven Development(BDD) using excellent testing frameworks like RSpec and Cucumber. When you are using ZenTest’s autotest or autospec, it’s nice to have unobtrusive notifications of passing/failing tests while you work. Mac users can integrate autotest with the excellent Growl, but what about us Linux users? Well be can use a library called lib-notify.

First lets play with the lib-notify binary. Install it:

sudo apt-get install libnotify-bin

To make sure it is working, try this:

notify-send "Hello World!"

You should see the notification appear! Now lets edit our ~/.autotest file to use that notification:

vi ~/.autotest
module Autotest::GnomeNotify
 
  # Time notification will be displayed before disappearing automatically
  EXPIRATION_IN_SECONDS = 2
  ERROR_STOCK_ICON = "gtk-dialog-error"
  SUCCESS_STOCK_ICON = "gtk-dialog-info"
 
  # Convenience method to send an error notification message
  #
  # [stock_icon]   Stock icon name of icon to display
  # [title]        Notification message title
  # [message]      Core message for the notification
  def self.notify stock_icon, title, message
    options = "-t #{EXPIRATION_IN_SECONDS * 1000} -i #{stock_icon}"
    system "notify-send #{options} '#{title}' '#{message}'"
  end
 
  Autotest.add_hook :red do |at|
    notify ERROR_STOCK_ICON, "Tests failed", "#{at.files_to_test.size} tests failed"
  end
 
  Autotest.add_hook :green do |at|
    notify SUCCESS_STOCK_ICON, "All tests passed, good job!", ""
  end
 
end

Although not quite as pretty as Growl yet, it works! If you are happy with that you can stop there. If you want something that looks a little bit nicer, read on!

The next few steps are going to require a few development libraries and gems, so install the following packages to make sure we are on the same page:

apt-get install  libinotify-ruby libgtk2-ruby  libnotify-dev
sudo gem install zentest autotest autotest-rails red-green launchy

Unfortunately the libnotify ruby library isn’t included in Ubuntu 9.04 Jaunty, so we will have to build that ourselves:

wget http://rubyforge.org/frs/download.php/27134/ruby-libnotify-0.3.3.tar.bz2
tar -xvjpf ruby-libnotify-0.3.3.tar.bz2
cd  ruby-libnotify-0.3.3
ruby extconf.rb
make
sudo make install

Next put the following pretty images in your ~./autotest_images directory:

Now we can use a modified version of Linden LAN’s .autotest:

require 'rnotify'
require 'launchy'
require 'gtk2'
 
module Autotest::RNotify
  class Notification
    attr_accessor :verbose, :image_root, :tray_icon, :notification,
                  :image_pass, :image_pending, :image_fail,
                  :image_file_pass, :image_file_pending, :image_file_fail,
                  :status_image_pass, :status_image_pending, :status_image_fail
 
    def initialize(timeout = 5000,
                   image_root = "#{ENV['HOME']}/.autotest_images" ,
                   report_url = "doc/spec/report.html",
                   verbose = false)
      self.verbose = verbose
      self.image_root = image_root
      self.image_file_pass = "#{image_root}/pass.png"
      self.image_file_pending = "#{image_root}/pending.png"
      self.image_file_fail = "#{image_root}/fail.png"
 
      raise("#{image_file_pass} not found") unless File.exists?(image_file_pass)
      raise("#{image_file_pending} not found") unless File.exists?(image_file_pending)
      raise("#{image_file_fail} not found") unless File.exists?(image_file_fail)
 
      puts 'Autotest Hook: loading Notify' if verbose
      Notify.init('Autotest') || raise('Failed to initialize Notify')
 
      puts 'Autotest Hook: initializing tray icon' if verbose
      self.tray_icon = Gtk::StatusIcon.new
      tray_icon.pixbuf = Gdk::Pixbuf.new(image_file_pending,22,22)
      tray_icon.tooltip = 'RSpec Autotest'
 
      puts 'Autotest Hook: Creating Notifier' if verbose
      self.notification = Notify::Notification.new('X', nil, nil, tray_icon)
 
      notification.timeout = timeout
 
      puts 'Autotest Hook: Connecting mouse click event' if verbose
      tray_icon.signal_connect("activate") do
        Launchy::Browser.new.visit(report_url)
      end
 
      Thread.new { Gtk.main }
      sleep 1
      tray_icon.embedded? || raise('Failed to set up tray icon')
    end
 
    def notify(icon, tray, title, message)
      notification.update(title, message, nil)
      notification.pixbuf_icon = icon
      tray_icon.tooltip = "Last Result: #{message}"
      tray_icon.pixbuf = tray
      notification.show
    end
 
    def passed(title, message)
      self.image_pass ||= Gdk::Pixbuf.new(image_file_pass, 48, 48)
      self.status_image_pass ||= Gdk::Pixbuf.new(image_file_pass, 22, 22)
      notify(image_pass, status_image_pass, title, message)
    end
 
    def pending(title, message)
      self.image_pending ||= Gdk::Pixbuf.new(image_file_pending, 48, 48)
      self.status_image_pending ||= Gdk::Pixbuf.new(image_file_pending, 22, 22)
      notify(image_pending, status_image_pending, title, message)
    end
 
    def failed(title, message)
      self.image_fail ||= Gdk::Pixbuf.new(image_file_fail, 48, 48)
      self.status_image_fail ||= Gdk::Pixbuf.new(image_file_fail, 22, 22)
      notify(image_fail, status_image_fail, title, message)
    end
 
    def quit
      puts 'Autotest Hook: Shutting Down...' if verbose
      #Notify.uninit
      Gtk.main_quit
    end
  end
 
  Autotest.add_hook :initialize do |at|
    @notify = Notification.new
  end
 
  Autotest.add_hook :ran_command do |at|
    results = at.results.last
 
    unless results.nil?
      output = results[/(\d+)\s+examples?,\s*(\d+)\s+failures?(,\s*(\d+)\s+pending)?/]
      if output
        failures = $~[2].to_i
        pending = $~[4].to_i
      end
 
      if failures > 0
        @notify.failed("Tests Failed", output)
      elsif pending > 0
        @notify.pending("Tests Pending", output)
      else
        unless at.tainted
          @notify.passed("All Tests Passed", output)
        else
          @notify.passed("Tests Passed", output)
        end
      end
    end
  end
 
  Autotest.add_hook :quit do |at|
    @notify.quit
  end
end

This should leave you with a pretty good base if you want to further customize the script!