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