Tuesday, October 31, 2006

Rails Macro for JEdit

I have written a macro for JEdit that allows you to jump around from one related file to another in your Rails project. The behavior mimics some of the capability of the TextMate Rails bundle. This screencast shows the macro in action:

***Update - 07/04/08***
At long last I have finally decided to post this macro to a highly-available public repository. For the longest time, I have been hosting the zip file for this macro on my home network and many times the server was unavailable when someone needed it. No longer! You can download the macro from this github repo.

You'll want to copy the 'Rails' directory into your <home_dir>/.jedit/macros directory. You will need to have the Ruby and SideKick JEdit plugins as well as the "ruby" executable for your platform visible in the "path". This macro has been tested on Windows and Linux (OS X users will have to settle for TextMate ;-) ).

Saturday, July 29, 2006

My Ruby Stub

Being new to Ruby, one of the first things I wanted to do was figure out how to apply the testing style I am comfortable with in Java.  Several years ago, Bob Lee and I wrote an IDEA plugin to generate stub-object source files from interface definitions.  For a given interface:

public interface Foo {
Bar makeABar(String hey, List now) throws BarClosedException;
}
view raw snippet.java hosted with ❤ by GitHub

The stub generator would create a class that looks like:

public class StubFoo implements Foo {
public boolean makeABarCalled;
public String makeABarHey;
public List makeABarNow;
public Bar makeABarReturn;
public BarClosedException makeABarException;
public Bar makeABar(String hey, List now) {
makeABarCalled = true;
makeABarHey = hey;
makeABarNow = now;
if (makeABarException != null) {
throw makeABarException;
}
return makeABarReturn;
}
}
view raw snippet.java hosted with ❤ by GitHub

The generated stubs are useful in testing because I can set them up with a return value or exception to simulate some condition that I am trying to test. The methods of the stub keep track of whether they were called and any parameters that were passed to them. This approach follows the state-based testing that Fowler describes, and is central to my own programming style.

In Ruby, the same kind of thing can be accomplished at runtime in a very dynamic way. This unit test demonstrates the kind of thing I am looking for in a stub generator:

require 'stub'
class Foo
def bar(hey, now)
end
def hoy()
end
end
class Bar < Foo
def blap(howdy)
end
end
class TestStub < Test::Unit::TestCase
def test_declared_methods
stub = Stub.new(Foo)
stub.bar_return = 'heynow'
result = stub.bar('baba', 'ganush')
assert stub.bar_called?
assert_equal false, stub.hoy_called?
assert_equal 'baba', stub.bar_params[0]
assert_equal 'ganush', stub.bar_params[1]
assert_equal 'heynow', result
end
def test_equals_method
one = Stub.new(Foo)
two = Stub.new(Foo)
assert_equal one, two
one.bar_return = 'yeah'
two.bar_return = 'yeah'
assert_equal one, two
two.bar_return = 'foo'
assert_not_equal one, two
assert_not_equal one, Stub.new(Fixnum)
end
def test_stub_throws_supplied_error
stub = Stub.new(Foo)
stub.bar_error = StandardError.new
assert_raise(StandardError) { stub.bar('hey', 'now') }
end
def test_stub_inherited_methods
stub = Stub.new(Bar, true)
stub.blap_return = 'one'
stub.hoy_return = 'two'
blap_result = stub.blap
hoy_result = stub.hoy
assert stub.blap_called?
assert stub.hoy_called?
assert_equal 'one', blap_result
assert_equal 'two', hoy_result
end
def test_stub_no_inherited_methods_by_default
assert_raise(NoMethodError) { Stub.new(Bar).hoy }
end
def test_params_method_created_when_needed
stub = Stub.new(Foo)
assert_nothing_raised do
stub.bar('a', 'b')
stub.bar_params[0]
end
stub.hoy
assert_raise(NoMethodError) { stub.hoy_params[0] }
end
end
view raw snippet.rb hosted with ❤ by GitHub

The Ruby Stub implementation was tricky to figure out until I learned how to use define_method:

class Stub
def initialize(clazz, include_super = false)
@clazz = clazz
meta = class << self; self; end
methods = clazz.public_instance_methods(include_super)
@called_table = Hash.new(false)
@returns_table = Hash.new()
@errors_table = Hash.new()
methods.each do |method_name|
add_reader(meta, "#{method_name}_called?") {
@called_table[method_name]
}
add_writer(meta, "#{method_name}_return") { |value|
@returns_table[method_name] = value
}
add_writer(meta, "#{method_name}_error") { |error|
@errors_table[method_name] = error
}
add_reader(meta, method_name) { |*args|
@called_table[method_name] = true
add_reader(meta, "#{method_name}_params") { args } unless args.empty?
raise @errors_table[method_name] unless @errors_table[method_name].nil?
@returns_table[method_name]
}
end
end
def ==(other)
return false unless(other.kind_of?(Stub))
return false unless(@clazz == other.clazz)
@returns_table == other.returns_table
end
private
def add_reader(meta, method_name, &block)
meta.send(:define_method, method_name.to_sym, block)
end
def add_writer(meta, item, &block)
meta.send(:define_method, "#{item}=".to_sym, block)
end
attr_reader :clazz, :returns_table, :errors_table
protected :clazz, :returns_table, :errors_table
end
view raw snippet.rb hosted with ❤ by GitHub

So in my first real foray into a Ruby utility, I was able to create something that does what I need and learn a little about the API in the process. Of course, after savoring the feeling of learning and having written something that I thought was cool, I had a closer look the Stubba API from the Mocha library and realized that I still had a long way to go. Still, the exercise was a useful one; the more I use Ruby the more I really like it.

Wednesday, March 29, 2006

STL RUG

Last night, I attended a meeting of the St. Louis Ruby User Group. I was impressed by the size of the turnout. There were easily 30 people there in the empty, nondescript office space. Though the space was crappy, it felt more community-like than the Java SIG that is held every month in the much nicer CityPlace auditorium. As well, the attendence at the few JavaSIGs I have been to in the last year has been comparitively anemic - though the Java "community" is much larger in St. Louis, it doesn't have the continuing interest that this group seems to. I think of it like a new relationship with a pretty girl - everything feels new and interesting and there are is a lot to learn and talk about. At this point, Java is the spouse the community has had a relationship with for a long time and we have covered so many things that new conversations are harder to come by.