diff --git a/Rakefile b/Rakefile index e956eb17..de1db20e 100644 --- a/Rakefile +++ b/Rakefile @@ -74,6 +74,13 @@ else task :compile => 'lib/racc/parser-text.rb' end +task :sync_tool do + require 'fileutils' + FileUtils.cp "../ruby/tool/lib/core_assertions.rb", "./test/lib" + FileUtils.cp "../ruby/tool/lib/envutil.rb", "./test/lib" + FileUtils.cp "../ruby/tool/lib/find_executable.rb", "./test/lib" +end + task :build => "lib/racc/parser-text.rb" task :test => :compile diff --git a/test/lib/core_assertions.rb b/test/lib/core_assertions.rb index 63d79ae2..44715255 100644 --- a/test/lib/core_assertions.rb +++ b/test/lib/core_assertions.rb @@ -24,23 +24,8 @@ def message msg = nil, ending = nil, &default end module CoreAssertions - if defined?(MiniTest) - require_relative '../../envutil' - # for ruby core testing - include MiniTest::Assertions - - # Compatibility hack for assert_raise - Test::Unit::AssertionFailedError = MiniTest::Assertion - else - module MiniTest - class Assertion < Exception; end - class Skip < Assertion; end - end - - require 'pp' - require_relative 'envutil' - include Test::Unit::Assertions - end + require_relative 'envutil' + require 'pp' def mu_pp(obj) #:nodoc: obj.pretty_inspect.chomp @@ -94,6 +79,161 @@ def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], end end + if defined?(RubyVM::InstructionSequence) + def syntax_check(code, fname, line) + code = code.dup.force_encoding(Encoding::UTF_8) + RubyVM::InstructionSequence.compile(code, fname, fname, line) + :ok + ensure + raise if SyntaxError === $! + end + else + def syntax_check(code, fname, line) + code = code.b + code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) { + "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n" + } + code = code.force_encoding(Encoding::UTF_8) + catch {|tag| eval(code, binding, fname, line - 1)} + end + end + + def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt) + # TODO: consider choosing some appropriate limit for MJIT and stop skipping this once it does not randomly fail + pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::JIT) && RubyVM::JIT.enabled? + + require_relative 'memory_status' + raise Test::Unit::PendedError, "unsupported platform" unless defined?(Memory::Status) + + token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" + token_dump = token.dump + token_re = Regexp.quote(token) + envs = args.shift if Array === args and Hash === args.first + args = [ + "--disable=gems", + "-r", File.expand_path("../memory_status", __FILE__), + *args, + "-v", "-", + ] + if defined? Memory::NO_MEMORY_LEAK_ENVS then + envs ||= {} + newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break } + envs = newenvs if newenvs + end + args.unshift(envs) if envs + cmd = [ + 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}', + prepare, + 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")', + '$initial_size = $initial_status.size', + code, + 'GC.start', + ].join("\n") + _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt) + before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1) + after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1) + assert(status.success?, FailDesc[status, message, err]) + ([:size, (rss && :rss)] & after.members).each do |n| + b = before[n] + a = after[n] + next unless a > 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 + line = __LINE__; yield + rescue Test::Unit::PendedError + raise + rescue Exception => e + bt = e.backtrace + as = e.instance_of?(Test::Unit::AssertionFailedError) + if as + ans = /\A#{Regexp.quote(__FILE__)}:#{line}:in /o + bt.reject! {|ln| ans =~ ln} + end + if ((args.empty? && !as) || + args.any? {|a| a.instance_of?(Module) ? e.is_a?(a) : e.class == a }) + msg = message(msg) { + "Exception raised:\n<#{mu_pp(e)}>\n" + + "Backtrace:\n" + + e.backtrace.map{|frame| " #{frame}"}.join("\n") + } + raise Test::Unit::AssertionFailedError, msg.call, bt + else + raise + end + 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] @@ -104,35 +244,58 @@ def assert_ruby_status(args, test_stdin="", message=nil, **opt) ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM") + def separated_runner(out = nil) + include(*Test::Unit::TestCase.ancestors.select {|c| !c.is_a?(Class) }) + out = out ? IO.new(out, 'w') : STDOUT + at_exit { + out.puts [Marshal.dump($!)].pack('m'), "assertions=#{self._assertions}" + } + Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) if defined?(Test::Unit::Runner) + 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/ =~ RUBY_PLATFORM + 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 src = < marshal_error ignore_stderr = nil + res = nil end - if res + if res and !(SystemExit === res) if bt = res.backtrace bt.each do |l| l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"} @@ -141,7 +304,7 @@ class Test::Unit::Runner else res.set_backtrace(caller) end - raise res unless SystemExit === res + raise res end # really is it succeed? @@ -153,6 +316,27 @@ class Test::Unit::Runner 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 ) # @@ -203,8 +387,8 @@ def assert_raise(*exp, &b) begin yield - rescue MiniTest::Skip => e - return e if exp.include? MiniTest::Skip + rescue Test::Unit::PendedError => e + return e if exp.include? Test::Unit::PendedError raise e rescue Exception => e expected = exp.any? { |ex| @@ -216,7 +400,7 @@ def assert_raise(*exp, &b) } assert expected, proc { - exception_details(e, message(msg) {"#{mu_pp(exp)} exception expected, not"}.call) + flunk(message(msg) {"#{mu_pp(exp)} exception expected, not #{mu_pp(e)}"}) } return e @@ -279,6 +463,121 @@ def assert_raise_with_message(exception, expected, msg = nil, &block) ex end + MINI_DIR = File.join(File.dirname(File.expand_path(__FILE__)), "minitest") #: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?(MINI_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?(MINI_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?(MINI_DIR) + return unless obj.respond_to?(meth) + end + refute_respond_to(obj, meth, msg) + end + + # pattern_list is an array which contains regexp and :*. + # :* means any sequence. + # + # pattern_list is anchored. + # Use [:*, regexp, :*] 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 = /\A#{pattern}/.match(rest) + else + match = pattern.match(rest) + end + unless match + 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 = match.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) { @@ -295,7 +594,22 @@ def assert_warn(*args) assert_warning(*args) {$VERBOSE = false; yield} end + def assert_deprecated_warning(mesg = /deprecated/) + assert_warning(mesg) do + Warning[:deprecated] = true + yield + end + end + + def assert_deprecated_warn(mesg = /deprecated/) + assert_warn(mesg) do + Warning[:deprecated] = true + yield + end + end + class << (AssertFile = Struct.new(:failure_message).new) + include Assertions include CoreAssertions def assert_file_predicate(predicate, *args) if /\Anot_/ =~ predicate @@ -381,13 +695,23 @@ def assert_join_threads(threads, message = nil) msg = "exceptions on #{errs.length} threads:\n" + errs.map {|t, err| "#{t.inspect}:\n" + - err.full_message(highlight: false, order: :top) + RUBY_VERSION >= "2.5.0" ? err.full_message(highlight: false, order: :top) : err.message }.join("\n---\n") if message msg = "#{message}\n#{msg}" end - raise MiniTest::Assertion, msg + 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) @@ -398,6 +722,14 @@ def assert_all_assertions(msg = nil) 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 + def message(msg = nil, *args, &default) # :nodoc: if Proc === msg super(nil, *args) do @@ -415,6 +747,22 @@ def message(msg = nil, *args, &default) # :nodoc: super 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 end end end diff --git a/test/lib/envutil.rb b/test/lib/envutil.rb index 2faf4838..0391b90c 100644 --- a/test/lib/envutil.rb +++ b/test/lib/envutil.rb @@ -47,12 +47,13 @@ def rubybin class << self attr_accessor :timeout_scale attr_reader :original_internal_encoding, :original_external_encoding, - :original_verbose + :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 @@ -86,7 +87,20 @@ def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1) 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 @@ -100,6 +114,8 @@ def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1) begin Timeout.timeout(reprieve) {Process.wait(pid)} rescue Timeout::Error + else + break end end end @@ -109,7 +125,7 @@ def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1) 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, + stdout_filter: nil, stderr_filter: nil, ios: nil, signal: :TERM, rubybin: EnvUtil.rubybin, precommand: nil, **opt) @@ -125,6 +141,8 @@ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = 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} @@ -134,11 +152,14 @@ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = if RUBYLIB and lib = child_env["RUBYLIB"] child_env["RUBYLIB"] = [lib, RUBYLIB].join(File::PATH_SEPARATOR) end + child_env['ASAN_OPTIONS'] = ENV['ASAN_OPTIONS'] if ENV['ASAN_OPTIONS'] args = [args] if args.kind_of?(String) - pid = spawn(child_env, *precommand, rubybin, *args, **opt) + pid = spawn(child_env, *precommand, rubybin, *args, opt) in_c.close - out_c.close if capture_stdout - err_c.close if capture_stderr && capture_stderr != :merge_to_stdout + 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 @@ -180,11 +201,6 @@ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = end module_function :invoke_ruby - alias rubyexec invoke_ruby - class << self - alias rubyexec invoke_ruby - end - def verbose_warning class << (stderr = "".dup) alias write concat @@ -197,6 +213,7 @@ def flush; end ensure stderr, $stderr = $stderr, stderr $VERBOSE = EnvUtil.original_verbose + EnvUtil.original_warning&.each {|i, v| Warning[i] = v} end module_function :verbose_warning @@ -242,7 +259,11 @@ def with_default_internal(enc) def labeled_module(name, &block) Module.new do - singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s} + singleton_class.class_eval { + define_method(:to_s) {name} + alias inspect to_s + alias name to_s + } class_eval(&block) if block end end @@ -250,7 +271,11 @@ def labeled_module(name, &block) def labeled_class(name, superclass = Object, &block) Class.new(superclass) do - singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s} + singleton_class.class_eval { + define_method(:to_s) {name} + alias inspect to_s + alias name to_s + } class_eval(&block) if block end end