From 84e39f324f0818b25ef23eb1cd16981d4bfcf9e7 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Thu, 3 Oct 2019 01:13:20 +0900 Subject: [PATCH] Update test/lib --- test/lib/colorize.rb | 51 +++ test/lib/envutil.rb | 185 ++++++---- test/lib/find_executable.rb | 2 +- test/lib/leakchecker.rb | 45 ++- test/lib/minitest/autorun.rb | 2 +- test/lib/minitest/mock.rb | 2 +- test/lib/minitest/unit.rb | 43 ++- test/lib/test/unit.rb | 167 +++++---- test/lib/test/unit/assertions.rb | 472 +++++--------------------- test/lib/test/unit/core_assertions.rb | 416 +++++++++++++++++++++++ test/lib/test/unit/testcase.rb | 2 +- 11 files changed, 847 insertions(+), 540 deletions(-) create mode 100644 test/lib/colorize.rb create mode 100644 test/lib/test/unit/core_assertions.rb diff --git a/test/lib/colorize.rb b/test/lib/colorize.rb new file mode 100644 index 0000000..7494a21 --- /dev/null +++ b/test/lib/colorize.rb @@ -0,0 +1,51 @@ +# frozen-string-literal: true + +class Colorize + def initialize(color = nil, opts = ((_, color = color, nil)[0] if Hash === color)) + @colors = @reset = nil + if color or (color == nil && STDOUT.tty?) + if (/\A\e\[.*m\z/ =~ IO.popen("tput smso", "r", :err => IO::NULL, &:read) rescue nil) + @beg = "\e[" + colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {} + if opts and colors_file = opts[:colors_file] + begin + File.read(colors_file).scan(/(\w+)=([^:\n]*)/) do |n, c| + colors[n] ||= c + end + rescue Errno::ENOENT + end + end + @colors = colors + @reset = "#{@beg}m" + end + end + self + end + + DEFAULTS = { + "pass"=>"32", "fail"=>"31;1", "skip"=>"33;1", + "black"=>"30", "red"=>"31", "green"=>"32", "yellow"=>"33", + "blue"=>"34", "magenta"=>"35", "cyan"=>"36", "white"=>"37", + "bold"=>"1", "underline"=>"4", "reverse"=>"7", + } + + def decorate(str, name) + if @colors and color = (@colors[name] || DEFAULTS[name]) + "#{@beg}#{color}m#{str}#{@reset}" + else + str + end + end + + DEFAULTS.each_key do |name| + define_method(name) {|str| + decorate(str, name) + } + end +end + +if $0 == __FILE__ + colorize = Colorize.new + col = ARGV.shift + ARGV.each {|str| puts colorize.decorate(str, col)} +end diff --git a/test/lib/envutil.rb b/test/lib/envutil.rb index e6cdd77..2faf483 100644 --- a/test/lib/envutil.rb +++ b/test/lib/envutil.rb @@ -1,5 +1,5 @@ # -*- coding: us-ascii -*- -# frozen_string_literal: false +# frozen_string_literal: true require "open3" require "timeout" require_relative "find_executable" @@ -42,20 +42,79 @@ def rubybin DEFAULT_SIGNALS = Signal.list DEFAULT_SIGNALS.delete("TERM") if /mswin|mingw/ =~ RUBY_PLATFORM + RUBYLIB = ENV["RUBYLIB"] + class << self - attr_accessor :subprocess_timeout_scale + attr_accessor :timeout_scale + attr_reader :original_internal_encoding, :original_external_encoding, + :original_verbose + + def capture_global_values + @original_internal_encoding = Encoding.default_internal + @original_external_encoding = Encoding.default_external + @original_verbose = $VERBOSE + 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 + while signal = signals.shift + 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 + 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, signal: :TERM, - rubybin: EnvUtil.rubybin, + rubybin: EnvUtil.rubybin, precommand: nil, **opt) - if scale = EnvUtil.subprocess_timeout_scale - timeout *= scale if timeout - reprieve *= scale if reprieve - end + 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 @@ -72,8 +131,11 @@ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = 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 args = [args] if args.kind_of?(String) - pid = spawn(child_env, 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 @@ -87,35 +149,8 @@ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout)) timeout_error = nil else - 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 = opt[:pgroup] - when 0, true - pgroup = -pid - when nil, false - pgroup = pid - end - while signal = signals.shift - 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 - end - end - end - status = $? + 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 @@ -126,8 +161,8 @@ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = stderr = stderr_filter.call(stderr) if stderr_filter if timeout_error bt = caller_locations - msg = "execution of #{bt.shift.label} expired" - msg = Test::Unit::Assertions::FailDesc[status, msg, [stdout, stderr].join("\n")].() + 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 @@ -151,30 +186,33 @@ class << self end def verbose_warning - class << (stderr = "") - alias write << + class << (stderr = "".dup) + alias write concat + def flush; end end - stderr, $stderr, verbose, $VERBOSE = $stderr, stderr, $VERBOSE, true + stderr, $stderr = $stderr, stderr + $VERBOSE = true yield stderr return $stderr ensure - stderr, $stderr, $VERBOSE = $stderr, stderr, verbose + stderr, $stderr = $stderr, stderr + $VERBOSE = EnvUtil.original_verbose end module_function :verbose_warning def default_warning - verbose, $VERBOSE = $VERBOSE, false + $VERBOSE = false yield ensure - $VERBOSE = verbose + $VERBOSE = EnvUtil.original_verbose end module_function :default_warning def suppress_warning - verbose, $VERBOSE = $VERBOSE, nil + $VERBOSE = nil yield ensure - $VERBOSE = verbose + $VERBOSE = EnvUtil.original_verbose end module_function :suppress_warning @@ -187,26 +225,18 @@ def under_gc_stress(stress = true) module_function :under_gc_stress def with_default_external(enc) - verbose, $VERBOSE = $VERBOSE, nil - origenc, Encoding.default_external = Encoding.default_external, enc - $VERBOSE = verbose + suppress_warning { Encoding.default_external = enc } yield ensure - verbose, $VERBOSE = $VERBOSE, nil - Encoding.default_external = origenc - $VERBOSE = verbose + suppress_warning { Encoding.default_external = EnvUtil.original_external_encoding } end module_function :with_default_external def with_default_internal(enc) - verbose, $VERBOSE = $VERBOSE, nil - origenc, Encoding.default_internal = Encoding.default_internal, enc - $VERBOSE = verbose + suppress_warning { Encoding.default_internal = enc } yield ensure - verbose, $VERBOSE = $VERBOSE, nil - Encoding.default_internal = origenc - $VERBOSE = verbose + suppress_warning { Encoding.default_internal = EnvUtil.original_internal_encoding } end module_function :with_default_internal @@ -257,6 +287,37 @@ 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!(/(?' unless e.backtrace # SystemStackError can return nil. + e.backtrace.reverse_each do |s| break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/ last_before_assertion = s @@ -1168,6 +1171,14 @@ def rubinius? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE def windows? platform = RUBY_PLATFORM /mswin|mingw/ =~ platform end + + ## + # Is this running on mingw? + + def mingw? platform = RUBY_PLATFORM + /mingw/ =~ platform + end + end ## diff --git a/test/lib/test/unit.rb b/test/lib/test/unit.rb index 7357a62..2d5a32e 100644 --- a/test/lib/test/unit.rb +++ b/test/lib/test/unit.rb @@ -1,4 +1,4 @@ -# frozen_string_literal: false +# frozen_string_literal: true begin gem 'minitest', '< 5.0.0' if defined? Gem rescue Gem::LoadError @@ -6,6 +6,7 @@ require 'minitest/unit' require 'test/unit/assertions' require_relative '../envutil' +require_relative '../colorize' require 'test/unit/testcase' require 'optparse' @@ -136,8 +137,9 @@ def process_args(args = []) def non_options(files, options) @jobserver = nil + makeflags = ENV.delete("MAKEFLAGS") if !options[:parallel] and - /(?:\A|\s)--jobserver-(?:auth|fds)=(\d+),(\d+)/ =~ ENV["MAKEFLAGS"] + /(?:\A|\s)--jobserver-(?:auth|fds)=(\d+),(\d+)/ =~ makeflags begin r = IO.for_fd($1.to_i(10), "rb", autoclose: false) w = IO.for_fd($2.to_i(10), "wb", autoclose: false) @@ -145,6 +147,8 @@ def non_options(files, options) r.close if r nil else + r.close_on_exec = true + w.close_on_exec = true @jobserver = [r, w] options[:parallel] ||= 1 end @@ -184,15 +188,17 @@ def setup_options(opts, options) options[:retry] = false end - opts.on '--ruby VAL', "Path to ruby; It'll have used at -j option" do |a| + opts.on '--ruby VAL', "Path to ruby which is used at -j option" do |a| options[:ruby] = a.split(/ /).reject(&:empty?) end end class Worker def self.launch(ruby,args=[]) + scale = EnvUtil.timeout_scale io = IO.popen([*ruby, "-W1", "#{File.dirname(__FILE__)}/unit/parallel.rb", + *("--timeout-scale=#{scale}" if scale), *args], "rb+") new(io, io.pid, :waiting) end @@ -251,6 +257,8 @@ def quit return if @io.closed? @quit_called = true @io.puts "quit" + rescue Errno::EPIPE => e + warn "#{@pid}:#{@status.to_s.ljust(7)}:#{@file}: #{e.message}" end def kill @@ -290,13 +298,20 @@ def call_hook(id,*additional) end + def flush_job_tokens + if @jobserver + r, w = @jobserver.shift(2) + @jobserver = nil + w << @job_tokens.slice!(0..-1) + r.close + w.close + end + end + def after_worker_down(worker, e=nil, c=false) return unless @options[:parallel] return if @interrupt - if @jobserver - @jobserver[1] << @job_tokens - @job_tokens.clear - end + flush_job_tokens warn e if e real_file = worker.real_file and warn "running file: #{real_file}" @need_quit = true @@ -313,8 +328,8 @@ def after_worker_quit(worker) return unless @options[:parallel] return if @interrupt worker.close - if @jobserver and !@job_tokens.empty? - @jobserver[1] << @job_tokens.slice!(0) + if @jobserver and (token = @job_tokens.slice!(0)) + @jobserver[1] << token end @workers.delete(worker) @dead_workers << worker @@ -393,7 +408,7 @@ def deal(io, type, result, rep, shutting_down = false) end if @options[:separate] and not bang worker.quit - worker = add_worker + worker = launch_worker end worker.run(task, type) @test_count += 1 @@ -445,8 +460,7 @@ def _run_parallel suites, type, result return end - # Require needed things for parallel running - require 'thread' + # Require needed thing for parallel running require 'timeout' @tasks = @files.dup # Array of filenames. @need_quit = false @@ -488,6 +502,7 @@ def _run_parallel suites, type, result end quit_workers + flush_job_tokens unless @interrupt || !@options[:retry] || @need_quit parallel = @options[:parallel] @@ -572,7 +587,6 @@ def setup_options(opts, options) end end - private def _run_suites(suites, type) result = super report.reject!{|r| r.start_with? "Skipped:" } if @options[:hide_skip] @@ -694,26 +708,11 @@ def _prepare_run(suites, type) when :always color = true when :auto, nil - color = (@tty || @options[:job_status] == :replace) && /dumb/ !~ ENV["TERM"] + color = true if @tty || @options[:job_status] == :replace else color = false end - if color - # dircolors-like style - colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {} - begin - File.read(File.join(__dir__, "../../colors")).scan(/(\w+)=([^:\n]*)/) do |n, c| - colors[n] ||= c - end - rescue - end - @passed_color = "\e[;#{colors["pass"] || "32"}m" - @failed_color = "\e[;#{colors["fail"] || "31"}m" - @skipped_color = "\e[;#{colors["skip"] || "33"}m" - @reset_color = "\e[m" - else - @passed_color = @failed_color = @skipped_color = @reset_color = "" - end + @colorize = Colorize.new(color, colors_file: File.join(__dir__, "../../colors")) if color or @options[:job_status] == :replace @verbose = !options[:parallel] end @@ -737,9 +736,7 @@ def new_test(s) def update_status(s) count = @test_count.to_s(10).rjust(@total_tests.size) del_status_line(false) - print(@passed_color) - add_status("[#{count}/#{@total_tests}]") - print(@reset_color) + add_status(@colorize.pass("[#{count}/#{@total_tests}]")) add_status(" #{s}") $stdout.print "\r" if @options[:job_status] == :replace and !@verbose $stdout.flush @@ -758,14 +755,13 @@ def failed(s) del_status_line next end - color = @skipped_color + color = :skip else - color = @failed_color + color = :fail end - msg = msg.split(/$/, 2) - $stdout.printf("%s%s%3d) %s%s%s\n", - sep, color, @report_count += 1, - msg[0], @reset_color, msg[1]) + first, msg = msg.split(/$/, 2) + first = sprintf("%3d) %s", @report_count += 1, first) + $stdout.print(sep, @colorize.decorate(first, color), msg, "\n") sep = nil end report.clear @@ -835,7 +831,7 @@ def non_options(files, options) begin require "rbconfig" rescue LoadError - warn "#{caller(1)[0]}: warning: Parallel running disabled because can't get path to ruby; run specify with --ruby argument" + warn "#{caller(1, 1)[0]}: warning: Parallel running disabled because can't get path to ruby; run specify with --ruby argument" options[:parallel] = nil else options[:ruby] ||= [RbConfig.ruby] @@ -860,7 +856,8 @@ module GlobOption # :nodoc: all def setup_options(parser, options) super parser.separator "globbing options:" - parser.on '-b', '--basedir=DIR', 'Base directory of test suites.' do |dir| + parser.on '-B', '--base-directory DIR', 'Base directory to glob.' do |dir| + raise OptionParser::InvalidArgument, "not a directory: #{dir}" unless File.directory?(dir) options[:base_directory] = dir end parser.on '-x', '--exclude REGEXP', 'Exclude test files on pattern.' do |pattern| @@ -868,6 +865,18 @@ def setup_options(parser, options) end end + def complement_test_name f, orig_f + basename = File.basename(f) + + if /\.rb\z/ !~ basename + return File.join(File.dirname(f), basename+'.rb') + elsif /\Atest_/ !~ basename + return File.join(File.dirname(f), 'test_'+basename) + end if f.end_with?(basename) # otherwise basename is dirname/ + + raise ArgumentError, "file not found: #{orig_f}" + end + def non_options(files, options) paths = [options.delete(:base_directory), nil].uniq if reject = options.delete(:reject) @@ -875,26 +884,38 @@ def non_options(files, options) end files.map! {|f| f = f.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR - ((paths if /\A\.\.?(?:\z|\/)/ !~ f) || [nil]).any? do |prefix| - if prefix - path = f.empty? ? prefix : "#{prefix}/#{f}" - else - next if f.empty? - path = f - end - if !(match = (Dir["#{path}/**/#{@@testfile_prefix}_*.rb"] + Dir["#{path}/**/*_#{@@testfile_suffix}.rb"]).uniq).empty? - if reject - match.reject! {|n| - n[(prefix.length+1)..-1] if prefix - reject_pat =~ n - } + orig_f = f + while true + ret = ((paths if /\A\.\.?(?:\z|\/)/ !~ f) || [nil]).any? do |prefix| + if prefix + path = f.empty? ? prefix : "#{prefix}/#{f}" + else + next if f.empty? + path = f + end + if f.end_with?(File::SEPARATOR) or !f.include?(File::SEPARATOR) or File.directory?(path) + match = (Dir["#{path}/**/#{@@testfile_prefix}_*.rb"] + Dir["#{path}/**/*_#{@@testfile_suffix}.rb"]).uniq + else + match = Dir[path] end - break match - elsif !reject or reject_pat !~ f and File.exist? path - break path + if !match.empty? + if reject + match.reject! {|n| + n = n[(prefix.length+1)..-1] if prefix + reject_pat =~ n + } + end + break match + elsif !reject or reject_pat !~ f and File.exist? path + break path + end + end + if !ret + f = complement_test_name(f, orig_f) + else + break ret end - end or - raise ArgumentError, "file not found: #{f}" + end } files.flatten! super(files, options) @@ -1030,18 +1051,23 @@ def _run_suite(suite, type) end end - module SubprocessOption + module TimeoutOption def setup_options(parser, options) super - parser.separator "subprocess options:" - parser.on '--subprocess-timeout-scale NUM', "Scale subprocess timeout", Float do |scale| + parser.separator "timeout options:" + parser.on '--timeout-scale NUM', '--subprocess-timeout-scale NUM', "Scale timeout", Float do |scale| raise OptionParser::InvalidArgument, "timeout scale must be positive" unless scale > 0 options[:timeout_scale] = scale end + end + + def non_options(files, options) if scale = options[:timeout_scale] or - (scale = ENV["RUBY_TEST_SUBPROCESS_TIMEOUT_SCALE"] and (scale = scale.to_f) > 0) - EnvUtil.subprocess_timeout_scale = scale + (scale = ENV["RUBY_TEST_TIMEOUT_SCALE"] || ENV["RUBY_TEST_SUBPROCESS_TIMEOUT_SCALE"] and + (scale = scale.to_f) > 0) + EnvUtil.timeout_scale = scale end + super end end @@ -1056,9 +1082,17 @@ class Runner < MiniTest::Unit # :nodoc: all include Test::Unit::LoadPathOption include Test::Unit::GCStressOption include Test::Unit::ExcludesOption - include Test::Unit::SubprocessOption + include Test::Unit::TimeoutOption include Test::Unit::RunCount + def run(argv) + super + rescue NoMemoryError + system("cat /proc/meminfo") if File.exist?("/proc/meminfo") + system("ps x -opid,args,%cpu,%mem,nlwp,rss,vsz,wchan,stat,start,time,etime,blocked,caught,ignored,pending,f") if File.exist?("/bin/ps") + raise + end + class << self; undef autorun; end @@stop_auto_run = false @@ -1098,10 +1132,11 @@ class Runner < Test::Unit::Runner def initialize(force_standalone = false, default_dir = nil, argv = ARGV) @force_standalone = force_standalone @runner = Runner.new do |files, options| - options[:base_directory] ||= default_dir + base = options[:base_directory] ||= default_dir files << default_dir if files.empty? and default_dir @to_run = files yield self if block_given? + $LOAD_PATH.unshift base if base files end Runner.runner = @runner diff --git a/test/lib/test/unit/assertions.rb b/test/lib/test/unit/assertions.rb index 56f6ae5..48ee458 100644 --- a/test/lib/test/unit/assertions.rb +++ b/test/lib/test/unit/assertions.rb @@ -1,15 +1,12 @@ -# frozen_string_literal: false +# frozen_string_literal: true require 'minitest/unit' +require 'test/unit/core_assertions' require 'pp' module Test module Unit module Assertions - include MiniTest::Assertions - - def mu_pp(obj) #:nodoc: - obj.pretty_inspect.chomp - end + include Test::Unit::CoreAssertions MINI_DIR = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), "minitest") #:nodoc: @@ -51,105 +48,10 @@ def assert_block(*msgs) assert yield, *msgs 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 MiniTest::Skip => e - return e if exp.include? MiniTest::Skip - 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 { - exception_details(e, message(msg) {"#{mu_pp(exp)} exception expected, not"}.call) - } - - return e - end - - exp = exp.first if exp.size == 1 - - flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"}) - end - def assert_raises(*exp, &b) raise NoMethodError, "use assert_raise", caller 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 - # :call-seq: # assert_nothing_raised( *args, &block ) # @@ -217,35 +119,6 @@ def assert_nothing_thrown(msg=nil) ret 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_equal( expected, actual, failure_message = nil ) # @@ -452,7 +325,7 @@ def assert_not_send send_ary, m = nil ms = instance_methods(true).map {|sym| sym.to_s } ms.grep(/\Arefute_/) do |m| - mname = ('assert_not_' << m.to_s[/.*?_(.*)/, 1]) + mname = ('assert_not_'.dup << m.to_s[/.*?_(.*)/, 1]) alias_method(mname, m) unless ms.include? mname end alias assert_include assert_includes @@ -472,7 +345,7 @@ def assert_not_all?(obj, m = nil, &blk) failed = [] obj.each do |*a, &b| if blk.call(*a, &b) - failed << a.size > 1 ? a : a[0] + failed << (a.size > 1 ? a : a[0]) end end assert(failed.empty?, message(m) {failed.pretty_inspect}) @@ -481,8 +354,26 @@ def assert_not_all?(obj, m = nil, &blk) # compatibility with test-unit alias pend skip - def prepare_syntax_check(code, fname = caller_locations(2, 1)[0], mesg = fname.to_s, verbose: nil) - code = code.dup.force_encoding(Encoding::UTF_8) + 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 + 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 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 @@ -492,16 +383,22 @@ def prepare_syntax_check(code, fname = caller_locations(2, 1)[0], mesg = fname.t else line = 1 end - yield(code, fname, line, mesg) + 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) - prepare_syntax_check(code, *args) do |src, fname, line, mesg| + 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 - RubyVM::InstructionSequence.compile(src, fname, fname, line) + assert_equal(:ok, syntax_check(src, fname, line), mesg) end end end @@ -510,9 +407,10 @@ def assert_syntax_error(code, error, *args) prepare_syntax_check(code, *args) do |src, fname, line, mesg| yield if defined?(yield) e = assert_raise(SyntaxError, mesg) do - RubyVM::InstructionSequence.compile(src, fname, fname, line) + syntax_check(src, fname, line) end assert_match(error, e.message, mesg) + e end end @@ -527,152 +425,22 @@ def assert_normal_exit(testsrc, message = '', child_env: nil, **opt) assert !status.signaled?, FailDesc[status, message, out] end - FailDesc = proc do |status, message = "", out = ""| - pid = status.pid - now = Time.now - faildesc = proc do - if signo = status.termsig - signame = Signal.signame(signo) - sigdesc = "signal #{signo}" - end - log = EnvUtil.diagnostic_reports(signame, pid, now) - if signame - sigdesc = "SIG#{signame} (#{sigdesc})" - end - if status.coredump? - sigdesc << " (core dumped)" - end - full_message = '' - 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 << "\n" if /\n\z/ !~ full_message - end - if log - full_message << "\n#{log.b.gsub(/^/, '| ')}" - end - full_message - end - faildesc - end - - def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil, - success: nil, **opt) - stdout, stderr, status = EnvUtil.invoke_ruby(args, test_stdin, true, true, **opt) - if signo = status.termsig - EnvUtil.diagnostic_reports(Signal.signame(signo), status.pid, Time.now) - end - if block_given? - raise "test_stdout ignored, use block only or without block" if test_stdout != [] - raise "test_stderr ignored, use block only or without block" if test_stderr != [] - yield(stdout.lines.map {|l| l.chomp }, stderr.lines.map {|l| l.chomp }, status) - else - all_assertions(message) do |a| - [["stdout", test_stdout, stdout], ["stderr", test_stderr, stderr]].each do |key, exp, act| - a.for(key) do - if exp.is_a?(Regexp) - assert_match(exp, act) - elsif exp.all? {|e| String === e} - assert_equal(exp, act.lines.map {|l| l.chomp }) - else - assert_pattern_list(exp, act) - end - end - end - unless success.nil? - a.for("success?") do - if success - assert_predicate(status, :success?) - else - assert_not_predicate(status, :success?) - end - end - end - end - status - end - 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 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 - src = < marshal_error - ignore_stderr = nil - end - if 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 unless SystemExit === 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 - - def assert_warning(pat, msg = nil) + def assert_no_warning(pat, msg = nil) + result = nil stderr = EnvUtil.verbose_warning { EnvUtil.with_default_internal(pat.encoding) { - yield + result = yield } } msg = message(msg) {diff pat, stderr} - assert(pat === stderr, msg) - end - - def assert_warn(*args) - assert_warning(*args) {$VERBOSE = false; yield} + refute(pat === stderr, msg) + result 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 + skip 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? + require_relative '../../memory_status' raise MiniTest::Skip, "unsupported platform" unless defined?(Memory::Status) @@ -714,12 +482,43 @@ def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: fal skip end - def assert_is_minus_zero(f) - assert(1.0/f == -Float::INFINITY, "#{f} is not -0.0") + # kernel resolution can limit the minimum time we can measure + # [ruby-core:81540] + MIN_HZ = MiniTest::Unit::TestCase.windows? ? 67 : 100 + MIN_MEASURABLE = 1.0 / MIN_HZ + + def assert_cpu_usage_low(msg = nil, pct: 0.05, wait: 1.0, stop: nil) + require 'benchmark' + + wait = EnvUtil.apply_timeout_scale(wait) + if wait < 0.1 # TIME_QUANTUM_USEC in thread_pthread.c + warn "test #{msg || 'assert_cpu_usage_low'} too short to be accurate" + end + tms = Benchmark.measure(msg || '') do + if stop + th = Thread.start {sleep wait; stop.call} + yield + th.join + else + begin + Timeout.timeout(wait) {yield} + rescue Timeout::Error + end + end + end + + max = pct * tms.real + min_measurable = MIN_MEASURABLE + min_measurable *= 1.30 # add a little (30%) to account for misc. overheads + if max < min_measurable + max = min_measurable + end + + assert_operator tms.total, :<=, max, msg end - def assert_file - AssertFile + def assert_is_minus_zero(f) + assert(1.0/f == -Float::INFINITY, "#{f} is not -0.0") end # pattern_list is an array which contains regexp and :*. @@ -743,13 +542,13 @@ def assert_pattern_list(pattern_list, actual, message=nil) msg = message(msg) { expect_msg = "Expected #{mu_pp pattern}\n" if /\n[^\n]/ =~ rest - actual_mesg = "to match\n" + 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}" + actual_mesg = "to match " + mu_pp(rest) end actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters" expect_msg + actual_mesg @@ -765,119 +564,18 @@ def assert_pattern_list(pattern_list, actual, message=nil) 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, $!] - end - end - if !errs.empty? - msg = "exceptions on #{errs.length} threads:\n" + - errs.map {|t, err| - "#{t.inspect}:\n" + - err.backtrace.map.with_index {|line, i| - if i == 0 - "#{line}: #{err.message} (#{err.class})" - else - "\tfrom #{line}" - end - }.join("\n") - }.join("\n---\n") - if message - msg = "#{message}\n#{msg}" - end - raise MiniTest::Assertion, msg - end - values - end - - class << (AssertFile = Struct.new(:failure_message).new) - include Assertions - 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 " << 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 - rescue Exception => e - @failures[key] = [@count, e] - end - - def message - i = 0 - total = @count.to_s - fmt = "%#{total.size}d" - @failures.map {|k, (n, v)| - "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.message.b.gsub(/^/, ' | ')}" - }.join("\n") - end - - def pass? - @failures.empty? - end - end - - def assert_all_assertions(msg = nil) + def assert_all_assertions_foreach(msg = nil, *keys, &block) all = AllFailures.new - yield all + all.foreach(*keys, &block) ensure assert(all.pass?, message(msg) {all.message.chomp(".")}) end - alias all_assertions assert_all_assertions + alias all_assertions_foreach assert_all_assertions_foreach def build_message(head, template=nil, *arguments) #:nodoc: template &&= template.chomp template.gsub(/\G((?:[^\\]|\\.)*?)(\\)?\?/) { $1 + ($2 ? "?" : mu_pp(arguments.shift)) } end - - def message(msg = nil, *args, &default) # :nodoc: - if Proc === msg - super(nil, *args) do - ary = [msg.call, (default.call if default)].compact.reject(&:empty?) - if 1 < ary.length - ary[0...-1] = ary[0...-1].map {|str| str.sub(/(? marshal_error + ignore_stderr = nil + end + if 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 unless SystemExit === 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 + + # :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 MiniTest::Skip => e + return e if exp.include? MiniTest::Skip + 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 { + exception_details(e, message(msg) {"#{mu_pp(exp)} exception expected, not"}.call) + } + + 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 + + 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 + + class << (AssertFile = Struct.new(:failure_message).new) + 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 + 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.full_message(highlight: false, order: :top) + }.join("\n---\n") + if message + msg = "#{message}\n#{msg}" + end + raise MiniTest::Assertion, msg + end + 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 message(msg = nil, *args, &default) # :nodoc: + if Proc === msg + super(nil, *args) do + ary = [msg.call, (default.call if default)].compact.reject(&:empty?) + if 1 < ary.length + ary[0...-1] = ary[0...-1].map {|str| str.sub(/(?