Hi all, I have been working on some code to assist with distributed unit testing using Test::Unit and Rinda. I thought I'd post it here assuming that someone else might find it interesting or useful. It's a bit raw, and I'm still working out some bugs with unclean shutdowns of the test servers. Also, it's not documented (yet), but it's only about 240 lines of code. There are easier ways of doing this, of course, but I had a few requirements that caused me to write it this way: 1) Distribute tests to the test servers on an individual test method basis 2) Avoid (as much as possible) having to rewrite any of the Test::Unit code via method aliasing. You'll have to run a ring server - see ringserver.rb from Eric Hodel's site at http://segment7.net/projects/ruby/drb/rinda/ringserver.html. Also, I did not provide the 'attribute_accessors' file, since that is just like the one in the rails support package (except that is modified to be used in a Module instead of Class). The rest of the files are included inline below. Here is an explanation of what to do with each: service.rb - This file continas definitions for producer/consumer classes for the distributed test service, which is shared via a tuple space. distributed.rb - This file contains mixins for Test::Unit::TestCase and Test::Unit::TestSuite that enable them to use the distributed service. server.rb - Run this on every machine that will be given unit tests to run. tests.rb - This is a sample unit test file test.rb - This is a sample master script, which is run as 'ruby test.rb -d tests.rb.' If you run 'ruby test.rb tests.rb,' the tests are run locally. Regards, Joe Hosteny jhosteny at gmail dot com --service.rb-- require 'rinda/ring' require 'rinda/tuplespace' require 'rinda/rinda' def log *args $stdout.write "(#{Thread.current}) " puts *args $stdout.flush end module Rinda class RingFinger # Change this to your local network broadcast netmask @@broadcast_list.push("192.168.1.255") end end module Service class Base def initialize(name) @name = name DRb.start_service log "Started DRb on URI #{DRb.uri}" Rinda::RingFinger.primary end def consumer? respond_to? :consume end def method_missing(meth, *args) ts = Thread.current[:tuplespace][2] ts = Rinda::TupleSpaceProxy.new(ts) if consumer? ts.send(meth, *args) end end class Producer < Base def initialize(name) super ts = Rinda::TupleSpace.new name = "#{@name}:#{DRb.uri}" tuple = Rinda::RingProvider.new(@name.to_sym, ts, name).provide Thread.current[:tuplespace] = Rinda::RingFinger.primary.read(tuple) trap("EXIT") do Rinda::RingFinger.primary.take(Thread.current[:tuplespace]) end end end class Consumer < Base def consume tuple = [:name, @name.to_sym, nil, nil] Thread.current[:tuplespace] = Rinda::RingFinger.primary.take(tuple) log "Got tuplespace from URI: #{Thread.current[:tuplespace][2].__drburi}" begin yield self ensure Rinda::RingFinger.primary.write(Thread.current[:tuplespace]) end end end end --distributed.rb-- require 'test/unit' require 'test/unit/testresult' require 'attribute_accessors' require 'service' module DistributedTestCase module ClassMethods @@service = nil mattr_accessor :service @@file = nil mattr_accessor :file module Run end def start_client @@service = Service::Consumer.new('DistributedTest') end def start_server @@service = Service::Producer.new('DistributedTest') loop do log "Waiting to take test" file, name, meth, oid = *(@@service.take([:test, nil]).last) log "Loading #{name}::#{meth} in file #{file}" load(file) klass = nil i = 0 ObjectSpace.each_object do |obj| if (obj.class == Class and obj.to_s == name) klass = obj break end i += 1 end log "Checked #{i} objects" begin test = klass.new(meth) log "Running #{name}::#{meth} in file #{file})" test.run(Test::Unit::TestResultProxy.new(@@service, oid)) log "Finished running #{name}::#{meth} in file #{file}" rescue => e @@service.write([:result, oid, :exception, e]) end end end def inherited(base) caller[0] =~ /(.+?):.*/ @@file = File.expand_path($1) end end class << self def included(base) base.extend(ClassMethods) base.class_eval do alias_method :run_original, :run alias_method :run, :run_distributed end end end def run_distributed(result) if ClassMethods.service.consumer? th = Thread.new do log "New thread" ClassMethods.service.consume do |srv| oid = method(method_name).object_id log "Dispatching test #{self.class.to_s}::#{method_name} (#{oid})" srv.write [:test, [ClassMethods.file, self.class.to_s, method_name, oid]] log "Waiting for result from #{self.class.to_s}::#{method_name}" loop do tuple = [:result, oid, nil, nil] tuple = srv.take(tuple) args, method = tuple.pop, tuple.pop log "Test #{self.class.to_s}::#{method_name} called #{method}" if method == :exception raise args.class, "#{args.message}\n\t(remote) #{args.backtrace.join("\n\t(remote) ")}\n" end if %W(add_failure add_error).include? method.to_s klass = Test::Unit::Error klass = Test::Unit::Failure if method.to_s =~ /failure/ result.send(method, klass.new(*args)) else result.send(method) end break if method == :add_run end end log "Thread exiting" end callcc do |cc| throw :new_thread, [th, cc] end else run_original(result) do |s,n| end end end end module DistributedTestSuite class << self def included(base) base.class_eval do alias_method :run_original, :run alias_method :run, :run_distributed end end end def run_distributed(result, &block) threads = [] th, cc = *catch(:new_thread) do run_original(result, &block) nil end if th threads << th cc.call end threads.each { |th| th.join } end end module Test module Unit class TestSuite include DistributedTestSuite end class TestCase include DistributedTestCase end class TestResultProxy def initialize(server, oid) @server = server @oid = oid end def method_missing(name, *args) name = name.id2name if name =~ /add_(.*)/ if %W(failure error).include? $1 args = args[0] if $1 =~ /failure/ args = [args.test_name, args.location, args.message] else args = [args.test_name, args.exception] end end @server.write([:result, @oid, name.to_sym, args]) end end end end end --server.rb-- #!/bin/env ruby require 'optparse' require 'distributed' Test::Unit::TestCase.start_server --tests.rb-- require 'test/unit' class TC_MyTest < Test::Unit::TestCase def setup puts "in setup" end def teardown puts "in teardown" end def test_it assert(false, 'Assertion was false.') end def test_pass assert(true, 'Assertion was true.') end end --test.rb-- #!/bin/env ruby require 'optparse' require 'distributed' Test::Unit::TestCase.start_client require ARGV.shift -- Posted via http://www.ruby-forum.com/.