ruby-changes:71092
From: Koichi <ko1@a...>
Date: Sun, 6 Feb 2022 03:06:14 +0900 (JST)
Subject: [ruby-changes:71092] 603ab70961 (master): support concurrent btest execution
https://git.ruby-lang.org/ruby.git/commit/?id=603ab70961 From 603ab709615dd35fa8ebe53087c27631f5b07812 Mon Sep 17 00:00:00 2001 From: Koichi Sasada <ko1@a...> Date: Sat, 5 Feb 2022 03:10:15 +0900 Subject: support concurrent btest execution * `-j` option for concurrent test with threads * `-jN` uses N threads * `-j` uses nproc/2 threads * Introduce `BT` struct to manage configurations * Introduce `Assertion` to manage all assertions * Remove all toplevel instance variables * Show elapsed seconds at last ``` $ time make btest ... real 0m37.319s user 0m26.221s sys 0m16.534s $ time make btest TESTOPTS=-j ... real 0m11.812s user 0m36.667s sys 0m21.872s ``` --- bootstraptest/runner.rb | 763 +++++++++++++++++++++++++---------------- bootstraptest/test_autoload.rb | 30 +- bootstraptest/test_fiber.rb | 2 +- bootstraptest/test_method.rb | 2 +- bootstraptest/test_thread.rb | 8 +- 5 files changed, 480 insertions(+), 325 deletions(-) diff --git a/bootstraptest/runner.rb b/bootstraptest/runner.rb index a4101594f1..1be0d677c0 100755 --- a/bootstraptest/runner.rb +++ b/bootstraptest/runner.rb @@ -8,6 +8,8 @@ https://github.com/ruby/ruby/blob/trunk/bootstraptest/runner.rb#L8 # Never use Ruby extensions in this file. # Maintain Ruby 1.8 compatibility for now +$start_time = Time.now + begin require 'fileutils' require 'tmpdir' @@ -58,24 +60,44 @@ if !Dir.respond_to?(:mktmpdir) https://github.com/ruby/ruby/blob/trunk/bootstraptest/runner.rb#L60 end end +# Configuration +BT = Struct.new(:ruby, + :verbose, + :color, + :tty, + :quiet, + :wn, + :progress, + :progress_bs, + :passed, + :failed, + :reset, + :columns, + :width, + ).new + +BT_STATE = Struct.new(:count, :error).new + def main - @ruby = File.expand_path('miniruby') - @verbose = false + BT.ruby = File.expand_path('miniruby') + BT.verbose = false $VERBOSE = false $stress = false - @color = nil - @tty = nil - @quiet = false + BT.color = nil + BT.tty = nil + BT.quiet = false + BT.wn = 1 dir = nil quiet = false tests = nil ARGV.delete_if {|arg| case arg when /\A--ruby=(.*)/ - @ruby = $1 - @ruby.gsub!(/^([^ ]*)/){File.expand_path($1)} - @ruby.gsub!(/(\s+-I\s*)((?!(?:\.\/)*-(?:\s|\z))\S+)/){$1+File.expand_path($2)} - @ruby.gsub!(/(\s+-r\s*)(\.\.?\/\S+)/){$1+File.expand_path($2)} + ruby = $1 + ruby.gsub!(/^([^ ]*)/){File.expand_path($1)} + ruby.gsub!(/(\s+-I\s*)((?!(?:\.\/)*-(?:\s|\z))\S+)/){$1+File.expand_path($2)} + ruby.gsub!(/(\s+-r\s*)(\.\.?\/\S+)/){$1+File.expand_path($2)} + BT.ruby = ruby true when /\A--sets=(.*)/ tests = Dir.glob("#{File.dirname($0)}/test_{#{$1}}*.rb").sort @@ -88,18 +110,26 @@ def main https://github.com/ruby/ruby/blob/trunk/bootstraptest/runner.rb#L110 $stress = true when /\A--color(?:=(?:always|(auto)|(never)|(.*)))?\z/ warn "unknown --color argument: #$3" if $3 - @color = $1 ? nil : !$2 + BT.color = color = $1 ? nil : !$2 true when /\A--tty(=(?:yes|(no)|(.*)))?\z/ warn "unknown --tty argument: #$3" if $3 - @tty = !$1 || !$2 + BT.tty = !$1 || !$2 true when /\A(-q|--q(uiet))\z/ quiet = true - @quiet = true + BT.quiet = true + true + when /\A-j(\d+)?/ + wn = $1.to_i + if wn <= 0 + require 'etc' + wn = [Etc.nprocessors / 2, 1].max + end + BT.wn = wn true when /\A(-v|--v(erbose))\z/ - @verbose = true + BT.verbose = true when /\A(-h|--h(elp)?)\z/ puts(<<-End) Usage: #{File.basename($0, '.*')} --ruby=PATH [--sets=NAME,NAME,...] @@ -128,15 +158,16 @@ End https://github.com/ruby/ruby/blob/trunk/bootstraptest/runner.rb#L158 tests = Dir.glob("#{File.dirname($0)}/test_*.rb").sort if tests.empty? pathes = tests.map {|path| File.expand_path(path) } - @progress = %w[- \\ | /] - @progress_bs = "\b" * @progress[0].size - @tty = $stderr.tty? if @tty.nil? - case @color + BT.progress = %w[- \\ | /] + BT.progress_bs = "\b" * BT.progress[0].size + BT.tty = $stderr.tty? if BT.tty.nil? + + case BT.color when nil - @color = @tty && /dumb/ !~ ENV["TERM"] + BT.color = BT.tty && /dumb/ !~ ENV["TERM"] end - @tty &&= !@verbose - if @color + BT.tty &&= !BT.verbose + if BT.color # dircolors-like style colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {} begin @@ -145,14 +176,14 @@ End https://github.com/ruby/ruby/blob/trunk/bootstraptest/runner.rb#L176 end rescue end - @passed = "\e[;#{colors["pass"] || "32"}m" - @failed = "\e[;#{colors["fail"] || "31"}m" - @reset = "\e[m" + BT.passed = "\e[;#{colors["pass"] || "32"}m" + BT.failed = "\e[;#{colors["fail"] || "31"}m" + BT.reset = "\e[m" else - @passed = @failed = @reset = "" + BT.passed = BT.failed = BT.reset = "" end unless quiet - puts Time.now + puts $start_time if defined?(RUBY_DESCRIPTION) puts "Driver is #{RUBY_DESCRIPTION}" elsif defined?(RUBY_PATCHLEVEL) @@ -160,289 +191,473 @@ End https://github.com/ruby/ruby/blob/trunk/bootstraptest/runner.rb#L191 else puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]" end - puts "Target is #{`#{@ruby} -v`.chomp}" + puts "Target is #{`#{BT.ruby} -v`.chomp}" puts $stdout.flush end - in_temporary_working_directory(dir) { + in_temporary_working_directory(dir) do exec_test pathes - } + end end def erase(e = true) - if e and @columns > 0 and @tty and !@verbose + if e and BT.columns > 0 and BT.tty and !BT.verbose "\e[1K\r" else "" end end -def exec_test(pathes) - @count = 0 - @error = 0 - @errbuf = [] - @location = nil - @columns = 0 - @width = pathes.map {|path| File.basename(path).size}.max + 2 +def load_test pathes pathes.each do |path| - @basename = File.basename(path) - unless @quiet - $stderr.printf("%s%-*s ", erase(@quiet), @width, @basename) - $stderr.flush - end - @columns = @width + 1 - $stderr.puts if @verbose - count = @count - error = @error load File.expand_path(path) - if @tty - if @error == error - msg = "PASS #{@count-count}" - @columns += msg.size - 1 - $stderr.print "#{@progress_bs}#{@passed}#{msg}#{@reset}" unless @quiet + end +end + +def concurrent_exec_test + aq = Queue.new + rq = Queue.new + + ts = BT.wn.times.map do + Thread.new do + while as = aq.pop + as.call + rq << as + end + ensure + rq << nil + end + end + + Assertion.all.to_a.shuffle.each do |path, assertions| + assertions.each do |as| + aq << as + end + end + + $stderr.print ' ' unless BT.quiet + aq.close + i = 1 + term_wn = 0 + begin + while BT.wn != term_wn + if r = rq.pop + case + when BT.quiet + when BT.tty + $stderr.print "#{BT.progress_bs}#{BT.progress[(i+=1) % BT.progress.size]}" + else + $stderr.print '.' + end else - msg = "FAIL #{@error-error}/#{@count-count}" - $stderr.print "#{@progress_bs}#{@failed}#{msg}#{@reset}" - @columns = 0 + term_wn += 1 + end + end + ensure + ts.each(&:kill) + ts.each(&:join) + end +end + +def exec_test(pathes) + # setup + load_test pathes + BT_STATE.count = 0 + BT_STATE.error = 0 + BT.columns = 0 + BT.width = pathes.map {|path| File.basename(path).size}.max + 2 + + # execute tests + if BT.wn > 1 + concurrent_exec_test if BT.wn > 1 + else + prev_basename = nil + Assertion.all.each do |basename, assertions| + if !BT.quiet && basename != prev_basename + prev_basename = basename + $stderr.printf("%s%-*s ", erase(BT.quiet), BT.width, basename) + $stderr.flush + end + BT.columns = BT.width + 1 + $stderr.puts if BT.verbose + count = BT_STATE.count + error = BT_STATE.error + + assertions.each do |assertion| + BT_STATE.count += 1 + assertion.call + end + + if BT.tty + if BT_STATE.error == error + msg = "PASS #{BT_STATE.count-count}" + BT.columns += msg.size - 1 + $stderr.print "#{BT.progress_bs}#{BT.passed}#{msg}#{BT.reset}" unless BT.quiet + else + msg = "FAIL #{BT_STATE.error-error}/#{BT_STATE.count-count}" + $stderr.print "#{BT.progress_bs}#{BT.failed}#{msg}#{BT.reset}" + BT.columns = 0 + end end + $stderr.puts if !BT.quiet and (BT.tty or BT_STATE.error == error) end - $stderr.puts if !@quiet and (@tty or @error == error) end - $stderr.print(erase) if @quiet - @errbuf.each do |msg| + + # show results + unless BT.quiet + $stderr.puts(erase) + + sec = Time.now - $start_time + $stderr.puts "Finished in #{'%.2f' % sec} sec\n\n" if Assertion.count > 0 + end + + Assertion.errbuf.each do |msg| $stderr.puts msg end - out = @quiet ? $stdout : $stderr + out = BT.quiet ? $stdout : $stderr - if @error == 0 - if @count == 0 - out.puts "No tests, no problem" unless @quiet + if BT_STATE.error == 0 + if Assertion.count == 0 + out.puts "No tests, no problem" unless BT.quiet else - out.puts "#{@passed}PASS#{@reset} all #{@count} tests" + out.puts "#{BT.passed}PASS#{BT.reset} all #{Assertion.count} tests" end - exit true + true else - $stderr.puts "#{@failed}FAIL#{@reset} #{@error}/#{@count} tests failed" - exit false + $stderr.puts "#{BT.failed}FAIL#{BT.reset} #{BT_STATE.error}/#{BT_STATE.count} tests failed" + false end end -def show_progress(message = '') - if @quiet - # do nothing - elsif @verbose - $stderr.print "\##{@count} #{@location} " - e (... truncated) -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/