Autumn gem released!
Completed the gemination of Autumn. Currently the source, as well as a source code browser are available in our repository. Hopefully in the coming week Tim can find the time to merge this re-factor in. Thank’s to drnic and everyone involved in newgem, Hoe, and RubiGen, they were wonderfully straightforward to use. Most of the major time spent was learning the ins and outs of these new tools, as well as getting familiar with Rspec. It’s a great weekend when you can accomplish a set goal (gem autumn) and Sharpen the Saw (learn Rspec) at the same time. I’ve attached the completed autumn gem to this post and here if you’re interested in a preview.
Geminating Autumn (Making a new ruby gem with newgem and RubiGen Spec helper)
This weekend has been spent creating a gem out of autumn. The autumn IRC framework has allowed us to create single-purpose and very complex IRC (and Jabber) bots which manage workflows for both fun and profit. It allows you to create such a workflow processer with a minimum amount of code utilizing a rails-like structure.
Thanks to some great articles (drnic’s newgem article, drnic’s gem articles). Other articles pointed me there, but unfortunately the RubyGems manual does not have much information at all about creating a new gem. Thanks to RISCfuture autumn was a pretty simply transition from the full tree to a newgem generated tree. First move was to take everything in
AUTUMN_ROOT/libs/and move it to
NEWGEM_ROOT/lib/autumn/. Once that was complete I moved on to the autumn script/ directory, files which generate autumn leaves and control the autumn process. First thing I noticed is that the original autumn ‘generate’ and ‘destroy’ were not rubigen generators, but self-written generators in the rails generator/RubiGen style by RISCfuture. After some digging in to RubiGen and what it would take to convert these specialized generators, I decided to put that in the code debt category and just rename them to autumn_generate and autumn_destroy and put them in the script directory as such. A few changes to the autumn libs (mostly replacing libs/ with autumn/), getting ./script/console, ./script/daemon, ./script/server running was fairly straightforward, again mostly lib path replacing (libs/ -> autumn/). Next step was to create the autumn (bin/autumn) application generator to create a bare autumn tree, ready to use the gem lib/autumn/* libraries and lib/autumn/resources/ libraries instead of local ones. This led to an immersion course into the wonderful RubiGen system, particularly the generator_test_helper.rb, which is used to create Unit tests for the application generator. After corresponding with RISCfuture, I heard the good news that he has been working on writing a spec suite for testing autumn itself, so I decided it was time to delve into spec myself (I’ve always used Test::Unit thus far). This meant creating an equivalent to generator_test_helper.rb that was spec-oriented.
The new helper results (generator_spec_helper.rb):
module RubiGen
module GeneratorSpecHelper
# Runs the create command (like the command line does)
def run_generator(name, params, sources, options = {})
generator = build_generator(name, params, sources, options)
silence_generator do
generator.command(:create).invoke!
end
generator
end
# Instatiates the Generator
def build_generator(name, params, sources, options)
options.merge!(:collision => :force) # so no questions are prompted
if sources.is_a?(Symbol)
if sources == :app
RubiGen::Base.use_application_sources!
else
RubiGen::Base.use_component_sources!
end
else
RubiGen::Base.reset_sources
RubiGen::Base.prepend_sources(*sources) unless sources.blank?
end
RubiGen::Base.instance(name, params, options)
end
# Silences the logger temporarily and returns the output as a String
def silence_generator
logger_original=RubiGen::Base.logger
myout=StringIO.new
RubiGen::Base.logger=RubiGen::SimpleLogger.new(myout)
# TODO redirect $stdout to myout
yield if block_given?
RubiGen::Base.logger=logger_original
# TODO fix $stdout again
myout.string
end
# asserts that the given file was generated.
# the contents of the file is passed to a block.
def generated_file?(path)
file_exists?(path)
File.open("#{APP_ROOT}/#{path}") do |f|
yield f.read if block_given?
end
end
# asserts that the given file exists
def file_exists?(path)
File.exists?("#{APP_ROOT}/#{path}").should eql(true)
end
# asserts that the given directory exists
def directory_exists?(path)
File.directory?("#{APP_ROOT}/#{path}").should eql(true)
end
# asserts that the given class source file was generated.
# It takes a path without the <tt>.rb</tt> part and an optional super class.
# the contents of the class source file is passed to a block.
def generated_class?(path,parent=nil)
path=~/\/?(\d+_)?(\w+)$/
class_name=$2.camelize
generated_file?("#{path}.rb") do |body|
it "should define #{class_name} in #{path}.rb" do
body.should match(/class #{class_name}#{parent.nil? ? '':" < #{parent}"}/)
end
yield body if block_given?
end
end
# asserts that the given module source file was generated.
# It takes a path without the <tt>.rb</tt> part.
# the contents of the class source file is passed to a block.
def generated_module?(path)
path=~/\/?(\w+)$/
module_name=$1.camelize
generated_file?("#{path}.rb") do |body|
it "should define #{module_name} in #{path}.rb" do
body.should match(/module #{module_name}/)
end
yield body if block_given?
end
end
# asserts that the given unit test was generated.
# It takes a name or symbol without the <tt>test_</tt> part and an optional super class.
# the contents of the class source file is passed to a block.
def generated_test_for?(name, parent="Test::Unit::TestCase")
generated_class? "test/test_#{name.to_s.underscore}", parent do |body|
yield body if block_given?
end
end
# asserts that the given methods are defined in the body.
# This does assume standard rails code conventions with regards to the source code.
# The body of each individual method is passed to a block.
def has_method?(body,*methods)
methods.each do |name|
it "should define the method #{name.to_s}" do
body.should match(/^ def #{name.to_s}\n((\n| .*\n)*) end/)
end
yield( name, $1 ) if block_given?
end
end
def app_root_files
Dir[APP_ROOT + '/**/*']
end
def rubygem_folders
%w[bin examples lib test]
end
def rubygems_setup
bare_setup
rubygem_folders.each do |folder|
Dir.mkdir("#{APP_ROOT}/#{folder}") unless File.exists?("#{APP_ROOT}/#{folder}")
end
end
def rubygems_teardown
bare_teardown
end
def bare_setup
FileUtils.mkdir_p(APP_ROOT)
end
def bare_teardown
FileUtils.rm_rf TMP_ROOT || APP_ROOT
end
end
end
This of course requires including RubiGen and spec in the autumn_generator_spec.rb. RubiGen also provided a nice local test_generator_helper.rb which I converted to spec_generator_helper.rb:
begin
require 'spec'
rescue LoadError
require 'rubygems'
gem 'rspec'
require 'spec'
end
require 'fileutils'
# Must set before requiring generator libs.
TMP_ROOT = File.dirname(__FILE__) + "/tmp" unless defined?(TMP_ROOT)
PROJECT_NAME = "autumn" unless defined?(PROJECT_NAME)
app_root = File.join(TMP_ROOT, PROJECT_NAME)
if defined?(APP_ROOT)
APP_ROOT.replace(app_root)
else
APP_ROOT = app_root
end
if defined?(RAILS_ROOT)
RAILS_ROOT.replace(app_root)
else
RAILS_ROOT = app_root
end
begin
require 'rubigen'
rescue LoadError
require 'rubygems'
require 'rubigen'
end
require File.join(File.dirname(__FILE__), 'generator_spec_helper')
$:.unshift(File.dirname(__FILE__) + '/../app_generators/autumn')
require 'autumn_generator'
And finally the autumn_generator_spec.rb (not yet complete):
require File.join(File.dirname(__FILE__), '/spec_generator_helper.rb')
module AutumnGeneratorSpecHelper
include RubiGen::GeneratorSpecHelper
def generator_path
"app_generators"
end
def sources
[RubiGen::PathSource.new(:test, File.join(File.dirname(__FILE__),"..", generator_path))
]
end
end
# Time to add your specs!
# http://rspec.info/
describe AutumnGenerator, "when application is generated" do
include AutumnGeneratorSpecHelper
before(:all) do
bare_setup
run_generator('autumn', [APP_ROOT], sources)
end
%w{lib log script test tmp}.each do |dir|
it "should create#{dir}" do
directory_exists?(dir)
end
end
%w{console destroy generate autumn_generate autumn_destroy daemon server}.each do |script|
script_path = File.join("script", script)
it "should create #{script_path}" do
file_exists?(script_path)
end
end
after(:all) do
bare_teardown
end
end
As I add to autumn_generator_spec.rb, I add the code necessary to pass the tests to ../app_generators/autumn/autumn_generator.rb. Should be complete by the end of today (Sunday). Check back for the release announcement.