Skip to content

Instantly share code, notes, and snippets.

@sorah sorah/gist:833420
Created Feb 18, 2011

Embed
What would you like to do?
diff --git common.mk common.mk
index dbbbe93..d12e882 100644
--- common.mk
+++ common.mk
@@ -471,7 +471,7 @@ test: test-sample btest-ruby test-knownbug
test-all: $(TEST_RUNNABLE)-test-all
yes-test-all: PHONY
- $(RUNRUBY) "$(srcdir)/test/runner.rb" $(TESTS)
+ $(RUNRUBY) "$(srcdir)/test/runner.rb" $(TESTS) --ruby="$(RUNRUBY)"
TESTS_BUILD = mkmf
no-test-all: PHONY
$(MINIRUBY) -I"$(srcdir)/lib" "$(srcdir)/test/runner.rb" $(TESTS_BUILD)
diff --git lib/test/unit.rb lib/test/unit.rb
index fd50d81..584ac86 100644
--- lib/test/unit.rb
+++ lib/test/unit.rb
@@ -51,6 +51,11 @@ module Test
non_options(args, options)
@help = orig_args.map { |s| s =~ /[\s|&<>$()]/ ? s.inspect : s }.join " "
@options = options
+ @opts = @options = options
+ if @options[:parallel]
+ @files = args
+ @args = orig_args
+ end
end
private
@@ -75,9 +80,31 @@ module Test
opts.on '-n', '--name PATTERN', "Filter test names on pattern." do |a|
options[:filter] = a
end
+
+ opts.on '--jobs-status [TYPE]', "Show status of jobs every file; Disabled when --jobs isn't specified." do |type|
+ options[:job_status] = true
+ options[:job_status_type] = type.to_sym if type
+ end
+
+ opts.on '-j N', '--jobs N', "Allow run tests with N jobs at once" do |a|
+ options[:parallel] = a.to_i
+ end
+
+ opts.on '--ruby VAL', "Path to ruby; It'll have used at -j option" do |a|
+ options[:ruby] = a
+ end
end
def non_options(files, options)
+ begin
+ require "rbconfig"
+ rescue LoadError
+ warn "#{caller(1)[0]}: warning: Parallel running disabled because can't get path to ruby; run specify with --ruby argument"
+ options[:parallel] = nil
+ else
+ options[:ruby] = RbConfig.ruby
+ end
+
true
end
end
@@ -175,7 +202,7 @@ module Test
$: << d
end
begin
- require path
+ require path unless options[:parallel]
result = true
rescue LoadError
puts "#{f}: #{$!}"
@@ -194,26 +221,252 @@ module Test
include Test::Unit::RunCount
class << self; undef autorun; end
+
+ alias orig_run_anything _run_anything
+ undef _run_anything
+
+ def _run_anything type
+ if @opts[:parallel] && @warnings
+ warn ""
+ ary = []
+ @warnings.reject! do |w|
+ r = ary.include?(w[1].message)
+ ary << w[1].message
+ r
+ end
+ @warnings.each do |w|
+ warn "#{w[0]}: #{w[1].message} (#{w[1].class})"
+ end
+ warn ""
+ end
+ orig_run_anything(type)
+ end
+
+ @@stop_auto_run = false
def self.autorun
at_exit {
Test::Unit::RunCount.run_once {
exit(Test::Unit::Runner.new.run(ARGV) || true)
- }
+ } unless @@stop_auto_run
} unless @@installed_at_exit
@@installed_at_exit = true
end
+ def after_worker_down(worker, e=nil, c=1)
+ return unless @opts[:parallel]
+ return if @interrupt
+ after_worker_dead worker
+ if e
+ b = e.backtrace
+ warn "#{b.shift}: #{e.message} (#{e.class})"
+ STDERR.print b.map{|s| "\tfrom #{s}"}.join("\n")
+ end
+ @need_quit = true
+ warn ""
+ warn "Some worker was crashed. It seems ruby interpreter's bug"
+ warn "or, a bug of test/unit/parallel.rb. try again without -j"
+ warn "option."
+ warn ""
+ STDERR.flush
+ exit c
+ end
+
+ def jobs_status
+ puts "" unless @opts[:verbose]
+ if @opts[:job_status]
+ b = []
+ str = @workers.map { |x|
+ a = "#{x[:pid]}:#{x[:status].to_s.ljust(7)}"
+ if x[:file]
+ if @opts[:job_status_type] == :replace
+ a = "#{x[:pid]}=#{x[:file]}"
+ else
+ if a.size > x[:file].size
+ b << x[:file].ljust(a.size)
+ else
+ a << " "*(x[:file].size-a.size)
+ b << x[:file]
+ end
+ end
+ else
+ b << " "*a.size
+ end
+ a
+ }.join(" ")
+ if @opts[:job_status_type] == :replace
+ @terminal_width ||= %x{stty size 2>/dev/null}.split[1].to_i.nonzero? \
+ || %x{tput cols 2>/dev/null}.to_i.nonzero? \
+ || 80
+ @jstr_size ||= 0
+ del_jobs_status
+ STDOUT.flush
+ print str[0...@terminal_width]
+ STDOUT.flush
+ @jstr_size = str.size > @terminal_width ? @terminal_width : str.size
+ else
+ puts str
+ puts b.join(" ")
+ end
+ end
+ end
+
+ def del_jobs_status
+ return unless @opts[:job_status_type] == :replace && @jstr_size
+ print "\r"+" "*@jstr_size+"\r"
+ end
+
+ def after_worker_dead(worker)
+ return unless @opts[:parallel]
+ return if @interrupt
+ worker[:status] = :quit
+ worker[:in].close
+ worker[:out].close
+ @workers.delete(worker)
+ @dead_workers << worker
+ @ios = @workers.map{|w| w[:out] }
+ end
+
def _run_suites suites, type
@interrupt = nil
result = []
- suites.each {|suite|
+ if @opts[:parallel]
begin
- result << _run_suite(suite, type)
+ # Require needed things for parallel running
+ require 'thread'
+ require 'timeout'
+ @tasks = @files.dup # Array of filenames.
+ @need_quit = false
+ @dead_workers = [] # Array of dead workers.
+ @warnings = []
+ shutting_down = false
+
+ # Array of workers.
+ @workers = @opts[:parallel].times.map do
+ i,o = IO.pipe("ASCII-8BIT") # worker o>|i> master
+ j,k = IO.pipe("ASCII-8BIT") # worker <j|<k master
+ k.sync = true
+ pid = spawn(*@opts[:ruby].split(/ /),File.dirname(__FILE__) +
+ "/unit/parallel.rb", *@args, out: o, in: j)
+ [o,j].each{|io| io.close }
+ {in: k, out: i, pid: pid, status: :waiting}
+ end
+
+ # Thread: watchdog
+ watchdog = Thread.new do
+ while stat = Process.wait2
+ break if @interrupt # Break when interrupt
+ w = (@workers + @dead_workers).find{|x| stat[0] == x[:pid] }.dup
+ next unless w
+ unless w[:status] == :quit
+ # Worker down
+ after_worker_down w, nil, stat[1].to_i
+ end
+ end
+ end
+ @workers_hash = Hash[@workers.map {|w| [w[:out],w] }] # out-IO => worker
+ @ios = @workers.map{|w| w[:out] } # Array of worker IOs
+
+ while _io = IO.select(@ios)[0]
+ break unless _io.each do |io|
+ break if @need_quit
+ a = @workers_hash[io]
+ buf = ((a[:status] == :quit) ? io.read : io.gets).chomp
+ case buf
+ when /^okay$/ # Worker will run task
+ a[:status] = :running
+ jobs_status
+ when /^ready$/ # Worker is ready
+ a[:status] = :ready
+ if @tasks.empty?
+ break unless @workers.find{|x| x[:status] == :running }
+ else
+ task = @tasks.shift
+ a[:file] = File.basename(task).gsub(/\.rb/,"")
+ begin
+ a[:loadpath] ||= []
+ a[:in].puts "loadpath #{[Marshal.dump($:-a[:loadpath])].pack("m").gsub("\n","")}"
+ a[:loadpath] = $:.dup
+ a[:in].puts "run #{task} #{type}"
+ rescue Errno::EPIPE
+ after_worker_down a
+ rescue IOError
+ raise unless ["stream closed","closed stream"].include? $!.message
+ after_worker_down a
+ end
+ end
+
+ jobs_status
+ when /^done (.+?)$/ # Worker ran a one of suites in a file
+ r = Marshal.load($1.unpack("m")[0])
+ # [result,result,report,$:]
+ result << r[0..1]
+ report.push(*r[2])
+ @errors += r[3][0]
+ @failures += r[3][1]
+ @skips += r[3][2]
+ $:.push(*r[4]).uniq!
+ a[:status] = :done
+ jobs_status if @opts[:job_status_type] == :replace
+ a[:status] = :running
+ when /^p (.+?)$/ # Worker wanna print to STDOUT
+ del_jobs_status
+ print $1.unpack("m")[0]
+ jobs_status if @opts[:job_status_type] == :replace
+ when /^after (.+?)$/
+ @warnings << Marshal.load($1.unpack("m")[0])
+ when /^bye (.+?)$/ # Worker will shutdown
+ e = Marshal.load($1.unpack("m")[0])
+ after_worker_down a, e
+ when /^bye$/ # Worker will shutdown
+ if shutting_down
+ after_worker_dead a
+ else
+ after_worker_down a
+ end
+ end
+ break if @need_quit
+ end
+ end
rescue Interrupt => e
@interrupt = e
- break
+ return result
+ ensure
+ shutting_down = true
+ watchdog.kill if watchdog
+ @workers.each do |w|
+ begin
+ timeout(1) do
+ w[:in].puts "quit"
+ end
+ rescue Errno::EPIPE
+ rescue Timeout::Error
+ end
+ [:in,:out].each do |x|
+ w[x].close
+ end
+ end
+ begin
+ timeout(0.2*@workers.size) do
+ Process.waitall
+ end
+ rescue Timeout::Error
+ @workers.each do |w|
+ begin
+ Process.kill(:KILL,w[:pid])
+ rescue Errno::ESRCH; end
+ end
+ end
end
- }
+ else
+ suites.each {|suite|
+ begin
+ result << _run_suite(suite, type)
+ rescue Interrupt => e
+ @interrupt = e
+ break
+ end
+ }
+ end
result
end
diff --git lib/test/unit/parallel.rb lib/test/unit/parallel.rb
new file mode 100644
index 0000000..0757378
--- /dev/null
+++ lib/test/unit/parallel.rb
@@ -0,0 +1,152 @@
+require 'test/unit'
+
+module Test # :nodoc:
+ module Unit # :nodoc:
+ class TestCase # :nodoc:
+ class << self; alias orig_inherited inherited; end
+ def self.inherited x # :nodoc:
+ orig_inherited x
+ Test::Unit::Worker.suites << x
+ end
+ end
+ end
+end
+
+module Test
+ module Unit
+ class Worker < Runner
+ @@suites = []
+
+ class << self
+ def suites; @@suites; end
+ undef autorun
+ end
+
+ alias orig_run_suite _run_suite
+ undef _run_suite
+ undef _run_suites
+
+ def _run_suites suites, type
+ suites.map do |suite|
+ result = _run_suite(suite, type)
+ end
+ end
+
+ def _run_suite(suite, type)
+ r = report.dup
+ orig_stdout = MiniTest::Unit.output
+ i,o = IO.pipe
+ MiniTest::Unit.output = o
+
+ stdout = STDOUT.dup
+
+ th = Thread.new(i.dup) do |io|
+ begin
+ while buf = (self.verbose ? io.gets : io.read(5))
+ stdout.puts "p #{[buf].pack("m").gsub("\n","")}"
+ end
+ rescue IOError
+ rescue Errno::EPIPE
+ end
+ end
+
+ e, f, s = @errors, @failures, @skips
+
+ result = orig_run_suite(suite, type)
+
+ MiniTest::Unit.output = orig_stdout
+
+ o.close
+ i.close
+
+ begin
+ th.join
+ rescue IOError
+ raise unless ["stream closed","closed stream"].include? $!.message
+ end
+
+ result << (report - r)
+ result << [@errors-e,@failures-f,@skips-s]
+ result << ($: - @old_loadpath)
+
+ begin
+ STDOUT.puts "done #{[Marshal.dump(result)].pack("m").gsub("\n","")}"
+ rescue Errno::EPIPE; end
+ return result
+ ensure
+ MiniTest::Unit.output = orig_stdout
+ o.close unless o.closed?
+ i.close unless i.closed?
+ end
+
+ def run(args = [])
+ process_args args
+ @@stop_auto_run = true
+ @opts = @options.dup
+
+ STDOUT.sync = true
+ STDOUT.puts "ready"
+ Signal.trap(:INT,"IGNORE")
+
+
+ @old_loadpath = []
+ begin
+ while buf = STDIN.gets
+ case buf.chomp
+ when /^loadpath (.+?)$/
+ @old_loadpath = $:.dup
+ $:.push(*Marshal.load($1.unpack("m")[0].force_encoding("ASCII-8BIT"))).uniq!
+ when /^run (.+?) (.+?)$/
+ puts "okay"
+
+ stdin = STDIN.dup
+ stdout = STDOUT.dup
+ th = Thread.new do
+ while puf = stdin.gets
+ if puf.chomp == "quit"
+ begin
+ stdout.puts "bye"
+ rescue Errno::EPIPE; end
+ exit
+ end
+ end
+ end
+
+ @options = @opts.dup
+ @@suites = []
+
+ begin
+ require $1
+ rescue LoadError
+ STDOUT.puts "after #{[Marshal.dump([$1, $!])].pack("m").gsub("\n","")}"
+ STDOUT.puts "ready"
+ next
+ end
+ _run_suites @@suites, $2.to_sym
+
+ STDIN.reopen(stdin)
+ STDOUT.reopen(stdout)
+
+ th.kill
+ STDOUT.puts "ready"
+ when /^quit$/
+ begin
+ STDOUT.puts "bye"
+ rescue Errno::EPIPE; end
+ exit
+ end
+ end
+ rescue Exception => e
+ begin
+ STDOUT.puts "bye #{[Marshal.dump(e)].pack("m").gsub("\n","")}"
+ rescue Errno::EPIPE;end
+ exit
+ ensure
+ stdin.close
+ end
+ end
+ end
+ end
+end
+
+Test::Unit::Worker.new.run(ARGV)
@zdavatz

This comment has been minimized.

Copy link

commented Feb 20, 2011

Hi

Extremely nice! Congratulations!

Let me know if I can test something for you on Windows or Linux!

Best
Zeno

@sorah

This comment has been minimized.

Copy link
Owner Author

commented Feb 20, 2011

Hi,

Thank you for testing on my patch.

But this patch is old, pull github/sorah/ruby.git parallel_test branch

https://github.com/sorah/ruby/commits/parallel_test

Then build ruby, finally run

$ make TESTS='-j4 -v' test-all

-j4 means start 4 workers.

Thanks,
sora_h

@zdavatz

This comment has been minimized.

Copy link

commented Feb 21, 2011

Dear Sora-san

What version of Ruby should I apply the patch to? Can I just take the standard 1.9.2 version or should I git clone from somewhere?

Best
Zeno

@zdavatz

This comment has been minimized.

Copy link

commented Feb 21, 2011

Ok, I am doing

git clone https://github.com/sorah/ruby.git

autoconf
configure
make
make install (to /usr/local/bin)
now when I try to do
make TESTS='-j4 -v' test-all
I get
/home/zeno/.software/ruby/lib/test/unit.rb:48:in `process_args': invalid option: -j4 (OptionParser::InvalidOption)

    from /home/zeno/.software/ruby/lib/minitest/unit.rb:767:in `run'

    from /home/zeno/.software/ruby/lib/test/unit.rb:21:in `run'

    from /home/zeno/.software/ruby/lib/test/unit.rb:250:in `run'

    from /home/zeno/.software/ruby/lib/test/unit.rb:254:in `run'

    from ./test/runner.rb:10:in `<main>'

make: *** [yes-test-all] Fehler 1

Do I need to apply the patch separately?

Best
Zeno

@zdavatz

This comment has been minimized.

Copy link

commented Feb 21, 2011

This I can not pull:

git pull https://github.com/sorah/ruby/commits/parallel_test
fatal: https://github.com/sorah/ruby/commits/parallel_test/info/refs not found: did you run git update-server-info on the server?

Best
Zeno

@sorah

This comment has been minimized.

Copy link
Owner Author

commented Feb 21, 2011

this is branch.

google "git remote branch" at yourself.

@zdavatz

This comment has been minimized.

Copy link

commented Feb 21, 2011

I am trying to test this on Gentoo-Linux 2.6.38-rc5

@sorah

This comment has been minimized.

Copy link
Owner Author

commented Feb 21, 2011

Thanks. I'm looking forward your report.

@zdavatz

This comment has been minimized.

Copy link

commented Feb 21, 2011

Sorry, I do not understand. Can't I just git pull the patch or git clone the patch?

Best
Zeno

@zdavatz

This comment has been minimized.

Copy link

commented Feb 21, 2011

Ok, I done:

git checkout parallel_test
Switched to branch 'parallel_test'

then

make TESTS='-j4 -v' test-all

same result:

./miniruby -I./lib -I. -I.ext/common ./tool/runruby.rb --extout=.ext -- "./test/runner.rb" -j4 -v

/home/zeno/.software/ruby/lib/test/unit.rb:48:in `process_args': invalid option: -j4 (OptionParser::InvalidOption)

    from /home/zeno/.software/ruby/lib/minitest/unit.rb:767:in `run'

    from /home/zeno/.software/ruby/lib/test/unit.rb:21:in `run'

    from /home/zeno/.software/ruby/lib/test/unit.rb:250:in `run'

    from /home/zeno/.software/ruby/lib/test/unit.rb:254:in `run'

    from ./test/runner.rb:10:in `<main>'

make: *** [yes-test-all] Fehler 1

Something seems wrong.

Best
Zeno

@sorah

This comment has been minimized.

Copy link
Owner Author

commented Feb 21, 2011

Check lib/test/unit/parallel.rb exist (If not exist you're not checked out remote branch)

and run:

$ make main install-nodoc

Checkout remote branch

$ git fetch origin parallel_test
$ git checkout origin/parallel_test
@zdavatz

This comment has been minimized.

Copy link

commented Feb 21, 2011

Ah, ok, I needed to do a git fetch first, thanks for the hint!

make TESTS='-j4 -v' test-all

is running on my Gentoo now!

Best
Zeno

@zdavatz

This comment has been minimized.

Copy link

commented Feb 21, 2011

Tests run until here and then they stop running:

TestEncodingConverter#test_xml_escape_text = 0.01 s = .
TestEncodingConverter#test_xml_escape_with_charref = 0.00 s = .
TestEncodingConverter#test_xml_hasharg = 0.00 s = .

htop shows no more CPU usage.

I got an intel i7 CPU with 8 cores.

Shall I send you a file?

Best
Zeno

@zdavatz

This comment has been minimized.

Copy link

commented Feb 21, 2011

~/.software/ruby> make TESTS='-j4 -v' test-all >> sora_h_test

DB->del: attempt to modify a read-only database

DB->put: attempt to modify a read-only database

mkdir -p testdata/flatten

mkdir -p testdata/src/jw

Result:

http://dev.ywesee.com/uploads/$PageName//sora_h_test

Best
Zeno

@sorah

This comment has been minimized.

Copy link
Owner Author

commented Feb 21, 2011

Yeah, thank you for reporting ;)

Some tests don't work well when parallel running, so I'm trying to fix those :)

Please looking forward for fixing.

@zdavatz

This comment has been minimized.

Copy link

commented Feb 21, 2011

Thank you Sorah-sensei!

Let me know anytime when I should test some more!

Best
Zeno

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.