Monday, October 22, 2007

ruby-libnotify

Back Story: For the time being, I write web applications in Ruby on Rails for a living. One of those little annoyances of application development is you have to actually run your code to see if it works. Well, since 95% of my programming experience is self-taught (yeah yeah, I studied it in an ivy league school, but that's another blog post for another day), I've grown up on the philosophy of "write a whole bunch of code then try running it". The prospect of manually testing an entire application every single time you change something is rather disenchanting to most, and causes bugs to creep up only to be found later, in alpha or beta testing; or worse, when the application gets released.

Enter the 21st-century paradigm of behavior-(or test-)driven development, where you write small pieces of application code along with bits of "throwaway" test code (in that they don't appear in your release) that invoke your application directly and make sure it does what you expect it to do. Comprehensive tests of all your code can help ensure both quality and speed of development. Running this code as often as possible alerts you to software bugs quickly and lets you dispatch with them easily.

For this reason I try to use RSpec as much as possible when developing in Rails, either through test-first development (when I have the discipline and motivation) or by alternating between cycles of coding and testing. Tools like RCov which tell you what parts of your code are getting executed by your test code are great for this. Having Autotest integration is great for ensuring your tests get run only as often as you change the code on your machine. What's even better is you can install hooks into autotest that do neat things like display graphical notifications on your desktop.

Now, from my google research, I found that most of the well-documented examples on graphical autotest notifications are written for growl, a library for Mac OS X, the platform of choice amongst the hardcore Rubyvangelists. Although I worked for Apple for a year, I am a poor person, and tend to work with more economic solutions. As such I prefer Linux on a PC I threw together from the cheapest parts I could find on the interwebs to a computer the price of a low-end luxury sedan. That meant I had to find an alternate solution to growl.

My first attempt at this was a very rudimentary growl clone that I wrote in C based on ghosd and called via a command line, which resulted in something like this:


Kinda cool, quick and dirty, but really really really really inflexible in its current state and a pain in the ass to maintain and no I don't want to show you the source code or help you set it up. You're on your own there. At any rate this is the solution on my laptop. Due to having subsequently heard of libnotify, a library for showing information popups in Linux systems, and changing jobs and getting a brand new machine at work to play with, I decided to rewrite my autotest hooks. Since libnotify is pretty core to graphical window managers that run on linux, and since there's a ruby wrapper library for it, I figured this would be a piece of cake.

Oh, how wrong I was. After building ruby-libnotify on my computer and trying it out in irb (the interactive ruby command line), I quickly got this lovely little error:

irb: symbol lookup error:
/usr/local/lib/site_ruby/1.8/i486-linux/libnotify.so: undefined
symbol: notify_notification_new


Which basically means that the wrapper library I built for Ruby, despite having seen libnotify during the build, wasn't seeing it at runtime. Cry. Anyway after a lot of googling and manpage searching, I basically ended up hacking the makefile post-configuration and changed -lnotify to -l:/usr/local/lib/libnotify.a, which forced it to favor the static library over the shared one. (What that means is that I forced it put the code into the wrapper library rather than allow it to assume the code would be present at runtime and go and try to find it on my computer.)
FOUND NEW CLUE!
That colon right there after -l makes the location of the library explicit.


So anyway I go in and build this stuff and from searching the libraries it becomes apparent that I can install this notifier as a system tray icon using the GTK libraries. Sweeeeet. Well, I try writing some code based on the Ruby-GTK wiki, and I get an unrecognized symbol error for Gtk::StatusIcon. Well Crap. It turns out Debian provides an old version of Ruby-GTK and I had to uninstall it using aptitude and go ahead and compile it myself from the source. Here I hit an additional snag where it gripes about a few inconsistencies in the code. VALUE and GType are incompatible. So the solution there is wherever there's an inconsistency, replace one with the other in the prototype. Then there's a constant whose definition it can't find, so I google and find out it's been changed, so I drop the "_TYPE" suffix from it. Finally an imported function is being called with one too many arguments, so I drop the last one. This gets me a compiled version of ruby-gtk that I can use in IRB and all is swell.

After some hacking in irb, I successfully convert the code from the link above into this:

#!/usr/bin/env ruby

require 'autotest/redgreen'
require 'autotest/timestamp'

require 'libnotify'

puts 'Autotest Hook: loading LibNotify'
LibNotify.init('Rake Autotest') || raise('Failed to initialize LibNotify')
puts 'Autotest Hook: initializing tray icon'
$tray_icon = Gtk::StatusIcon.new
$tray_icon.icon_name = 'apple-red'
$tray_icon.tooltip = 'RSpec Autotest'
puts 'Autotest Hook: Creating Notifier'
$notification = LibNotify::Notification.new('X',nil,nil,$tray_icon)
$notification.timeout = 5000

puts 'Autotest Hook: Loading Autotest Images'
image_root = "/home/dberner/.misc/autotest_images"
$png_fail = Gdk::Pixbuf.new("#{image_root}/fail.png", 48,48)
$png_pass = Gdk::Pixbuf.new("#{image_root}/pass.png", 48,48)
$png_pending = Gdk::Pixbuf.new("#{image_root}/pending.png",48,48)

Thread.new { Gtk.main }
sleep 5
$tray_icon.embedded? || raise('Failed to set up tray icon')

Autotest.add_hook :ran_command do |at|
results = [at.results].flatten.join("\n")
md = results.match(/(\d+)\sexample[s]?,\s(\d+)\sfailure[s]?,\s(\d+)\spending/)
if md
if md[2].to_i > 0
$notification.update('FAIL',md[0],nil)
$notification.pixbuf_icon = $png_fail
elsif md[3].to_i > 0
$notification.update('Pending',md[0],nil)
$notification.pixbuf_icon = $png_pending
else
$notification.update('Success',md[0],nil)
$notification.pixbuf_icon = $png_pass
end
$tray_icon.tooltip = "Last Result: #{md[0]}"
$notification.show
end
end

Autotest.add_hook :quit do |at|
puts 'Autotest Hook: Shutting Down...'
LibNotify.uninit
Gtk.main_quit
end


Which yields a rather spiffy little apple icon (you can browse icon names using IconTheme in RubyGTK) in my notification tray when I run rake spec:autotest...



And a sleek notification window when the test completes...



As well as a reminder when I hover over it telling me what happened the last time a test ran.



I got the icons from here. Since there are few good blog entries that I could find out there for using libnotify instead of growl with autotest, here's mine.

UPDATE: I informed the guy who maintains ruby-libnotify when I was having trouble with the linker as described above. He has updated the package to try and deal with these problems.

2 comments:

Dante S. Regis said...

Great Derek! It's much more Rubier than my approach! I liked it more! :)

rduht said...

I think wow gold and world of warcraft gold