Geminating Autumn (Making a new ruby gem with newgem and RubiGen Spec helper)

Posted by Bougyman Sun, 10 Aug 2008 16:30:00 GMT

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

AUTUMNROOT/libs/
and move it to
NEWGEMROOT/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 autumngenerate and autumndestroy 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 generatortesthelper.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 generatortesthelper.rb that was spec-oriented.

The new helper results (generatorspechelper.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 .rb 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 .rb 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 test_ 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}
((
|   .*
)*)  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 autumngeneratorspec.rb. RubiGen also provided a nice local testgeneratorhelper.rb which I converted to specgeneratorhelper.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 autumngeneratorspec.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 autumngeneratorspec.rb, I add the code necessary to pass the tests to ../appgenerators/autumn/autumngenerator.rb. Should be complete by the end of today (Sunday). Check back for the release announcement.