1"exec" "${RUBY-ruby}" "-x" "$0" "$@" || true # -*- mode: ruby; coding: utf-8 -*- 2#!./ruby 3# $Id: runner.rb 66344 2018-12-12 00:38:49Z k0kubun $ 4 5# NOTE: 6# Never use optparse in this file. 7# Never use test/unit in this file. 8# Never use Ruby extensions in this file. 9# Maintain Ruby 1.8 compatibility for now 10 11begin 12 require 'fileutils' 13 require 'tmpdir' 14rescue LoadError 15 $:.unshift File.join(File.dirname(__FILE__), '../lib') 16 retry 17end 18 19if !Dir.respond_to?(:mktmpdir) 20 # copied from lib/tmpdir.rb 21 def Dir.mktmpdir(prefix_suffix=nil, tmpdir=nil) 22 case prefix_suffix 23 when nil 24 prefix = "d" 25 suffix = "" 26 when String 27 prefix = prefix_suffix 28 suffix = "" 29 when Array 30 prefix = prefix_suffix[0] 31 suffix = prefix_suffix[1] 32 else 33 raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}" 34 end 35 tmpdir ||= Dir.tmpdir 36 t = Time.now.strftime("%Y%m%d") 37 n = nil 38 begin 39 path = "#{tmpdir}/#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}" 40 path << "-#{n}" if n 41 path << suffix 42 Dir.mkdir(path, 0700) 43 rescue Errno::EEXIST 44 n ||= 0 45 n += 1 46 retry 47 end 48 49 if block_given? 50 begin 51 yield path 52 ensure 53 FileUtils.remove_entry_secure path 54 end 55 else 56 path 57 end 58 end 59end 60 61def main 62 @ruby = File.expand_path('miniruby') 63 @verbose = false 64 $VERBOSE = false 65 $stress = false 66 @color = nil 67 @tty = nil 68 @quiet = false 69 dir = nil 70 quiet = false 71 tests = nil 72 ARGV.delete_if {|arg| 73 case arg 74 when /\A--ruby=(.*)/ 75 @ruby = $1 76 @ruby.gsub!(/^([^ ]*)/){File.expand_path($1)} 77 @ruby.gsub!(/(\s+-I\s*)((?!(?:\.\/)*-(?:\s|\z))\S+)/){$1+File.expand_path($2)} 78 @ruby.gsub!(/(\s+-r\s*)(\.\.?\/\S+)/){$1+File.expand_path($2)} 79 true 80 when /\A--sets=(.*)/ 81 tests = Dir.glob("#{File.dirname($0)}/test_{#{$1}}*.rb").sort 82 puts tests.map {|path| File.basename(path) }.inspect 83 true 84 when /\A--dir=(.*)/ 85 dir = $1 86 true 87 when /\A(--stress|-s)/ 88 $stress = true 89 when /\A--color(?:=(?:always|(auto)|(never)|(.*)))?\z/ 90 warn "unknown --color argument: #$3" if $3 91 @color = $1 ? nil : !$2 92 true 93 when /\A--tty(=(?:yes|(no)|(.*)))?\z/ 94 warn "unknown --tty argument: #$3" if $3 95 @tty = !$1 || !$2 96 true 97 when /\A(-q|--q(uiet))\z/ 98 quiet = true 99 @quiet = true 100 true 101 when /\A(-v|--v(erbose))\z/ 102 @verbose = true 103 when /\A(-h|--h(elp)?)\z/ 104 puts(<<-End) 105Usage: #{File.basename($0, '.*')} --ruby=PATH [--sets=NAME,NAME,...] 106 --sets=NAME,NAME,... Name of test sets. 107 --dir=DIRECTORY Working directory. 108 default: /tmp/bootstraptestXXXXX.tmpwd 109 --color[=WHEN] Colorize the output. WHEN defaults to 'always' 110 or can be 'never' or 'auto'. 111 -s, --stress stress test. 112 -v, --verbose Output test name before exec. 113 -q, --quiet Don\'t print header message. 114 -h, --help Print this message and quit. 115End 116 exit true 117 when /\A-j/ 118 true 119 else 120 false 121 end 122 } 123 if tests and not ARGV.empty? 124 $stderr.puts "--tests and arguments are exclusive" 125 exit false 126 end 127 tests ||= ARGV 128 tests = Dir.glob("#{File.dirname($0)}/test_*.rb").sort if tests.empty? 129 pathes = tests.map {|path| File.expand_path(path) } 130 131 @progress = %w[- \\ | /] 132 @progress_bs = "\b" * @progress[0].size 133 @tty = $stderr.tty? if @tty.nil? 134 case @color 135 when nil 136 @color = @tty && /dumb/ !~ ENV["TERM"] 137 end 138 @tty &&= !@verbose 139 if @color 140 # dircolors-like style 141 colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {} 142 begin 143 File.read(File.join(__dir__, "../test/colors")).scan(/(\w+)=([^:\n]*)/) do |n, c| 144 colors[n] ||= c 145 end 146 rescue 147 end 148 @passed = "\e[;#{colors["pass"] || "32"}m" 149 @failed = "\e[;#{colors["fail"] || "31"}m" 150 @reset = "\e[m" 151 else 152 @passed = @failed = @reset = "" 153 end 154 unless quiet 155 puts Time.now 156 if defined?(RUBY_DESCRIPTION) 157 puts "Driver is #{RUBY_DESCRIPTION}" 158 elsif defined?(RUBY_PATCHLEVEL) 159 puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}#{RUBY_PLATFORM}) [#{RUBY_PLATFORM}]" 160 else 161 puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]" 162 end 163 puts "Target is #{`#{@ruby} -v`.chomp}" 164 puts 165 $stdout.flush 166 end 167 168 in_temporary_working_directory(dir) { 169 exec_test pathes 170 } 171end 172 173def erase(e = true) 174 if e and @columns > 0 and !@verbose 175 "\r#{" "*@columns}\r" 176 else 177 "" 178 end 179end 180 181def exec_test(pathes) 182 @count = 0 183 @error = 0 184 @errbuf = [] 185 @location = nil 186 @columns = 0 187 @width = pathes.map {|path| File.basename(path).size}.max + 2 188 pathes.each do |path| 189 @basename = File.basename(path) 190 $stderr.printf("%s%-*s ", erase(@quiet), @width, @basename) 191 $stderr.flush 192 @columns = @width + 1 193 $stderr.puts if @verbose 194 count = @count 195 error = @error 196 load File.expand_path(path) 197 if @tty 198 if @error == error 199 msg = "PASS #{@count-count}" 200 @columns += msg.size - 1 201 $stderr.print "#{@progress_bs}#{@passed}#{msg}#{@reset}" 202 else 203 msg = "FAIL #{@error-error}/#{@count-count}" 204 $stderr.print "#{@progress_bs}#{@failed}#{msg}#{@reset}" 205 @columns = 0 206 end 207 end 208 $stderr.puts unless @quiet and @tty and @error == error 209 end 210 $stderr.print(erase) if @quiet 211 if @error == 0 212 if @count == 0 213 $stderr.puts "No tests, no problem" 214 else 215 $stderr.puts "#{@passed}PASS#{@reset} all #{@count} tests" 216 end 217 exit true 218 else 219 @errbuf.each do |msg| 220 $stderr.puts msg 221 end 222 $stderr.puts "#{@failed}FAIL#{@reset} #{@error}/#{@count} tests failed" 223 exit false 224 end 225end 226 227def show_progress(message = '') 228 if @verbose 229 $stderr.print "\##{@count} #{@location} " 230 elsif @tty 231 $stderr.print "#{@progress_bs}#{@progress[@count % @progress.size]}" 232 end 233 t = Time.now if @verbose 234 faildesc, errout = with_stderr {yield} 235 t = Time.now - t if @verbose 236 if !faildesc 237 if @tty 238 $stderr.print "#{@progress_bs}#{@progress[@count % @progress.size]}" 239 elsif @verbose 240 $stderr.printf(". %.3f\n", t) 241 else 242 $stderr.print '.' 243 end 244 else 245 $stderr.print "#{@failed}F" 246 $stderr.printf(" %.3f", t) if @verbose 247 $stderr.print "#{@reset}" 248 $stderr.puts if @verbose 249 error faildesc, message 250 unless errout.empty? 251 $stderr.print "#{@failed}stderr output is not empty#{@reset}\n", adjust_indent(errout) 252 end 253 if @tty and !@verbose 254 $stderr.printf("%-*s%s", @width, @basename, @progress[@count % @progress.size]) 255 end 256 end 257rescue Interrupt 258 $stderr.puts "\##{@count} #{@location}" 259 raise 260rescue Exception => err 261 $stderr.print 'E' 262 $stderr.puts if @verbose 263 error err.message, message 264end 265 266def assert_check(testsrc, message = '', opt = '', **argh) 267 show_progress(message) { 268 result = get_result_string(testsrc, opt, **argh) 269 check_coredump 270 yield(result) 271 } 272end 273 274def assert_equal(expected, testsrc, message = '', opt = '', **argh) 275 newtest 276 assert_check(testsrc, message, opt, **argh) {|result| 277 if expected == result 278 nil 279 else 280 desc = "#{result.inspect} (expected #{expected.inspect})" 281 pretty(testsrc, desc, result) 282 end 283 } 284end 285 286def assert_match(expected_pattern, testsrc, message = '') 287 newtest 288 assert_check(testsrc, message) {|result| 289 if expected_pattern =~ result 290 nil 291 else 292 desc = "#{expected_pattern.inspect} expected to be =~\n#{result.inspect}" 293 pretty(testsrc, desc, result) 294 end 295 } 296end 297 298def assert_not_match(unexpected_pattern, testsrc, message = '') 299 newtest 300 assert_check(testsrc, message) {|result| 301 if unexpected_pattern !~ result 302 nil 303 else 304 desc = "#{unexpected_pattern.inspect} expected to be !~\n#{result.inspect}" 305 pretty(testsrc, desc, result) 306 end 307 } 308end 309 310def assert_valid_syntax(testsrc, message = '') 311 newtest 312 assert_check(testsrc, message, '-c') {|result| 313 result if /Syntax OK/ !~ result 314 } 315end 316 317def assert_normal_exit(testsrc, *rest, timeout: nil, **opt) 318 newtest 319 message, ignore_signals = rest 320 message ||= '' 321 show_progress(message) { 322 faildesc = nil 323 filename = make_srcfile(testsrc) 324 old_stderr = $stderr.dup 325 timeout_signaled = false 326 begin 327 $stderr.reopen("assert_normal_exit.log", "w") 328 io = IO.popen("#{@ruby} -W0 #{filename}") 329 pid = io.pid 330 th = Thread.new { 331 io.read 332 io.close 333 $? 334 } 335 if !th.join(timeout) 336 Process.kill :KILL, pid 337 timeout_signaled = true 338 end 339 status = th.value 340 ensure 341 $stderr.reopen(old_stderr) 342 old_stderr.close 343 end 344 if status && status.signaled? 345 signo = status.termsig 346 signame = Signal.list.invert[signo] 347 unless ignore_signals and ignore_signals.include?(signame) 348 sigdesc = "signal #{signo}" 349 if signame 350 sigdesc = "SIG#{signame} (#{sigdesc})" 351 end 352 if timeout_signaled 353 sigdesc << " (timeout)" 354 end 355 faildesc = pretty(testsrc, "killed by #{sigdesc}", nil) 356 stderr_log = File.read("assert_normal_exit.log") 357 if !stderr_log.empty? 358 faildesc << "\n" if /\n\z/ !~ faildesc 359 stderr_log << "\n" if /\n\z/ !~ stderr_log 360 stderr_log.gsub!(/^.*\n/) { '| ' + $& } 361 faildesc << stderr_log 362 end 363 end 364 end 365 faildesc 366 } 367end 368 369def assert_finish(timeout_seconds, testsrc, message = '') 370 timeout_seconds *= 3 if RubyVM::MJIT.enabled? # for --jit-wait 371 newtest 372 show_progress(message) { 373 faildesc = nil 374 filename = make_srcfile(testsrc) 375 io = IO.popen("#{@ruby} -W0 #{filename}") 376 pid = io.pid 377 waited = false 378 tlimit = Time.now + timeout_seconds 379 diff = timeout_seconds 380 while diff > 0 381 if Process.waitpid pid, Process::WNOHANG 382 waited = true 383 break 384 end 385 if io.respond_to?(:read_nonblock) 386 if IO.select([io], nil, nil, diff) 387 begin 388 io.read_nonblock(1024) 389 rescue Errno::EAGAIN, IO::WaitReadable, EOFError 390 break 391 end while true 392 end 393 else 394 sleep 0.1 395 end 396 diff = tlimit - Time.now 397 end 398 if !waited 399 Process.kill(:KILL, pid) 400 Process.waitpid pid 401 faildesc = pretty(testsrc, "not finished in #{timeout_seconds} seconds", nil) 402 end 403 io.close 404 faildesc 405 } 406end 407 408def flunk(message = '') 409 newtest 410 show_progress('') { message } 411end 412 413def pretty(src, desc, result) 414 src = src.sub(/\A\s*\n/, '') 415 (/\n/ =~ src ? "\n#{adjust_indent(src)}" : src) + " #=> #{desc}" 416end 417 418INDENT = 27 419 420def adjust_indent(src) 421 untabify(src).gsub(/^ {#{INDENT}}/o, '').gsub(/^/, ' ').sub(/\s*\z/, "\n") 422end 423 424def untabify(str) 425 str.gsub(/^\t+/) {' ' * (8 * $&.size) } 426end 427 428def make_srcfile(src, frozen_string_literal: nil) 429 filename = 'bootstraptest.tmp.rb' 430 File.open(filename, 'w') {|f| 431 f.puts "#frozen_string_literal:true" if frozen_string_literal 432 f.puts "GC.stress = true" if $stress 433 f.puts "print(begin; #{src}; end)" 434 } 435 filename 436end 437 438def get_result_string(src, opt = '', **argh) 439 if @ruby 440 filename = make_srcfile(src, **argh) 441 begin 442 `#{@ruby} -W0 #{opt} #{filename}` 443 ensure 444 raise Interrupt if $? and $?.signaled? && $?.termsig == Signal.list["INT"] 445 raise CoreDumpError, "core dumped" if $? and $?.coredump? 446 end 447 else 448 eval(src).to_s 449 end 450end 451 452def with_stderr 453 out = err = nil 454 begin 455 r, w = IO.pipe 456 stderr = $stderr.dup 457 $stderr.reopen(w) 458 w.close 459 reader = Thread.start {r.read} 460 begin 461 out = yield 462 ensure 463 $stderr.reopen(stderr) 464 err = reader.value 465 end 466 ensure 467 w.close rescue nil 468 r.close rescue nil 469 end 470 return out, err 471end 472 473def newtest 474 @location = File.basename(caller(2).first) 475 @count += 1 476 cleanup_coredump 477end 478 479def error(msg, additional_message) 480 msg = "#{@failed}\##{@count} #{@location}#{@reset}: #{msg} #{additional_message}" 481 if @tty 482 $stderr.puts "#{erase}#{msg}" 483 else 484 @errbuf.push msg 485 end 486 @error += 1 487end 488 489def in_temporary_working_directory(dir) 490 if dir 491 Dir.mkdir dir 492 Dir.chdir(dir) { 493 yield 494 } 495 else 496 Dir.mktmpdir(["bootstraptest", ".tmpwd"]) {|d| 497 Dir.chdir(d) { 498 yield 499 } 500 } 501 end 502end 503 504def cleanup_coredump 505 FileUtils.rm_f 'core' 506 FileUtils.rm_f Dir.glob('core.*') 507 FileUtils.rm_f @ruby+'.stackdump' if @ruby 508end 509 510class CoreDumpError < StandardError; end 511 512def check_coredump 513 if File.file?('core') or not Dir.glob('core.*').empty? or 514 (@ruby and File.exist?(@ruby+'.stackdump')) 515 raise CoreDumpError, "core dumped" 516 end 517end 518 519main 520