<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array"/>
  <modified type="array">
    <modified>
      <diff>@@ -16,19 +16,29 @@ OPTIONS
     Displays version information and exits.
   * -h
     Displays this help information and exits.
+  * -F
+    Don't freshen existing checkouts (skip git pull, svn update, etc).
   * -f
     Freshens existing checkouts (git pull, svn update, etc).
   * -p
     Prepares dependencies and exits without running checks.
+  * -n
+    Dry-run, display commands to execute but don't run them.
 
 
-BASIC EXAMPLE
--------------
+BASIC EXAMPLES
+--------------
   # Runs test suites against the &quot;ruby&quot; interpreter in your current
   # shell's PATH and records them to the &quot;reports&quot; directory:
-
   ./rubychecker.rb
 
+  # Run only the test suite for rspec:
+  ./rubychecker.rb rspec
+
+  # Run only the test suite for rails version 2.1.0 and record the results
+  # into a report with the tag &quot;p265&quot; in the filenames:
+  ./rubychecker.rb -t p265 rails=2.1.0
+
 
 COMPLEX EXAMPLE
 ---------------</diff>
      <filename>README.txt</filename>
    </modified>
    <modified>
      <diff>@@ -15,18 +15,28 @@
 #     Displays version information and exits.
 #   * -h
 #     Displays this help information and exits.
+#   * -F
+#     Don't freshen existing checkouts (skip git pull, svn update, etc).
 #   * -f
 #     Freshens existing checkouts (git pull, svn update, etc).
 #   * -p
 #     Prepares dependencies and exits without running checks.
+#   * -n
+#     Dry-run, display commands to execute but don't run them.
 #
-# == BASIC EXAMPLE
+# == BASIC EXAMPLES
 #
 #   # Runs test suites against the &quot;ruby&quot; interpreter in your current
 #   # shell's PATH and records them to the &quot;reports&quot; directory:
-#
 #   ./rubychecker.rb
 #
+#   # Run only the test suite for rspec:
+#   ./rubychecker.rb rspec
+#
+#   # Run only the test suite for rails version 2.1.0 and record the results
+#   # into a report with the tag &quot;p265&quot; in the filenames:
+#   ./rubychecker.rb -t p265 rails=2.1.0
+#
 # == COMPLEX EXAMPLE
 #
 #   # Download, compile and check a Ruby interpreter from SVN:
@@ -83,11 +93,15 @@
 
 require 'fileutils'
 require 'open-uri'
+require 'pathname'
+require 'set'
 require 'uri'
 
 class RubyChecker
-  VERSION = &quot;r1&quot;
+  # Version of RubyChecker.
+  VERSION = &quot;r2&quot;
 
+  # Source URLs for dependencies.
   SOURCES = {
     :rails    =&gt; &quot;git://github.com/rails/rails.git&quot;,
     :rspec    =&gt; &quot;git://github.com/dchelimsky/rspec.git&quot;,
@@ -95,6 +109,7 @@ class RubyChecker
     :rubygems =&gt; &quot;http://rubyforge.org/frs/download.php/38647/rubygems-1.2.0.zip&quot;,
   }
 
+  # Gems to install.
   GEMS = %w(
     diff-lcs
     heckle
@@ -108,50 +123,80 @@ class RubyChecker
     syntax
   )
 
-  SUITES = %w(
-    rubyspec
-    rspec
-    rails
-  )
-
+  # Base working directory.
   attr_accessor :base_dir
+  # Directory with cached downloads.
   attr_accessor :cache_dir
+  # Directory with reports.
   attr_accessor :reports_dir
+  # Directory with gems.
   attr_accessor :gems_dir
+
+  # Pathname instance for base_dir.
+  attr_accessor :base_path
+  # Pathname instance for cache_dir.
+  attr_accessor :cache_path
+  # Pathname instance for reports_dir.
+  attr_accessor :reports_path
+  # Pathname instance for gems_dir.
+  attr_accessor :gems_path
+
+  # String to tag report filenames with, e.g., &quot;p265&quot;.
   attr_accessor :tag
+  # Freshen the checked-out files? Defaults to true.
   attr_accessor :freshen
+  # Display commands without running them? Defaults to false.
+  attr_accessor :dryrun
 
+  # Instantiate a new RubyChecker object.
+  #
+  # Options:
+  # * :base_dir =&gt; Base working directory to use. Defaults to current directory.
+  # * :tag      =&gt; String to tag report filenames with, e.g., &quot;p265&quot;. Defaults to &quot;current&quot;.
+  # * :freshen  =&gt; Freshen the checked-out files? Defaults to true.
+  # * :dryrun   =&gt; Display commands without running them? Defaults to false.
   def initialize(opts={})
-    @base_dir = File.expand_path(opts[:base_dir] || Dir.pwd)
-    @tag      = opts[:tag] || &quot;current&quot;
-    @freshen  = opts[:freshen] == true || false
+    @base_dir     = File.expand_path(opts[:base_dir] || Dir.pwd)
+    @reports_dir  = File.expand_path(File.join(@base_dir, &quot;reports&quot;))
+    @cache_dir    = File.expand_path(File.join(@base_dir, &quot;cache&quot;))
+    @gems_dir     = File.expand_path(File.join(@cache_dir, &quot;gems&quot;))
+
+    @base_path    = Pathname.new(@base_dir)
+    @reports_path = Pathname.new(@reports_dir)
+    @cache_path   = Pathname.new(@cache_dir)
+    @gems_path    = Pathname.new(@gems_dir)
 
-    @reports_dir = File.expand_path(File.join(@base_dir, &quot;reports&quot;))
-    @cache_dir   = File.expand_path(File.join(@base_dir, &quot;cache&quot;))
-    @gems_dir    = File.expand_path(File.join(@cache_dir, &quot;gems&quot;))
+    @tag          = opts[:tag] || &quot;current&quot;
+    @freshen      = opts[:freshen] != false
+    @dryrun       = opts[:dryrun] == true || false
   end
 
   #---[ prepare ]---------------------------------------------------------
 
+  # Prepare the environment, by checking out the sources, installing the
+  # RubyGems application, and installing the Gem libraries.
   def prepare
     prepare_sources
     prepare_rubygems
     prepare_gems
   end
 
+  # Prepare directories, by creating them if necessary.
   def prepare_dirs
     [@reports_dir, @cache_dir, @gems_dir].each do |dir|
       Dir.mkdir(dir) unless File.directory?(dir)
     end
   end
 
+  # Prepare sources, download them if necessary.
   def prepare_sources
     SOURCES.each_pair do |suite, url|
-      prepare_source_for(url)
+      prepare_source_for(url, @freshen)
     end
   end
 
-  def prepare_source_for(url)
+  # Download or freshen a download from +url+.
+  def prepare_source_for(url, freshen=true)
     self.prepare_dirs
     FileUtils.cd(@cache_dir) do
       uri = URI.parse(url)
@@ -159,15 +204,15 @@ class RubyChecker
       when &quot;git&quot;
         name = File.basename(url, &quot;.git&quot;)
         if File.directory?(name)
-          if @freshen
+          if freshen
             FileUtils.cd(name) do
-              system &quot;git checkout -f master&quot;
-              system &quot;git pull --rebase origin master&quot;
-              system &quot;git fetch --tags&quot;
+              run &quot;git checkout -f master&quot;
+              run &quot;git pull --rebase origin master&quot;
+              run &quot;git fetch --tags&quot;
             end
           end
         else
-          system &quot;git clone #{url}&quot;
+          run &quot;git clone #{url}&quot;
         end
       when &quot;http&quot;, &quot;ftp&quot;
         name = File.basename(url)
@@ -184,10 +229,14 @@ class RubyChecker
     end
   end
 
+  # Prepare RubyGems application, install it if necessary.
+  #
+  # NOTE: This method sets the PATH to provide access to the installed Gem
+  # libraries, so it must be called before the checks run.
   def prepare_rubygems
     begin
       require &quot;rubygems&quot;
-      # RubyGems already installed
+      # RubyGems is installed
     rescue LoadError
       # RubyGems needs to be installed
       FileUtils.cd(@cache_dir) do
@@ -195,128 +244,289 @@ class RubyChecker
         dir     = File.basename(archive, &quot;.zip&quot;)
         FileUtils.rm_rf(dir) if File.directory?(dir)
         # TODO consider using rubyzip?
-        system &quot;unzip #{archive}&quot;
+        run &quot;unzip #{archive}&quot;, true
         FileUtils.cd(dir) do
-          system &quot;ruby 'setup.rb' --no-ri --no-rdoc&quot;
+          run &quot;ruby 'setup.rb' --no-ri --no-rdoc&quot;, true
         end
       end
     end
     ENV[&quot;PATH&quot;] = &quot;#{`gem env path`.strip}/bin:#{ENV['PATH']}&quot;
   end
 
-  def prepare_gems
+  # Prepare Gem libraries, install them if necessary.
+  def prepare_gems(gems=GEMS)
     FileUtils.cd(@gems_dir) do
       listing = `gem list --local`
-      missing = GEMS.reject{|package| listing.match(/^#{package}\s/)}
-      system &quot;gem install #{missing.join(' ')} --no-ri --no-rdoc&quot; if missing.size &gt; 0
+      missing = gems.reject{|package| listing.match(/^#{package}\s/)}
+      run &quot;gem install #{missing.join(' ')} --no-ri --no-rdoc&quot;, true if missing.size &gt; 0
     end
   end
 
   #---[ check ]-----------------------------------------------------------
 
+  # Check the +targets+ by running their test suites.
+  #
+  # Arguments:
+  # * targets =&gt; String, Tuples, or Array of Strings that are either a word
+  #   like &quot;rspec&quot; which represent a test suite's title, or a compound word
+  #   like &quot;rspec=1.1.4&quot; which represents a test suite's title and variant.
   def check(*targets)
     targets.flatten!
-    targets.concat(SUITES) if targets.size == 0
-    targets.each do |suite|
-      name = &quot;check_#{suite}&quot;
-      if self.respond_to?(name)
-        self.send(name)
-      else
-        raise ArgumentError, &quot;Unknown suite: #{suite}&quot;
+
+    suites = Set.new
+
+    if targets.size == 0
+      suites += Suite.suites
+    else
+      targets.each do |target|
+        title, variant = target.split(&quot;=&quot;)
+        matches = Suite.suites_for(title, variant)
+        if matches.size == 0
+          raise ArgumentError, &quot;Unknown suite: #{target}&quot;
+        else
+          suites += matches
+        end
       end
     end
-  end
 
-  def check_rubyspec
-    name = &quot;rubyspec&quot;
-    FileUtils.cd(File.join(@cache_dir, name, &quot;1.8&quot;)) do
-      system &quot;mspec . 2&gt;&amp;1 | tee #{report_filename_for(name)}&quot;
+    suites.each do |suite|
+      suite.new(self).invoke
     end
   end
 
-  def check_rspec
-    name = &quot;rspec&quot;
-    version = &quot;1.1.4&quot;
-    FileUtils.cd(File.join(@cache_dir, name)) do
-      system &quot;git checkout -f #{version}&quot;
-      system &quot;rake spec 2&gt;&amp;1 | tee #{report_filename_for(name, version)}&quot;
+  #---[ reporting ]-------------------------------------------------------
+
+  # Run the +command+. Displays the command alwasy, but only executes it if
+  # @dryrun is false. If +fatal+ is true, a non-zero exit value from a system
+  # call will cause the program to exit.
+  def run(command, fatal=false)
+    current_path    = Pathname.new(Dir.pwd)
+    base2current    = current_path.relative_path_from(@base_path)
+    current2reports = @reports_path.relative_path_from(current_path)
+    displayable = &quot;(cd #{base2current} &amp;&amp; #{command.gsub(/#{@reports_dir}/, current2reports.to_s)})&quot;
+
+    puts(displayable)
+    unless self.dryrun
+      system(command) 
+      if fatal &amp;&amp; $?.exitstatus != 0
+        puts &quot;ERROR RUNNING: #{displayable}&quot;
+        exit $?.exitstatus
+      end
     end
   end
 
-  RAILS_TESTS = {
-    &quot;2.1.0&quot; =&gt; lambda{|checker|
-      name = &quot;rails&quot;
-      version = &quot;2.1.0&quot;
-      system &quot;git checkout -f v#{version}&quot;
-      system &quot;rake test 2&gt;&amp;1 | tee #{checker.report_filename_for(name, version)}&quot;
-    },
-    &quot;2.0.2&quot; =&gt; lambda{|checker|
-      name = &quot;rails&quot;
-      version = &quot;2.0.2&quot;
-      system &quot;git checkout -f v#{version}&quot;
-      system &quot;rake test 2&gt;&amp;1 | tee #{checker.report_filename_for(name, version)}&quot;
-    },
-    &quot;1.2.6&quot; =&gt; lambda{|checker|
-      name = &quot;rails&quot;
-      version = &quot;1.2.6&quot;
-      system &quot;git checkout -f v#{version}&quot;
-      report = checker.report_filename_for(name, version)
-      system &quot;cat /dev/null &gt; #{report}&quot;
-      Dir[&quot;*&quot;].select{|path| File.directory?(path)}.each do |dir|
-        FileUtils.cd(dir) do
-          system &quot;rake test 2&gt;&amp;1 | tee -a #{report}&quot;
+  # Create a report file from the results of running the +command+ for the test
+  # suite with the +title+ and +variant+.
+  def create_report_for(command, title, variant)
+    self.run(&quot;#{command} 2&gt;&amp;1 | tee #{self.report_filename_for(title, variant)}&quot;)
+  end
+
+  # Append to an existing report file the results of running the +command+ for
+  # the test suite with the +title+ and +variant+.
+  def append_report_for(command, title, variant)
+    self.run(&quot;#{command} 2&gt;&amp;1 | tee -a #{self.report_filename_for(title, variant)}&quot;)
+  end
+
+  # Remove the report for the test suite with the +title+ and +variant.
+  def remove_report_for(title, variant)
+    report = self.report_filename_for(title, variant)
+    File.delete(report) if File.exist?(report)
+  end
+
+  # Return a report filename for the test suite +title+ and +variant+. If the
+  # +variant+ is nil, it's not included in the filename.
+  def report_filename_for(title, variant=nil)
+    return File.join(@reports_dir, &quot;#{title}#{variant ? '_'+variant.to_s : ''}_with_#{@tag}.log&quot;)
+  end
+
+  #---[ suite ]-----------------------------------------------------------
+
+  # A test suite hierarchy.
+  class Suite
+    class &lt;&lt; self
+      # Set of test suites, as Suite::Base subclasses.
+      attr_accessor :suites
+    end
+    self.suites = Set.new
+
+    # Return an array of test suite titles.
+    def self.suite_titles
+      self.suites.map{|suite| suite.title}
+    end
+
+    # Return an array of suites matching the +title+ and optional +variant+.
+    def self.suites_for(title, variant=nil)
+      self.suites.select{|suite| suite.title == title &amp;&amp; (variant ? suite.variant == variant : true)}
+    end
+
+    # Common base for all Suite subclasses.
+    class Base
+      def self.inherited(subclass)
+        # Add this test suite to the registry.
+        Suite.suites &lt;&lt; subclass
+
+        subclass.module_eval do
+          class &lt;&lt; self
+            # String title of test suite, e.g., &quot;RSpec&quot;
+            attr_accessor :title
+
+            # String variant of test suite, e.g., &quot;1.1.4&quot;
+            attr_accessor :variant
+          end
+
+          # RubyChecker instance
+          attr_accessor :checker
         end
       end
-    }
-  }
 
-  def check_rails(variant=nil)
-    name = &quot;rails&quot;
-    FileUtils.cd(File.join(@cache_dir, name)) do
-      if variant
-        RAILS_TESTS[variant].call(self)
-      else
-        RAILS_TESTS.each_pair do |variant, routine|
-          routine.call(self)
+      # Instantiate a new suite subclass using the +checker+ instance.
+      def initialize(checker)
+        self.checker = checker
+      end
+
+      # Invoke the test suite.
+      def invoke
+        raise NotImplementedError, &quot;Author of subclass forgot to implement #invoke&quot;
+      end
+
+      # Return this Suite's title, e.g., &quot;RSpec&quot;.
+      def title
+        self.class.title
+      end
+
+      # Return this Suite's variant, e.g., &quot;1.1.4&quot;.
+      def variant
+        self.class.variant
+      end
+
+      # Run the +command+ and create a report from its results.
+      def create_report_for(command)
+        checker.create_report_for(command, self.title, self.variant)
+      end
+
+      # Run the +command+ and append its results to a report.
+      def append_report_for(command)
+        checker.append_report_for(command, self.title, self.variant)
+      end
+
+      # Remove an report, if it exists.
+      def remove_report
+        checker.remove_report_for(self.title, self.variant)
+      end
+
+      # Run a +command+.
+      def run(command)
+        checker.run(command)
+      end
+    end
+
+    #---[ suites ]----------------------------------------------------------
+
+    class RubySpec &lt; Base # :nodoc:
+      self.title   = &quot;rubyspec&quot;
+      self.variant = &quot;master&quot;
+
+      def invoke
+        FileUtils.cd(File.join(checker.cache_dir, self.title, &quot;1.8&quot;)) do
+          create_report_for(&quot;mspec .&quot;)
+        end
+      end
+    end
+
+    class RSpec &lt; Base # :nodoc:
+      self.title   = &quot;rspec&quot;
+      self.variant = &quot;1.1.4&quot;
+
+      def invoke
+        FileUtils.cd(File.join(checker.cache_dir, self.title)) do
+          run &quot;git checkout -f #{self.variant}&quot;
+          create_report_for(&quot;rake spec&quot;)
+        end
+      end
+    end
+
+    class Rails126 &lt; Base # :nodoc:
+      self.title   = &quot;rails&quot;
+      self.variant = &quot;1.2.6&quot;
+
+      def invoke
+        FileUtils.cd(File.join(checker.cache_dir, self.title)) do
+          run &quot;git checkout -f v#{self.variant}&quot;
+          remove_report
+          Dir[&quot;*&quot;].select{|path| File.directory?(path)}.each do |dir|
+            FileUtils.cd(dir) do
+              if dir.match(/actionwebservice/)
+                append_report_for(&quot;rake build_database&quot;)
+              end
+              append_report_for(&quot;rake test&quot;)
+            end
+          end
         end
       end
     end
-  end
 
-  #---[ misc ]------------------------------------------------------------
+    class Rails202 &lt; Base # :nodoc:
+      self.title   = &quot;rails&quot;
+      self.variant = &quot;2.0.2&quot;
 
-  def report_filename_for(suite, version=nil)
-    return File.join(@reports_dir, &quot;#{suite}#{version ? '_'+version.to_s : ''}_with_#{@tag}.log&quot;)
+      def invoke
+        FileUtils.cd(File.join(checker.cache_dir, self.title)) do
+          run &quot;git checkout -f v#{self.variant}&quot;
+          create_report_for(&quot;rake test&quot;)
+        end
+      end
+    end
+
+    class Rails210 &lt; Base # :nodoc:
+      self.title   = &quot;rails&quot;
+      self.variant = &quot;2.1.0&quot;
+
+      def invoke
+        FileUtils.cd(File.join(checker.cache_dir, self.title)) do
+          run &quot;git checkout -f v#{self.variant}&quot;
+          create_report_for(&quot;rake test&quot;)
+        end
+      end
+    end
   end
+
 end
 
+#===[ main ]============================================================
+
 if __FILE__ == $0
   require 'getoptlong'
   require 'rdoc/usage'
 
-  rc = RubyChecker.new
+  checker = RubyChecker.new
   targets = []
   prepare_only = false
 
   opts = GetoptLong.new(*[
-    ['--freshen', '-f', GetoptLong::NO_ARGUMENT],
-    ['--help',    '-h', GetoptLong::NO_ARGUMENT],
-    ['--prepare', '-p', GetoptLong::NO_ARGUMENT],
-    ['--tag',     '-t', GetoptLong::OPTIONAL_ARGUMENT],
-    ['--version', '-v', GetoptLong::NO_ARGUMENT]
+    ['--dryrun',       '-n', GetoptLong::NO_ARGUMENT],
+    ['--freshen',      '-f', GetoptLong::NO_ARGUMENT],
+    ['--help',         '-h', GetoptLong::NO_ARGUMENT],
+    ['--prepare',      '-p', GetoptLong::NO_ARGUMENT],
+    ['--skip-freshen', '-F', GetoptLong::NO_ARGUMENT],
+    ['--tag',          '-t', GetoptLong::OPTIONAL_ARGUMENT],
+    ['--version',      '-v', GetoptLong::NO_ARGUMENT]
   ])
 
   begin
     opts.each do |opt, arg|
       case opt
+      when '--dryrun'
+        checker.dryrun = true
       when '--freshen'
-        rc.freshen = true
+        checker.freshen = true
       when '--help'
         RDoc::usage
       when '--prepare'
         prepare_only = true
+      when '--skip-freshen'
+        checker.freshen = false
       when '--tag'
-        rc.tag = arg
+        checker.tag = arg
       when '--version'
         puts &quot;rubychecker #{RubyChecker::VERSION}&quot;
         exit 0
@@ -334,6 +544,6 @@ if __FILE__ == $0
     exit 0
   end
 
-  rc.prepare
-  rc.check(*targets) unless prepare_only
+  checker.prepare
+  checker.check(*targets) unless prepare_only
 end</diff>
      <filename>rubychecker.rb</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>e71dc942b52bb0ea8f0cc15f6619aef87b21652b</id>
    </parent>
  </parents>
  <author>
    <name>Igal Koshevoy</name>
    <email>igal@pragmaticraft.com</email>
  </author>
  <url>http://github.com/igal/rubychecker/commit/b301a3cd534b2cc0f951cfcd203313ce229fc043</url>
  <id>b301a3cd534b2cc0f951cfcd203313ce229fc043</id>
  <committed-date>2008-07-04T14:44:35-07:00</committed-date>
  <authored-date>2008-07-04T14:44:35-07:00</authored-date>
  <message>Abstracted test suites into Suite objects and made it possible to execute specific ones off the command-line. Added more examples and documented all methods and instance/class variables. Replaced all system calls with a new run wrapper that prints useful information and can conditionally stop the entire program if a catastrophic condition is encountered, e.g., if rubygems fails to install. Added a dryrun model to see what commands are being run. Enabled freshening of checkouts on by default, added flag to disable freshening.</message>
  <tree>517545751a0f377d5963c8af7ff96dd7219214ec</tree>
  <committer>
    <name>Igal Koshevoy</name>
    <email>igal@pragmaticraft.com</email>
  </committer>
</commit>
