From 7f58093ca3ba10079bf2536aaa9b17749e901e34 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 28 Jul 2023 11:23:27 +0900 Subject: [PATCH] Use test-unit-ruby-core instead of deep-copy from ruby repository --- Gemfile | 1 + rakelib/sync_tool.rake | 17 - test/lib/core_assertions.rb | 817 ------------------------------------ test/lib/envutil.rb | 380 ----------------- test/lib/find_executable.rb | 22 - test/lib/helper.rb | 2 +- 6 files changed, 2 insertions(+), 1237 deletions(-) delete mode 100644 rakelib/sync_tool.rake delete mode 100644 test/lib/core_assertions.rb delete mode 100644 test/lib/envutil.rb delete mode 100644 test/lib/find_executable.rb diff --git a/Gemfile b/Gemfile index b5b5fb5664..df26e09887 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ group :development do gem 'racc', '> 1.4.10' gem 'kpeg', '>= 1.3.3' gem 'test-unit' + gem 'test-unit-ruby-core' gem 'rubocop', '>= 1.31.0' gem 'gettext' end diff --git a/rakelib/sync_tool.rake b/rakelib/sync_tool.rake deleted file mode 100644 index 8ea8cb0ad2..0000000000 --- a/rakelib/sync_tool.rake +++ /dev/null @@ -1,17 +0,0 @@ -task :sync_tool, [:from] do |t, from: nil| - from ||= (File.identical?(__dir__, "rakelib") ? "../ruby/tool" : File.dirname(__dir__)) - - require 'fileutils' - - { - "rakelib/sync_tool.rake" => "rakelib", - "lib/core_assertions.rb" => "test/lib", - "lib/envutil.rb" => "test/lib", - "lib/find_executable.rb" => "test/lib", - "lib/helper.rb" => "test/lib", - }.each do |src, dest| - FileUtils.mkpath(dest) - FileUtils.cp "#{from}/#{src}", dest - rescue Errno::ENOENT - end -end diff --git a/test/lib/core_assertions.rb b/test/lib/core_assertions.rb deleted file mode 100644 index 4887d944c5..0000000000 --- a/test/lib/core_assertions.rb +++ /dev/null @@ -1,817 +0,0 @@ -# frozen_string_literal: true - -module Test - module Unit - module Assertions - def assert_raises(*exp, &b) - raise NoMethodError, "use assert_raise", caller - end - - def _assertions= n # :nodoc: - @_assertions = n - end - - def _assertions # :nodoc: - @_assertions ||= 0 - end - - ## - # Returns a proc that will output +msg+ along with the default message. - - def message msg = nil, ending = nil, &default - proc { - ending ||= (ending_pattern = /(? 0 and b > 0 - assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"}) - end - rescue LoadError - pend - end - - # :call-seq: - # assert_nothing_raised( *args, &block ) - # - #If any exceptions are given as arguments, the assertion will - #fail if one of those exceptions are raised. Otherwise, the test fails - #if any exceptions are raised. - # - #The final argument may be a failure message. - # - # assert_nothing_raised RuntimeError do - # raise Exception #Assertion passes, Exception is not a RuntimeError - # end - # - # assert_nothing_raised do - # raise Exception #Assertion fails - # end - def assert_nothing_raised(*args) - self._assertions += 1 - if Module === args.last - msg = nil - else - msg = args.pop - end - begin - yield - rescue Test::Unit::PendedError, *(Test::Unit::AssertionFailedError if args.empty?) - raise - rescue *(args.empty? ? Exception : args) => e - msg = message(msg) { - "Exception raised:\n<#{mu_pp(e)}>\n""Backtrace:\n" << - Test.filter_backtrace(e.backtrace).map{|frame| " #{frame}"}.join("\n") - } - raise Test::Unit::AssertionFailedError, msg.call, e.backtrace - end - end - - def prepare_syntax_check(code, fname = nil, mesg = nil, verbose: nil) - fname ||= caller_locations(2, 1)[0] - mesg ||= fname.to_s - verbose, $VERBOSE = $VERBOSE, verbose - case - when Array === fname - fname, line = *fname - when defined?(fname.path) && defined?(fname.lineno) - fname, line = fname.path, fname.lineno - else - line = 1 - end - yield(code, fname, line, message(mesg) { - if code.end_with?("\n") - "```\n#{code}```\n" - else - "```\n#{code}\n```\n""no-newline" - end - }) - ensure - $VERBOSE = verbose - end - - def assert_valid_syntax(code, *args, **opt) - prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg| - yield if defined?(yield) - assert_nothing_raised(SyntaxError, mesg) do - assert_equal(:ok, syntax_check(src, fname, line), mesg) - end - end - end - - def assert_normal_exit(testsrc, message = '', child_env: nil, **opt) - assert_valid_syntax(testsrc, caller_locations(1, 1)[0]) - if child_env - child_env = [child_env] - else - child_env = [] - end - out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt) - assert !status.signaled?, FailDesc[status, message, out] - end - - def assert_ruby_status(args, test_stdin="", message=nil, **opt) - out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt) - desc = FailDesc[status, message, out] - assert(!status.signaled?, desc) - message ||= "ruby exit status is not success:" - assert(status.success?, desc) - end - - ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM") - - def separated_runner(token, out = nil) - include(*Test::Unit::TestCase.ancestors.select {|c| !c.is_a?(Class) }) - out = out ? IO.new(out, 'w') : STDOUT - at_exit { - out.puts "#{token}", [Marshal.dump($!)].pack('m'), "#{token}", "#{token}assertions=#{self._assertions}" - } - if defined?(Test::Unit::Runner) - Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) - elsif defined?(Test::Unit::AutoRunner) - Test::Unit::AutoRunner.need_auto_run = false - end - end - - def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt) - unless file and line - loc, = caller_locations(1,1) - file ||= loc.path - line ||= loc.lineno - end - capture_stdout = true - unless /mswin|mingw/ =~ RbConfig::CONFIG['host_os'] - capture_stdout = false - opt[:out] = Test::Unit::Runner.output if defined?(Test::Unit::Runner) - res_p, res_c = IO.pipe - opt[:ios] = [res_c] - end - token_dump, token_re = new_test_token - src = <\n\K.*\n(?=#{token_re}<\/error>$)/m].unpack1("m")) - rescue => marshal_error - ignore_stderr = nil - res = nil - end - if res and !(SystemExit === res) - if bt = res.backtrace - bt.each do |l| - l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"} - end - bt.concat(caller) - else - res.set_backtrace(caller) - end - raise res - end - - # really is it succeed? - unless ignore_stderr - # the body of assert_separately must not output anything to detect error - assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr]) - end - assert(status.success?, FailDesc[status, "assert_separately failed", stderr]) - raise marshal_error if marshal_error - end - - # Run Ractor-related test without influencing the main test suite - def assert_ractor(src, args: [], require: nil, require_relative: nil, file: nil, line: nil, ignore_stderr: nil, **opt) - return unless defined?(Ractor) - - require = "require #{require.inspect}" if require - if require_relative - dir = File.dirname(caller_locations[0,1][0].absolute_path) - full_path = File.expand_path(require_relative, dir) - require = "#{require}; require #{full_path.inspect}" - end - - assert_separately(args, file, line, <<~RUBY, ignore_stderr: ignore_stderr, **opt) - #{require} - previous_verbose = $VERBOSE - $VERBOSE = nil - Ractor.new {} # trigger initial warning - $VERBOSE = previous_verbose - #{src} - RUBY - end - - # :call-seq: - # assert_throw( tag, failure_message = nil, &block ) - # - #Fails unless the given block throws +tag+, returns the caught - #value otherwise. - # - #An optional failure message may be provided as the final argument. - # - # tag = Object.new - # assert_throw(tag, "#{tag} was not thrown!") do - # throw tag - # end - def assert_throw(tag, msg = nil) - ret = catch(tag) do - begin - yield(tag) - rescue UncaughtThrowError => e - thrown = e.tag - end - msg = message(msg) { - "Expected #{mu_pp(tag)} to have been thrown"\ - "#{%Q[, not #{thrown}] if thrown}" - } - assert(false, msg) - end - assert(true) - ret - end - - # :call-seq: - # assert_raise( *args, &block ) - # - #Tests if the given block raises an exception. Acceptable exception - #types may be given as optional arguments. If the last argument is a - #String, it will be used as the error message. - # - # assert_raise do #Fails, no Exceptions are raised - # end - # - # assert_raise NameError do - # puts x #Raises NameError, so assertion succeeds - # end - def assert_raise(*exp, &b) - case exp.last - when String, Proc - msg = exp.pop - end - - begin - yield - rescue Test::Unit::PendedError => e - return e if exp.include? Test::Unit::PendedError - raise e - rescue Exception => e - expected = exp.any? { |ex| - if ex.instance_of? Module then - e.kind_of? ex - else - e.instance_of? ex - end - } - - assert expected, proc { - flunk(message(msg) {"#{mu_pp(exp)} exception expected, not #{mu_pp(e)}"}) - } - - return e - ensure - unless e - exp = exp.first if exp.size == 1 - - flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"}) - end - end - end - - # :call-seq: - # assert_raise_with_message(exception, expected, msg = nil, &block) - # - #Tests if the given block raises an exception with the expected - #message. - # - # assert_raise_with_message(RuntimeError, "foo") do - # nil #Fails, no Exceptions are raised - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise ArgumentError, "foo" #Fails, different Exception is raised - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise "bar" #Fails, RuntimeError is raised but the message differs - # end - # - # assert_raise_with_message(RuntimeError, "foo") do - # raise "foo" #Raises RuntimeError with the message, so assertion succeeds - # end - def assert_raise_with_message(exception, expected, msg = nil, &block) - case expected - when String - assert = :assert_equal - when Regexp - assert = :assert_match - else - raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}" - end - - ex = m = nil - EnvUtil.with_default_internal(expected.encoding) do - ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do - yield - end - m = ex.message - end - msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"} - - if assert == :assert_equal - assert_equal(expected, m, msg) - else - msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" } - assert expected =~ m, msg - block.binding.eval("proc{|_|$~=_}").call($~) - end - ex - end - - TEST_DIR = File.join(__dir__, "test/unit") #:nodoc: - - # :call-seq: - # assert(test, [failure_message]) - # - #Tests if +test+ is true. - # - #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used - #as the failure message. Otherwise, the result of calling +msg+ will be - #used as the message if the assertion fails. - # - #If no +msg+ is given, a default message will be used. - # - # assert(false, "This was expected to be true") - def assert(test, *msgs) - case msg = msgs.first - when String, Proc - when nil - msgs.shift - else - bt = caller.reject { |s| s.start_with?(TEST_DIR) } - raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt - end unless msgs.empty? - super - end - - # :call-seq: - # assert_respond_to( object, method, failure_message = nil ) - # - #Tests if the given Object responds to +method+. - # - #An optional failure message may be provided as the final argument. - # - # assert_respond_to("hello", :reverse) #Succeeds - # assert_respond_to("hello", :does_not_exist) #Fails - def assert_respond_to(obj, (meth, *priv), msg = nil) - unless priv.empty? - msg = message(msg) { - "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}" - } - return assert obj.respond_to?(meth, *priv), msg - end - #get rid of overcounting - if caller_locations(1, 1)[0].path.start_with?(TEST_DIR) - return if obj.respond_to?(meth) - end - super(obj, meth, msg) - end - - # :call-seq: - # assert_not_respond_to( object, method, failure_message = nil ) - # - #Tests if the given Object does not respond to +method+. - # - #An optional failure message may be provided as the final argument. - # - # assert_not_respond_to("hello", :reverse) #Fails - # assert_not_respond_to("hello", :does_not_exist) #Succeeds - def assert_not_respond_to(obj, (meth, *priv), msg = nil) - unless priv.empty? - msg = message(msg) { - "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}" - } - return assert !obj.respond_to?(meth, *priv), msg - end - #get rid of overcounting - if caller_locations(1, 1)[0].path.start_with?(TEST_DIR) - return unless obj.respond_to?(meth) - end - refute_respond_to(obj, meth, msg) - end - - # pattern_list is an array which contains regexp, string and :*. - # :* means any sequence. - # - # pattern_list is anchored. - # Use [:*, regexp/string, :*] for non-anchored match. - def assert_pattern_list(pattern_list, actual, message=nil) - rest = actual - anchored = true - pattern_list.each_with_index {|pattern, i| - if pattern == :* - anchored = false - else - if anchored - match = rest.rindex(pattern, 0) - else - match = rest.index(pattern) - end - if match - post_match = $~ ? $~.post_match : rest[match+pattern.size..-1] - else - msg = message(msg) { - expect_msg = "Expected #{mu_pp pattern}\n" - if /\n[^\n]/ =~ rest - actual_mesg = +"to match\n" - rest.scan(/.*\n+/) { - actual_mesg << ' ' << $&.inspect << "+\n" - } - actual_mesg.sub!(/\+\n\z/, '') - else - actual_mesg = "to match " + mu_pp(rest) - end - actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters" - expect_msg + actual_mesg - } - assert false, msg - end - rest = post_match - anchored = true - end - } - if anchored - assert_equal("", rest) - end - end - - def assert_warning(pat, msg = nil) - result = nil - stderr = EnvUtil.with_default_internal(pat.encoding) { - EnvUtil.verbose_warning { - result = yield - } - } - msg = message(msg) {diff pat, stderr} - assert(pat === stderr, msg) - result - end - - def assert_warn(*args) - assert_warning(*args) {$VERBOSE = false; yield} - end - - def assert_deprecated_warning(mesg = /deprecated/) - assert_warning(mesg) do - Warning[:deprecated] = true if Warning.respond_to?(:[]=) - yield - end - end - - def assert_deprecated_warn(mesg = /deprecated/) - assert_warn(mesg) do - Warning[:deprecated] = true if Warning.respond_to?(:[]=) - yield - end - end - - class << (AssertFile = Struct.new(:failure_message).new) - include Assertions - include CoreAssertions - def assert_file_predicate(predicate, *args) - if /\Anot_/ =~ predicate - predicate = $' - neg = " not" - end - result = File.__send__(predicate, *args) - result = !result if neg - mesg = "Expected file ".dup << args.shift.inspect - mesg << "#{neg} to be #{predicate}" - mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty? - mesg << " #{failure_message}" if failure_message - assert(result, mesg) - end - alias method_missing assert_file_predicate - - def for(message) - clone.tap {|a| a.failure_message = message} - end - end - - class AllFailures - attr_reader :failures - - def initialize - @count = 0 - @failures = {} - end - - def for(key) - @count += 1 - yield key - rescue Exception => e - @failures[key] = [@count, e] - end - - def foreach(*keys) - keys.each do |key| - @count += 1 - begin - yield key - rescue Exception => e - @failures[key] = [@count, e] - end - end - end - - def message - i = 0 - total = @count.to_s - fmt = "%#{total.size}d" - @failures.map {|k, (n, v)| - v = v.message - "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.b.gsub(/^/, ' | ').force_encoding(v.encoding)}" - }.join("\n") - end - - def pass? - @failures.empty? - end - end - - # threads should respond to shift method. - # Array can be used. - def assert_join_threads(threads, message = nil) - errs = [] - values = [] - while th = threads.shift - begin - values << th.value - rescue Exception - errs << [th, $!] - th = nil - end - end - values - ensure - if th&.alive? - th.raise(Timeout::Error.new) - th.join rescue errs << [th, $!] - end - if !errs.empty? - msg = "exceptions on #{errs.length} threads:\n" + - errs.map {|t, err| - "#{t.inspect}:\n" + - (err.respond_to?(:full_message) ? err.full_message(highlight: false, order: :top) : err.message) - }.join("\n---\n") - if message - msg = "#{message}\n#{msg}" - end - raise Test::Unit::AssertionFailedError, msg - end - end - - def assert_all?(obj, m = nil, &blk) - failed = [] - obj.each do |*a, &b| - unless blk.call(*a, &b) - failed << (a.size > 1 ? a : a[0]) - end - end - assert(failed.empty?, message(m) {failed.pretty_inspect}) - end - - def assert_all_assertions(msg = nil) - all = AllFailures.new - yield all - ensure - assert(all.pass?, message(msg) {all.message.chomp(".")}) - end - alias all_assertions assert_all_assertions - - def assert_all_assertions_foreach(msg = nil, *keys, &block) - all = AllFailures.new - all.foreach(*keys, &block) - ensure - assert(all.pass?, message(msg) {all.message.chomp(".")}) - end - alias all_assertions_foreach assert_all_assertions_foreach - - %w[ - CLOCK_THREAD_CPUTIME_ID CLOCK_PROCESS_CPUTIME_ID - CLOCK_MONOTONIC - ].find do |clk| - if Process.const_defined?(clk) - [clk.to_sym, Process.const_get(clk)].find do |clk| - Process.clock_gettime(clk) - rescue - # Constants may be defined but not implemented, e.g., mingw. - else - PERFORMANCE_CLOCK = clk - end - end - end - - # Expect +seq+ to respond to +first+ and +each+ methods, e.g., - # Array, Range, Enumerator::ArithmeticSequence and other - # Enumerable-s, and each elements should be size factors. - # - # :yield: each elements of +seq+. - def assert_linear_performance(seq, rehearsal: nil, pre: ->(n) {n}) - pend "No PERFORMANCE_CLOCK found" unless defined?(PERFORMANCE_CLOCK) - - # Timeout testing generally doesn't work when RJIT compilation happens. - rjit_enabled = defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? - measure = proc do |arg, message| - st = Process.clock_gettime(PERFORMANCE_CLOCK) - yield(*arg) - t = (Process.clock_gettime(PERFORMANCE_CLOCK) - st) - assert_operator 0, :<=, t, message unless rjit_enabled - t - end - - first = seq.first - *arg = pre.call(first) - times = (0..(rehearsal || (2 * first))).map do - measure[arg, "rehearsal"].nonzero? - end - times.compact! - tmin, tmax = times.minmax - tbase = 10 ** Math.log10(tmax * ([(tmax / tmin), 2].max ** 2)).ceil - info = "(tmin: #{tmin}, tmax: #{tmax}, tbase: #{tbase})" - - seq.each do |i| - next if i == first - t = tbase * i.fdiv(first) - *arg = pre.call(i) - message = "[#{i}]: in #{t}s #{info}" - Timeout.timeout(t, Timeout::Error, message) do - measure[arg, message] - end - end - end - - def diff(exp, act) - require 'pp' - q = PP.new(+"") - q.guard_inspect_key do - q.group(2, "expected: ") do - q.pp exp - end - q.text q.newline - q.group(2, "actual: ") do - q.pp act - end - q.flush - end - q.output - end - - def new_test_token - token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" - return token.dump, Regexp.quote(token) - end - end - end -end diff --git a/test/lib/envutil.rb b/test/lib/envutil.rb deleted file mode 100644 index 728ca7059b..0000000000 --- a/test/lib/envutil.rb +++ /dev/null @@ -1,380 +0,0 @@ -# -*- coding: us-ascii -*- -# frozen_string_literal: true -require "open3" -require "timeout" -require_relative "find_executable" -begin - require 'rbconfig' -rescue LoadError -end -begin - require "rbconfig/sizeof" -rescue LoadError -end - -module EnvUtil - def rubybin - if ruby = ENV["RUBY"] - return ruby - end - ruby = "ruby" - exeext = RbConfig::CONFIG["EXEEXT"] - rubyexe = (ruby + exeext if exeext and !exeext.empty?) - 3.times do - if File.exist? ruby and File.executable? ruby and !File.directory? ruby - return File.expand_path(ruby) - end - if rubyexe and File.exist? rubyexe and File.executable? rubyexe - return File.expand_path(rubyexe) - end - ruby = File.join("..", ruby) - end - if defined?(RbConfig.ruby) - RbConfig.ruby - else - "ruby" - end - end - module_function :rubybin - - LANG_ENVS = %w"LANG LC_ALL LC_CTYPE" - - DEFAULT_SIGNALS = Signal.list - DEFAULT_SIGNALS.delete("TERM") if /mswin|mingw/ =~ RUBY_PLATFORM - - RUBYLIB = ENV["RUBYLIB"] - - class << self - attr_accessor :timeout_scale - attr_reader :original_internal_encoding, :original_external_encoding, - :original_verbose, :original_warning - - def capture_global_values - @original_internal_encoding = Encoding.default_internal - @original_external_encoding = Encoding.default_external - @original_verbose = $VERBOSE - @original_warning = defined?(Warning.[]) ? %i[deprecated experimental].to_h {|i| [i, Warning[i]]} : nil - end - end - - def apply_timeout_scale(t) - if scale = EnvUtil.timeout_scale - t * scale - else - t - end - end - module_function :apply_timeout_scale - - def timeout(sec, klass = nil, message = nil, &blk) - return yield(sec) if sec == nil or sec.zero? - sec = apply_timeout_scale(sec) - Timeout.timeout(sec, klass, message, &blk) - end - module_function :timeout - - def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1) - reprieve = apply_timeout_scale(reprieve) if reprieve - - signals = Array(signal).select do |sig| - DEFAULT_SIGNALS[sig.to_s] or - DEFAULT_SIGNALS[Signal.signame(sig)] rescue false - end - signals |= [:ABRT, :KILL] - case pgroup - when 0, true - pgroup = -pid - when nil, false - pgroup = pid - end - - lldb = true if /darwin/ =~ RUBY_PLATFORM - - while signal = signals.shift - - if lldb and [:ABRT, :KILL].include?(signal) - lldb = false - # sudo -n: --non-interactive - # lldb -p: attach - # -o: run command - system(*%W[sudo -n lldb -p #{pid} --batch -o bt\ all -o call\ rb_vmdebug_stack_dump_all_threads() -o quit]) - true - end - - begin - Process.kill signal, pgroup - rescue Errno::EINVAL - next - rescue Errno::ESRCH - break - end - if signals.empty? or !reprieve - Process.wait(pid) - else - begin - Timeout.timeout(reprieve) {Process.wait(pid)} - rescue Timeout::Error - else - break - end - end - end - $? - end - module_function :terminate - - def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false, - encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error, - stdout_filter: nil, stderr_filter: nil, ios: nil, - signal: :TERM, - rubybin: EnvUtil.rubybin, precommand: nil, - **opt) - timeout = apply_timeout_scale(timeout) - - in_c, in_p = IO.pipe - out_p, out_c = IO.pipe if capture_stdout - err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout - opt[:in] = in_c - opt[:out] = out_c if capture_stdout - opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr - if encoding - out_p.set_encoding(encoding) if out_p - err_p.set_encoding(encoding) if err_p - end - ios.each {|i, o = i|opt[i] = o} if ios - - c = "C" - child_env = {} - LANG_ENVS.each {|lc| child_env[lc] = c} - if Array === args and Hash === args.first - child_env.update(args.shift) - end - if RUBYLIB and lib = child_env["RUBYLIB"] - child_env["RUBYLIB"] = [lib, RUBYLIB].join(File::PATH_SEPARATOR) - end - - # remain env - %w(ASAN_OPTIONS RUBY_ON_BUG).each{|name| - child_env[name] = ENV[name] if ENV[name] - } - - args = [args] if args.kind_of?(String) - pid = spawn(child_env, *precommand, rubybin, *args, opt) - in_c.close - out_c&.close - out_c = nil - err_c&.close - err_c = nil - if block_given? - return yield in_p, out_p, err_p, pid - else - th_stdout = Thread.new { out_p.read } if capture_stdout - th_stderr = Thread.new { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout - in_p.write stdin_data.to_str unless stdin_data.empty? - in_p.close - if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout)) - timeout_error = nil - else - status = terminate(pid, signal, opt[:pgroup], reprieve) - terminated = Time.now - end - stdout = th_stdout.value if capture_stdout - stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout - out_p.close if capture_stdout - err_p.close if capture_stderr && capture_stderr != :merge_to_stdout - status ||= Process.wait2(pid)[1] - stdout = stdout_filter.call(stdout) if stdout_filter - stderr = stderr_filter.call(stderr) if stderr_filter - if timeout_error - bt = caller_locations - msg = "execution of #{bt.shift.label} expired timeout (#{timeout} sec)" - msg = failure_description(status, terminated, msg, [stdout, stderr].join("\n")) - raise timeout_error, msg, bt.map(&:to_s) - end - return stdout, stderr, status - end - ensure - [th_stdout, th_stderr].each do |th| - th.kill if th - end - [in_c, in_p, out_c, out_p, err_c, err_p].each do |io| - io&.close - end - [th_stdout, th_stderr].each do |th| - th.join if th - end - end - module_function :invoke_ruby - - def verbose_warning - class << (stderr = "".dup) - alias write concat - def flush; end - end - stderr, $stderr = $stderr, stderr - $VERBOSE = true - yield stderr - return $stderr - ensure - stderr, $stderr = $stderr, stderr - $VERBOSE = EnvUtil.original_verbose - EnvUtil.original_warning&.each {|i, v| Warning[i] = v} - end - module_function :verbose_warning - - def default_warning - $VERBOSE = false - yield - ensure - $VERBOSE = EnvUtil.original_verbose - end - module_function :default_warning - - def suppress_warning - $VERBOSE = nil - yield - ensure - $VERBOSE = EnvUtil.original_verbose - end - module_function :suppress_warning - - def under_gc_stress(stress = true) - stress, GC.stress = GC.stress, stress - yield - ensure - GC.stress = stress - end - module_function :under_gc_stress - - def with_default_external(enc) - suppress_warning { Encoding.default_external = enc } - yield - ensure - suppress_warning { Encoding.default_external = EnvUtil.original_external_encoding } - end - module_function :with_default_external - - def with_default_internal(enc) - suppress_warning { Encoding.default_internal = enc } - yield - ensure - suppress_warning { Encoding.default_internal = EnvUtil.original_internal_encoding } - end - module_function :with_default_internal - - def labeled_module(name, &block) - Module.new do - singleton_class.class_eval { - define_method(:to_s) {name} - alias inspect to_s - alias name to_s - } - class_eval(&block) if block - end - end - module_function :labeled_module - - def labeled_class(name, superclass = Object, &block) - Class.new(superclass) do - singleton_class.class_eval { - define_method(:to_s) {name} - alias inspect to_s - alias name to_s - } - class_eval(&block) if block - end - end - module_function :labeled_class - - if /darwin/ =~ RUBY_PLATFORM - DIAGNOSTIC_REPORTS_PATH = File.expand_path("~/Library/Logs/DiagnosticReports") - DIAGNOSTIC_REPORTS_TIMEFORMAT = '%Y-%m-%d-%H%M%S' - @ruby_install_name = RbConfig::CONFIG['RUBY_INSTALL_NAME'] - - def self.diagnostic_reports(signame, pid, now) - return unless %w[ABRT QUIT SEGV ILL TRAP].include?(signame) - cmd = File.basename(rubybin) - cmd = @ruby_install_name if "ruby-runner#{RbConfig::CONFIG["EXEEXT"]}" == cmd - path = DIAGNOSTIC_REPORTS_PATH - timeformat = DIAGNOSTIC_REPORTS_TIMEFORMAT - pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.{crash,ips}" - first = true - 30.times do - first ? (first = false) : sleep(0.1) - Dir.glob(pat) do |name| - log = File.read(name) rescue next - case name - when /\.crash\z/ - if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log - File.unlink(name) - File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil - return log - end - when /\.ips\z/ - if /^ *"pid" *: *#{pid},/ =~ log - File.unlink(name) - return log - end - end - end - end - nil - end - else - def self.diagnostic_reports(signame, pid, now) - end - end - - def self.failure_description(status, now, message = "", out = "") - pid = status.pid - if signo = status.termsig - signame = Signal.signame(signo) - sigdesc = "signal #{signo}" - end - log = diagnostic_reports(signame, pid, now) - if signame - sigdesc = "SIG#{signame} (#{sigdesc})" - end - if status.coredump? - sigdesc = "#{sigdesc} (core dumped)" - end - full_message = ''.dup - message = message.call if Proc === message - if message and !message.empty? - full_message << message << "\n" - end - full_message << "pid #{pid}" - full_message << " exit #{status.exitstatus}" if status.exited? - full_message << " killed by #{sigdesc}" if sigdesc - if out and !out.empty? - full_message << "\n" << out.b.gsub(/^/, '| ') - full_message.sub!(/(?