"exec" "${RUBY-ruby}" "-x" "$0" "$@" || true # -*- Ruby -*-
#!./ruby
# $Id$

# NOTE:
# Never use optparse in this file.
# Never use test/unit in this file.
# Never use Ruby extensions in this file.
# Maintain Ruby 1.8 compatibility for now

begin
  require 'fileutils'
  require 'tmpdir'
rescue LoadError
  $:.unshift File.join(File.dirname(__FILE__), '../lib')
  retry
end

if !Dir.respond_to?(:mktmpdir)
  # copied from lib/tmpdir.rb
  def Dir.mktmpdir(prefix_suffix=nil, tmpdir=nil)
    case prefix_suffix
    when nil
      prefix = "d"
      suffix = ""
    when String
      prefix = prefix_suffix
      suffix = ""
    when Array
      prefix = prefix_suffix[0]
      suffix = prefix_suffix[1]
    else
      raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}"
    end
    tmpdir ||= Dir.tmpdir
    t = Time.now.strftime("%Y%m%d")
    n = nil
    begin
      path = "#{tmpdir}/#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}"
      path << "-#{n}" if n
      path << suffix
      Dir.mkdir(path, 0700)
    rescue Errno::EEXIST
      n ||= 0
      n += 1
      retry
    end

    if block_given?
      begin
        yield path
      ensure
        FileUtils.remove_entry_secure path
      end
    else
      path
    end
  end
end

def main
  @ruby = File.expand_path('miniruby')
  @verbose = false
  $VERBOSE = false
  $stress = false
  @color = nil
  @tty = nil
  @quiet = false
  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)}
      true
    when /\A--sets=(.*)/
      tests = Dir.glob("#{File.dirname($0)}/test_{#{$1}}*.rb").sort
      puts tests.map {|path| File.basename(path) }.inspect
      true
    when /\A--dir=(.*)/
      dir = $1
      true
    when /\A(--stress|-s)/
      $stress = true
    when /\A--color(?:=(?:always|(auto)|(never)|(.*)))?\z/
      warn "unknown --color argument: #$3" if $3
      @color = $1 ? nil : !$2
      true
    when /\A--tty(=(?:yes|(no)|(.*)))?\z/
      warn "unknown --tty argument: #$3" if $3
      @tty = !$1 || !$2
      true
    when /\A(-q|--q(uiet))\z/
      quiet = true
      @quiet = true
      true
    when /\A(-v|--v(erbose))\z/
      @verbose = true
    when /\A(-h|--h(elp)?)\z/
      puts(<<-End)
Usage: #{File.basename($0, '.*')} --ruby=PATH [--sets=NAME,NAME,...]
        --sets=NAME,NAME,...        Name of test sets.
        --dir=DIRECTORY             Working directory.
                                    default: /tmp/bootstraptestXXXXX.tmpwd
        --color[=WHEN]              Colorize the output.  WHEN defaults to 'always'
                                    or can be 'never' or 'auto'.
    -s, --stress                    stress test.
    -v, --verbose                   Output test name before exec.
    -q, --quiet                     Don\'t print header message.
    -h, --help                      Print this message and quit.
End
      exit true
    when /\A-j/
      true
    else
      false
    end
  }
  if tests and not ARGV.empty?
    $stderr.puts "--tests and arguments are exclusive"
    exit false
  end
  tests ||= ARGV
  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
  when nil
    @color = @tty && /dumb/ !~ ENV["TERM"]
  end
  @tty &&= !@verbose
  if @color
    # dircolors-like style
    colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {}
    begin
      File.read(File.join(__dir__, "../tool/colors")).scan(/(\w+)=([^:\n]*)/) do |n, c|
        colors[n] ||= c
      end
    rescue
    end
    @passed = "\e[;#{colors["pass"] || "32"}m"
    @failed = "\e[;#{colors["fail"] || "31"}m"
    @reset = "\e[m"
  else
    @passed = @failed = @reset = ""
  end
  unless quiet
    puts Time.now
    if defined?(RUBY_DESCRIPTION)
      puts "Driver is #{RUBY_DESCRIPTION}"
    elsif defined?(RUBY_PATCHLEVEL)
      puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}#{RUBY_PLATFORM}) [#{RUBY_PLATFORM}]"
    else
      puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
    end
    puts "Target is #{`#{@ruby} -v`.chomp}"
    puts
    $stdout.flush
  end

  in_temporary_working_directory(dir) {
    exec_test pathes
  }
end

def erase(e = true)
  if e and @columns > 0 and @tty and !@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
  pathes.each do |path|
    @basename = File.basename(path)
    $stderr.printf("%s%-*s ", erase(@quiet), @width, @basename)
    $stderr.flush
    @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}"
      else
        msg = "FAIL #{@error-error}/#{@count-count}"
        $stderr.print "#{@progress_bs}#{@failed}#{msg}#{@reset}"
        @columns = 0
      end
    end
    $stderr.puts unless @quiet and @tty and @error == error
  end
  $stderr.print(erase) if @quiet
  @errbuf.each do |msg|
    $stderr.puts msg
  end
  if @error == 0
    if @count == 0
      $stderr.puts "No tests, no problem"
    else
      $stderr.puts "#{@passed}PASS#{@reset} all #{@count} tests"
    end
    exit true
  else
    $stderr.puts "#{@failed}FAIL#{@reset} #{@error}/#{@count} tests failed"
    exit false
  end
end

def show_progress(message = '')
  if @verbose
    $stderr.print "\##{@count} #{@location} "
  elsif @tty
    $stderr.print "#{@progress_bs}#{@progress[@count % @progress.size]}"
  end
  t = Time.now if @verbose
  faildesc, errout = with_stderr {yield}
  t = Time.now - t if @verbose
  if !faildesc
    if @tty
      $stderr.print "#{@progress_bs}#{@progress[@count % @progress.size]}"
    elsif @verbose
      $stderr.printf(". %.3f\n", t)
    else
      $stderr.print '.'
    end
  else
    $stderr.print "#{@failed}F"
    $stderr.printf(" %.3f", t) if @verbose
    $stderr.print @reset
    $stderr.puts if @verbose
    error faildesc, message
    unless errout.empty?
      $stderr.print "#{@failed}stderr output is not empty#{@reset}\n", adjust_indent(errout)
    end
    if @tty and !@verbose
      $stderr.printf("%-*s%s", @width, @basename, @progress[@count % @progress.size])
    end
  end
rescue Interrupt
  $stderr.puts "\##{@count} #{@location}"
  raise
rescue Exception => err
  $stderr.print 'E'
  $stderr.puts if @verbose
  error err.message, message
ensure
  begin
    check_coredump
  rescue CoreDumpError => err
    $stderr.print 'E'
    $stderr.puts if @verbose
    error err.message, message
  end
end

def show_limit(testsrc, opt = '', **argh)
  result = get_result_string(testsrc, opt, **argh)
  if @tty and @verbose
    $stderr.puts ".{#@reset}\n#{erase}#{result}"
  else
    @errbuf.push result
  end
end

def assert_check(testsrc, message = '', opt = '', **argh)
  show_progress(message) {
    result = get_result_string(testsrc, opt, **argh)
    yield(result)
  }
end

def assert_equal(expected, testsrc, message = '', opt = '', **argh)
  newtest
  assert_check(testsrc, message, opt, **argh) {|result|
    if expected == result
      nil
    else
      desc = "#{result.inspect} (expected #{expected.inspect})"
      pretty(testsrc, desc, result)
    end
  }
end

def assert_match(expected_pattern, testsrc, message = '')
  newtest
  assert_check(testsrc, message) {|result|
    if expected_pattern =~ result
      nil
    else
      desc = "#{expected_pattern.inspect} expected to be =~\n#{result.inspect}"
      pretty(testsrc, desc, result)
    end
  }
end

def assert_not_match(unexpected_pattern, testsrc, message = '')
  newtest
  assert_check(testsrc, message) {|result|
    if unexpected_pattern !~ result
      nil
    else
      desc = "#{unexpected_pattern.inspect} expected to be !~\n#{result.inspect}"
      pretty(testsrc, desc, result)
    end
  }
end

def assert_valid_syntax(testsrc, message = '')
  newtest
  assert_check(testsrc, message, '-c') {|result|
    result if /Syntax OK/ !~ result
  }
end

def assert_normal_exit(testsrc, *rest, timeout: nil, **opt)
  newtest
  message, ignore_signals = rest
  message ||= ''
  show_progress(message) {
    faildesc = nil
    filename = make_srcfile(testsrc)
    old_stderr = $stderr.dup
    timeout_signaled = false
    begin
      $stderr.reopen("assert_normal_exit.log", "w")
      io = IO.popen("#{@ruby} -W0 #{filename}")
      pid = io.pid
      th = Thread.new {
        io.read
        io.close
        $?
      }
      if !th.join(timeout)
        Process.kill :KILL, pid
        timeout_signaled = true
      end
      status = th.value
    ensure
      $stderr.reopen(old_stderr)
      old_stderr.close
    end
    if status && status.signaled?
      signo = status.termsig
      signame = Signal.list.invert[signo]
      unless ignore_signals and ignore_signals.include?(signame)
        sigdesc = "signal #{signo}"
        if signame
          sigdesc = "SIG#{signame} (#{sigdesc})"
        end
        if timeout_signaled
          sigdesc << " (timeout)"
        end
        faildesc = pretty(testsrc, "killed by #{sigdesc}", nil)
        stderr_log = File.read("assert_normal_exit.log")
        if !stderr_log.empty?
          faildesc << "\n" if /\n\z/ !~ faildesc
          stderr_log << "\n" if /\n\z/ !~ stderr_log
          stderr_log.gsub!(/^.*\n/) { '| ' + $& }
          faildesc << stderr_log
        end
      end
    end
    faildesc
  }
end

def assert_finish(timeout_seconds, testsrc, message = '')
  if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # for --jit-wait
    timeout_seconds *= 3
  end
  newtest
  show_progress(message) {
    faildesc = nil
    filename = make_srcfile(testsrc)
    io = IO.popen("#{@ruby} -W0 #{filename}")
    pid = io.pid
    waited = false
    tlimit = Time.now + timeout_seconds
    diff = timeout_seconds
    while diff > 0
      if Process.waitpid pid, Process::WNOHANG
        waited = true
        break
      end
      if io.respond_to?(:read_nonblock)
        if IO.select([io], nil, nil, diff)
          begin
            io.read_nonblock(1024)
          rescue Errno::EAGAIN, IO::WaitReadable, EOFError
            break
          end while true
        end
      else
        sleep 0.1
      end
      diff = tlimit - Time.now
    end
    if !waited
      Process.kill(:KILL, pid)
      Process.waitpid pid
      faildesc = pretty(testsrc, "not finished in #{timeout_seconds} seconds", nil)
    end
    io.close
    faildesc
  }
end

def flunk(message = '')
  newtest
  show_progress('') { message }
end

def pretty(src, desc, result)
  src = src.sub(/\A\s*\n/, '')
  (/\n/ =~ src ? "\n#{adjust_indent(src)}" : src) + "  #=> #{desc}"
end

INDENT = 27

def adjust_indent(src)
  untabify(src).gsub(/^ {#{INDENT}}/o, '').gsub(/^/, '   ').sub(/\s*\z/, "\n")
end

def untabify(str)
  str.gsub(/^\t+/) {' ' * (8 * $&.size) }
end

def make_srcfile(src, frozen_string_literal: nil)
  filename = 'bootstraptest.tmp.rb'
  File.open(filename, 'w') {|f|
    f.puts "#frozen_string_literal:true" if frozen_string_literal
    f.puts "GC.stress = true" if $stress
    f.puts "print(begin; #{src}; end)"
  }
  filename
end

def get_result_string(src, opt = '', **argh)
  if @ruby
    filename = make_srcfile(src, **argh)
    begin
      `#{@ruby} -W0 #{opt} #{filename}`
    ensure
      raise Interrupt if $? and $?.signaled? && $?.termsig == Signal.list["INT"]
    end
  else
    eval(src).to_s
  end
end

def with_stderr
  out = err = nil
  begin
    r, w = IO.pipe
    stderr = $stderr.dup
    $stderr.reopen(w)
    w.close
    reader = Thread.start {r.read}
    begin
      out = yield
    ensure
      $stderr.reopen(stderr)
      err = reader.value
    end
  ensure
    w.close rescue nil
    r.close rescue nil
  end
  return out, err
end

def newtest
  @location = File.basename(caller(2).first)
  @count += 1
  cleanup_coredump
end

def error(msg, additional_message)
  msg = "#{@failed}\##{@count} #{@location}#{@reset}: #{msg}  #{additional_message}"
  if @tty
    $stderr.puts "#{erase}#{msg}"
  else
    @errbuf.push msg
  end
  @error += 1
end

def in_temporary_working_directory(dir)
  if dir
    Dir.mkdir dir
    Dir.chdir(dir) {
      yield
    }
  else
    Dir.mktmpdir(["bootstraptest", ".tmpwd"]) {|d|
      Dir.chdir(d) {
        yield
      }
    }
  end
end

def cleanup_coredump
  if File.file?('core')
    require 'time'
    Dir.glob('/tmp/bootstraptest-core.*').each do |f|
      if Time.now - File.mtime(f) > 7 * 24 * 60 * 60 # 7 days
        warn "Deleting an old core file: #{f}"
        FileUtils.rm(f)
      end
    end
    core_path = "/tmp/bootstraptest-core.#{Time.now.utc.iso8601}"
    warn "A core file is found. Saving it at: #{core_path.dump}"
    FileUtils.mv('core', core_path)
    cmd = ['gdb', @ruby, '-c', core_path, '-ex', 'bt', '-batch']
    p cmd # debugging why it's not working
    system(*cmd)
  end
  FileUtils.rm_f Dir.glob('core.*')
  FileUtils.rm_f @ruby+'.stackdump' if @ruby
end

class CoreDumpError < StandardError; end

def check_coredump
  if File.file?('core') or not Dir.glob('core.*').empty? or
      (@ruby and File.exist?(@ruby+'.stackdump'))
    raise CoreDumpError, "core dumped"
  end
end

main
