diff options
Diffstat (limited to 'tool/lib')
-rw-r--r-- | tool/lib/_tmpdir.rb | 100 | ||||
-rw-r--r-- | tool/lib/bundled_gem.rb | 118 | ||||
-rw-r--r-- | tool/lib/colorize.rb | 35 | ||||
-rw-r--r-- | tool/lib/core_assertions.rb | 264 | ||||
-rw-r--r-- | tool/lib/envutil.rb | 81 | ||||
-rw-r--r-- | tool/lib/gc_checker.rb | 36 | ||||
-rw-r--r-- | tool/lib/gc_compact_checker.rb | 10 | ||||
-rw-r--r-- | tool/lib/iseq_loader_checker.rb | 9 | ||||
-rw-r--r-- | tool/lib/leakchecker.rb | 34 | ||||
-rw-r--r-- | tool/lib/memory_status.rb | 8 | ||||
-rw-r--r-- | tool/lib/minitest/README.txt | 457 | ||||
-rw-r--r-- | tool/lib/minitest/unit.rb | 1479 | ||||
-rw-r--r-- | tool/lib/output.rb | 70 | ||||
-rw-r--r-- | tool/lib/path.rb | 101 | ||||
-rw-r--r-- | tool/lib/profile_test_all.rb | 2 | ||||
-rw-r--r-- | tool/lib/test/unit.rb | 955 | ||||
-rw-r--r-- | tool/lib/test/unit/assertions.rb | 564 | ||||
-rw-r--r-- | tool/lib/test/unit/parallel.rb | 45 | ||||
-rw-r--r-- | tool/lib/test/unit/testcase.rb | 286 | ||||
-rw-r--r-- | tool/lib/vcs.rb | 231 | ||||
-rw-r--r-- | tool/lib/vpath.rb | 7 | ||||
-rw-r--r-- | tool/lib/webrick/httprequest.rb | 2 | ||||
-rw-r--r-- | tool/lib/webrick/httpserver.rb | 1 | ||||
-rw-r--r-- | tool/lib/webrick/httputils.rb | 2 |
24 files changed, 2601 insertions, 2296 deletions
diff --git a/tool/lib/_tmpdir.rb b/tool/lib/_tmpdir.rb new file mode 100644 index 0000000000..fd429dab37 --- /dev/null +++ b/tool/lib/_tmpdir.rb @@ -0,0 +1,100 @@ +template = "rubytest." + +# This path is only for tests. +# Assume the directory by these environment variables are safe. +base = [ENV["TMPDIR"], ENV["TMP"], "/tmp"].find do |tmp| + next unless tmp and tmp.size <= 50 and File.directory?(tmp) + # On macOS, the default TMPDIR is very long, inspite of UNIX socket + # path length is limited. + # + # Also Rubygems creates its own temporary directory per tests, and + # some tests copy the full path of gemhome there. In that caes, the + # path contains both temporary names twice, and can exceed path name + # limit very easily. + tmp +end +begin + tmpdir = File.join(base, template + Random.new_seed.to_s(36)[-6..-1]) + Dir.mkdir(tmpdir, 0o700) +rescue Errno::EEXIST + retry +end +# warn "tmpdir(#{tmpdir.size}) = #{tmpdir}" + +pid = $$ +END { + if pid == $$ + begin + Dir.rmdir(tmpdir) + rescue Errno::ENOENT + rescue Errno::ENOTEMPTY + require_relative "colorize" + colorize = Colorize.new + ls = Struct.new(:colorize) do + def mode_inspect(m, s) + [ + (m & 0o4 == 0 ? ?- : ?r), + (m & 0o2 == 0 ? ?- : ?w), + (m & 0o1 == 0 ? (s ? s.upcase : ?-) : (s || ?x)), + ] + end + def decorate_path(path, st) + case + when st.directory? + color = "bold;blue" + type = "/" + when st.symlink? + color = "bold;cyan" + # type = "@" + when st.executable? + color = "bold;green" + type = "*" + when path.end_with?(".gem") + color = "green" + end + colorize.decorate(path, color) + (type || "") + end + def list_tree(parent, indent = "", &block) + children = Dir.children(parent).map do |child| + [child, path = File.join(parent, child), File.lstat(path)] + end + nlink_width = children.map {|child, path, st| st.nlink}.max.to_s.size + size_width = children.map {|child, path, st| st.size}.max.to_s.size + + children.each do |child, path, st| + m = st.mode + m = [ + (st.file? ? ?- : st.ftype[0]), + mode_inspect(m >> 6, (?s unless m & 04000 == 0)), + mode_inspect(m >> 3, (?s unless m & 02000 == 0)), + mode_inspect(m, (?t unless m & 01000 == 0)), + ].join("") + warn sprintf("%s* %s %*d %*d %s % s%s", + indent, m, nlink_width, st.nlink, size_width, st.size, + st.mtime.to_s, decorate_path(child, st), + (" -> " + decorate_path(File.readlink(path), File.stat(path)) if + st.symlink?)) + if st.directory? + list_tree(File.join(parent, child), indent + " ", &block) + end + yield path, st if block + end + end + end.new(colorize) + warn colorize.notice("Children under ")+colorize.fail(tmpdir)+":" + Dir.chdir(tmpdir) do + ls.list_tree(".") do |path, st| + if st.directory? + Dir.rmdir(path) + else + File.unlink(path) + end + end + end + require "fileutils" + FileUtils.rm_rf(tmpdir) + end + end +} + +ENV["TMPDIR"] = ENV["SPEC_TEMP_DIR"] = ENV["GEM_TEST_TMPDIR"] = tmpdir diff --git a/tool/lib/bundled_gem.rb b/tool/lib/bundled_gem.rb new file mode 100644 index 0000000000..3ba27f6d64 --- /dev/null +++ b/tool/lib/bundled_gem.rb @@ -0,0 +1,118 @@ +require 'fileutils' +require 'rubygems' +require 'rubygems/package' + +# This library is used by "make extract-gems" to +# unpack bundled gem files. + +module BundledGem + DEFAULT_GEMS_DEPENDENCIES = [ + "net-protocol", # net-ftp + "time", # net-ftp + "singleton", # prime + "ipaddr", # rinda + "forwardable" # prime, rinda + ] + + module_function + + def unpack(file, *rest) + pkg = Gem::Package.new(file) + prepare_test(pkg.spec, *rest) {|dir| pkg.extract_files(dir)} + puts "Unpacked #{file}" + rescue Gem::Package::FormatError, Errno::ENOENT + puts "Try with hash version of bundled gems instead of #{file}. We don't use this gem with release version of Ruby." + if file =~ /^gems\/(\w+)-/ + file = Dir.glob("gems/#{$1}-*.gem").first + end + retry + end + + def build(gemspec, version, outdir = ".", validation: true) + outdir = File.expand_path(outdir) + gemdir, gemfile = File.split(gemspec) + Dir.chdir(gemdir) do + spec = Gem::Specification.load(gemfile) + abort "Failed to load #{gemspec}" unless spec + output = File.join(outdir, spec.file_name) + FileUtils.rm_rf(output) + package = Gem::Package.new(output) + package.spec = spec + package.build(validation == false) + end + end + + def copy(path, *rest) + path, n = File.split(path) + spec = Dir.chdir(path) {Gem::Specification.load(n)} or raise "Cannot load #{path}" + prepare_test(spec, *rest) do |dir| + FileUtils.rm_rf(dir) + files = spec.files.reject {|f| f.start_with?(".git")} + dirs = files.map {|f| File.dirname(f) if f.include?("/")}.uniq + FileUtils.mkdir_p(dirs.map {|d| d ? "#{dir}/#{d}" : dir}.sort_by {|d| d.count("/")}) + files.each do |f| + File.copy_stream(File.join(path, f), File.join(dir, f)) + end + end + puts "Copied #{path}" + end + + def prepare_test(spec, dir = ".") + target = spec.full_name + Gem.ensure_gem_subdirectories(dir) + gem_dir = File.join(dir, "gems", target) + yield gem_dir + spec_dir = spec.extensions.empty? ? "specifications" : File.join("gems", target) + if spec.extensions.empty? + spec.dependencies.reject! {|dep| DEFAULT_GEMS_DEPENDENCIES.include?(dep.name)} + end + File.binwrite(File.join(dir, spec_dir, "#{target}.gemspec"), spec.to_ruby) + unless spec.extensions.empty? + spec.dependencies.clear + File.binwrite(File.join(dir, spec_dir, ".bundled.#{target}.gemspec"), spec.to_ruby) + end + if spec.bindir and spec.executables + bindir = File.join(dir, "bin") + Dir.mkdir(bindir) rescue nil + spec.executables.each do |exe| + File.open(File.join(bindir, exe), "wb", 0o777) {|f| + f.print "#!ruby\n", + %[load File.realpath("../gems/#{target}/#{spec.bindir}/#{exe}", __dir__)\n] + } + end + end + FileUtils.rm_rf(Dir.glob("#{gem_dir}/.git*")) + end + + def dummy_gemspec(gemspec) + return if File.exist?(gemspec) + gemdir, gemfile = File.split(gemspec) + Dir.chdir(gemdir) do + spec = Gem::Specification.new do |s| + s.name = gemfile.chomp(".gemspec") + s.version = File.read("lib/#{s.name}.rb")[/VERSION = "(.+?)"/, 1] + s.authors = ["DUMMY"] + s.email = ["dummy@ruby-lang.org"] + s.files = Dir.glob("{lib,ext}/**/*").select {|f| File.file?(f)} + s.licenses = ["Ruby"] + s.description = "DO NOT USE; dummy gemspec only for test" + s.summary = "(dummy gemspec)" + end + File.write(gemfile, spec.to_ruby) + end + end + + def checkout(gemdir, repo, rev, git: $git) + return unless rev or !git or git.empty? + unless File.exist?("#{gemdir}/.git") + puts "Cloning #{repo}" + command = "#{git} clone #{repo} #{gemdir}" + system(command) or raise "failed: #{command}" + end + puts "Update #{File.basename(gemdir)} to #{rev}" + command = "#{git} fetch origin #{rev}" + system(command, chdir: gemdir) or raise "failed: #{command}" + command = "#{git} checkout --detach #{rev}" + system(command, chdir: gemdir) or raise "failed: #{command}" + end +end diff --git a/tool/lib/colorize.rb b/tool/lib/colorize.rb index 11b878d318..0904312119 100644 --- a/tool/lib/colorize.rb +++ b/tool/lib/colorize.rb @@ -6,8 +6,8 @@ class Colorize # Colorize.new(color: color, colors_file: colors_file) def initialize(color = nil, opts = ((_, color = color, nil)[0] if Hash === color)) @colors = @reset = nil - @color = (opts[:color] if opts) - if color or (color == nil && STDOUT.tty?) + @color = opts && opts[:color] || color + if color or (color == nil && coloring?) if (%w[smso so].any? {|attr| /\A\e\[.*m\z/ =~ IO.popen("tput #{attr}", "r", :err => IO::NULL, &:read)} rescue nil) @beg = "\e[" colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {} @@ -27,21 +27,48 @@ class Colorize end DEFAULTS = { - "pass"=>"32", "fail"=>"31;1", "skip"=>"33;1", + # color names "black"=>"30", "red"=>"31", "green"=>"32", "yellow"=>"33", "blue"=>"34", "magenta"=>"35", "cyan"=>"36", "white"=>"37", "bold"=>"1", "underline"=>"4", "reverse"=>"7", + "bright_black"=>"90", "bright_red"=>"91", "bright_green"=>"92", "bright_yellow"=>"93", + "bright_blue"=>"94", "bright_magenta"=>"95", "bright_cyan"=>"96", "bright_white"=>"97", + + # abstract decorations + "pass"=>"green", "fail"=>"red;bold", "skip"=>"yellow;bold", + "note"=>"bright_yellow", "notice"=>"bright_yellow", "info"=>"bright_magenta", } + def coloring? + STDOUT.tty? && (!(nc = ENV['NO_COLOR']) || nc.empty?) + end + # colorize.decorate(str, name = color_name) def decorate(str, name = @color) - if @colors and color = (@colors[name] || DEFAULTS[name]) + if coloring? and color = resolve_color(name) "#{@beg}#{color}m#{str}#{@reset}" else str end end + def resolve_color(color = @color, seen = {}, colors = nil) + return unless @colors + color.to_s.gsub(/\b[a-z][\w ]+/) do |n| + n.gsub!(/\W+/, "_") + n.downcase! + c = seen[n] and next c + if colors + c = colors[n] + elsif (c = (tbl = @colors)[n] || (tbl = DEFAULTS)[n]) + colors = tbl + else + next n + end + seen[n] = resolve_color(c, seen, colors) + end + end + DEFAULTS.each_key do |name| define_method(name) {|str| decorate(str, name) diff --git a/tool/lib/core_assertions.rb b/tool/lib/core_assertions.rb index c0c69e0ab5..b456a55b34 100644 --- a/tool/lib/core_assertions.rb +++ b/tool/lib/core_assertions.rb @@ -1,8 +1,49 @@ # frozen_string_literal: true module Test + + class << self + ## + # Filter object for backtraces. + + attr_accessor :backtrace_filter + end + + class BacktraceFilter # :nodoc: + def filter bt + return ["No backtrace"] unless bt + + new_bt = [] + pattern = %r[/(?:lib\/test/|core_assertions\.rb:)] + + unless $DEBUG then + bt.each do |line| + break if pattern.match?(line) + new_bt << line + end + + new_bt = bt.reject { |line| pattern.match?(line) } if new_bt.empty? + new_bt = bt.dup if new_bt.empty? + else + new_bt = bt.dup + end + + new_bt + end + end + + self.backtrace_filter = BacktraceFilter.new + + def self.filter_backtrace bt # :nodoc: + backtrace_filter.filter bt + end + module Unit module Assertions + def assert_raises(*exp, &b) + raise NoMethodError, "use assert_raise", caller + end + def _assertions= n # :nodoc: @_assertions = n end @@ -16,32 +57,30 @@ module Test def message msg = nil, ending = nil, &default proc { - msg = msg.call.chomp(".") if Proc === msg - custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty? - "#{custom_message}#{default.call}#{ending || "."}" + ending ||= (ending_pattern = /(?<!\.)\z/; ".") + ending_pattern ||= /(?<!#{Regexp.quote(ending)})\z/ + msg = msg.call if Proc === msg + ary = [msg, (default.call if default)].compact.reject(&:empty?) + ary.map! {|str| str.to_s.sub(ending_pattern, ending) } + begin + ary.join("\n") + rescue Encoding::CompatibilityError + ary.map(&:b).join("\n") + end } end 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 + require_relative 'envutil' + require 'pp' + begin + require '-test-/asan' + rescue LoadError end + nil.pretty_inspect + def mu_pp(obj) #:nodoc: obj.pretty_inspect.chomp end @@ -114,19 +153,22 @@ module Test 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 MiniTest::Skip, "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) + # TODO: consider choosing some appropriate limit for RJIT and stop skipping this once it does not randomly fail + pend 'assert_no_memory_leak may consider RJIT memory usage as leak' if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? + # For previous versions which implemented MJIT + pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? + # ASAN has the same problem - its shadow memory greatly increases memory usage + # (plus asan has better ways to detect memory leaks than this assertion) + pend 'assert_no_memory_leak may consider ASAN memory usage as leak' if defined?(Test::ASAN) && Test::ASAN.enabled? + + require_relative 'memory_status' + raise Test::Unit::PendedError, "unsupported platform" unless defined?(Memory::Status) + + token_dump, token_re = new_test_token envs = args.shift if Array === args and Hash === args.first args = [ "--disable=gems", - "-r", File.expand_path("../../../memory_status", __FILE__), + "-r", File.expand_path("../memory_status", __FILE__), *args, "-v", "-", ] @@ -182,27 +224,15 @@ module Test msg = args.pop end begin - line = __LINE__; yield - rescue MiniTest::Skip + yield + rescue Test::Unit::PendedError, *(Test::Unit::AssertionFailedError if args.empty?) raise - rescue Exception => e - bt = e.backtrace - as = e.instance_of?(MiniTest::Assertion) - 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 MiniTest::Assertion, msg.call, bt - else - raise - end + 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 @@ -259,13 +289,17 @@ module Test ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM") - def separated_runner(out = nil) + 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 [Marshal.dump($!)].pack('m'), "assertions=\#{self._assertions}" + out.puts "#{token}<error>", [Marshal.dump($!)].pack('m'), "#{token}</error>", "#{token}assertions=#{self._assertions}" } - Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) if defined?(Test::Unit::Runner) + 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) @@ -275,22 +309,24 @@ module Test line ||= loc.lineno end capture_stdout = true - unless /mswin|mingw/ =~ RUBY_PLATFORM + unless /mswin|mingw/ =~ RbConfig::CONFIG['host_os'] capture_stdout = false - opt[:out] = MiniTest::Unit.output if defined?(MiniTest::Unit) + 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 = <<eom # -*- coding: #{line += __LINE__; src.encoding}; -*- BEGIN { - require "test/unit";include Test::Unit::Assertions;require #{(__dir__ + "/core_assertions").dump};include Test::Unit::CoreAssertions - separated_runner #{res_c&.fileno} + require "test/unit";include Test::Unit::Assertions;require #{__FILE__.dump};include Test::Unit::CoreAssertions + separated_runner #{token_dump}, #{res_c&.fileno || 'nil'} } #{line -= __LINE__; src} eom args = args.dup args.insert((Hash === args.first ? 1 : 0), "-w", "--disable=gems", *$:.map {|l| "-I#{l}"}) + args << "--debug" if RUBY_ENGINE == 'jruby' # warning: tracing (e.g. set_trace_func) will not capture all events without --debug flag stdout, stderr, status = EnvUtil.invoke_ruby(args, src, capture_stdout, true, **opt) ensure if res_c @@ -303,9 +339,9 @@ eom raise if $! abort = status.coredump? || (status.signaled? && ABORT_SIGNALS.include?(status.termsig)) assert(!abort, FailDesc[status, nil, stderr]) - self._assertions += res[/^assertions=(\d+)/, 1].to_i + self._assertions += res[/^#{token_re}assertions=(\d+)/, 1].to_i begin - res = Marshal.load(res.unpack1("m")) + res = Marshal.load(res[/^#{token_re}<error>\n\K.*\n(?=#{token_re}<\/error>$)/m].unpack1("m")) rescue => marshal_error ignore_stderr = nil res = nil @@ -402,8 +438,8 @@ eom 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| @@ -478,7 +514,7 @@ eom ex end - MINI_DIR = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), "minitest") #:nodoc: + TEST_DIR = File.join(__dir__, "test/unit") #:nodoc: # :call-seq: # assert(test, [failure_message]) @@ -498,7 +534,7 @@ eom when nil msgs.shift else - bt = caller.reject { |s| s.start_with?(MINI_DIR) } + 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 @@ -521,7 +557,7 @@ eom return assert obj.respond_to?(meth, *priv), msg end #get rid of overcounting - if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) + if caller_locations(1, 1)[0].path.start_with?(TEST_DIR) return if obj.respond_to?(meth) end super(obj, meth, msg) @@ -544,17 +580,17 @@ eom return assert !obj.respond_to?(meth, *priv), msg end #get rid of overcounting - if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) + 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 and :*. + # pattern_list is an array which contains regexp, string and :*. # :* means any sequence. # # pattern_list is anchored. - # Use [:*, regexp, :*] for non-anchored match. + # Use [:*, regexp/string, :*] for non-anchored match. def assert_pattern_list(pattern_list, actual, message=nil) rest = actual anchored = true @@ -563,11 +599,13 @@ eom anchored = false else if anchored - match = /\A#{pattern}/.match(rest) + match = rest.rindex(pattern, 0) else - match = pattern.match(rest) + match = rest.index(pattern) end - unless match + 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 @@ -584,7 +622,7 @@ eom } assert false, msg end - rest = match.post_match + rest = post_match anchored = true end } @@ -611,19 +649,20 @@ eom def assert_deprecated_warning(mesg = /deprecated/) assert_warning(mesg) do - Warning[:deprecated] = true + Warning[:deprecated] = true if Warning.respond_to?(:[]=) yield end end def assert_deprecated_warn(mesg = /deprecated/) assert_warn(mesg) do - Warning[:deprecated] = true + 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 @@ -655,7 +694,7 @@ eom def for(key) @count += 1 - yield + yield key rescue Exception => e @failures[key] = [@count, e] end @@ -709,12 +748,12 @@ eom msg = "exceptions on #{errs.length} threads:\n" + errs.map {|t, err| "#{t.inspect}:\n" + - RUBY_VERSION >= "2.5.0" ? err.full_message(highlight: false, order: :top) : err.message + (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 MiniTest::Assertion, msg + raise Test::Unit::AssertionFailedError, msg end end @@ -736,21 +775,67 @@ eom 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(/(?<!\.)\z/, '.') } - end + 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 |c| + if Process.const_defined?(c) + [c.to_sym, Process.const_get(c)].find do |clk| begin - ary.join("\n") - rescue Encoding::CompatibilityError - ary.map(&:b).join("\n") + Process.clock_gettime(clk) + rescue + # Constants may be defined but not implemented, e.g., mingw. + else + PERFORMANCE_CLOCK = clk end end - else - super + 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 @@ -769,6 +854,11 @@ eom 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/tool/lib/envutil.rb b/tool/lib/envutil.rb index 0391b90c1c..642965047f 100644 --- a/tool/lib/envutil.rb +++ b/tool/lib/envutil.rb @@ -15,23 +15,22 @@ 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) + ruby + elsif defined?(RbConfig.ruby) RbConfig.ruby else + 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 "ruby" end end @@ -53,7 +52,14 @@ module EnvUtil @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 + @original_warning = + if defined?(Warning.categories) + Warning.categories.to_h {|i| [i, Warning[i]]} + elsif defined?(Warning.[]) # 2.7+ + %i[deprecated experimental performance].to_h do |i| + [i, begin Warning[i]; rescue ArgumentError; end] + end.compact + end end end @@ -152,7 +158,12 @@ module EnvUtil 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'] + + # remain env + %w(ASAN_OPTIONS RUBY_ON_BUG).each{|name| + child_env[name] = ENV[name] if !child_env.key?(name) and ENV.key?(name) + } + args = [args] if args.kind_of?(String) pid = spawn(child_env, *precommand, rubybin, *args, opt) in_c.close @@ -241,6 +252,24 @@ module EnvUtil end module_function :under_gc_stress + def under_gc_compact_stress(val = :empty, &block) + raise "compaction doesn't work well on s390x. Omit the test in the caller." if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077 + auto_compact = GC.auto_compact + GC.auto_compact = val + under_gc_stress(&block) + ensure + GC.auto_compact = auto_compact + end + module_function :under_gc_compact_stress + + def without_gc + prev_disabled = GC.disable + yield + ensure + GC.enable unless prev_disabled + end + module_function :without_gc + def with_default_external(enc) suppress_warning { Encoding.default_external = enc } yield @@ -292,16 +321,24 @@ module EnvUtil 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" + 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 - if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log - File.unlink(name) - File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil - return log + 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 diff --git a/tool/lib/gc_checker.rb b/tool/lib/gc_checker.rb new file mode 100644 index 0000000000..719da8cac0 --- /dev/null +++ b/tool/lib/gc_checker.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module GCDisabledChecker + def before_setup + if @__gc_disabled__ = GC.enable # return true if GC is disabled + GC.disable + end + + super + end + + def after_teardown + super + + disabled = GC.enable + GC.disable if @__gc_disabled__ + + if @__gc_disabled__ != disabled + label = { + true => 'disabled', + false => 'enabled', + } + raise "GC was #{label[@__gc_disabled__]}, but is #{label[disabled]} after the test." + end + end +end + +module GCCompactChecker + def after_teardown + super + GC.compact + end +end + +Test::Unit::TestCase.include GCDisabledChecker +Test::Unit::TestCase.include GCCompactChecker if ENV['RUBY_TEST_GC_COMPACT'] diff --git a/tool/lib/gc_compact_checker.rb b/tool/lib/gc_compact_checker.rb deleted file mode 100644 index a6ef3d3ce8..0000000000 --- a/tool/lib/gc_compact_checker.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -module GCCompactChecker - def after_teardown - super - GC.compact - end -end - -Test::Unit::TestCase.include GCCompactChecker if ENV['RUBY_TEST_GC_COMPACT'] diff --git a/tool/lib/iseq_loader_checker.rb b/tool/lib/iseq_loader_checker.rb index 3f07b3a999..73784f8450 100644 --- a/tool/lib/iseq_loader_checker.rb +++ b/tool/lib/iseq_loader_checker.rb @@ -76,6 +76,15 @@ class RubyVM::InstructionSequence # return value i2_bin if CHECK_TO_BINARY end if CHECK_TO_A || CHECK_TO_BINARY + + if opt == "prism" + # If RUBY_ISEQ_DUMP_DEBUG is "prism", we'll set up + # InstructionSequence.load_iseq to intercept loading filepaths to compile + # using prism. + def self.load_iseq(filepath) + RubyVM::InstructionSequence.compile_file_prism(filepath) + end + end end #require_relative 'x'; exit(1) diff --git a/tool/lib/leakchecker.rb b/tool/lib/leakchecker.rb index 1d45a3a8df..4cd28b9dd5 100644 --- a/tool/lib/leakchecker.rb +++ b/tool/lib/leakchecker.rb @@ -112,7 +112,7 @@ class LeakChecker } unless fd_leaked.empty? unless @@try_lsof == false - @@try_lsof |= system(*%W[lsof -a -d #{fd_leaked.minmax.uniq.join("-")} -p #$$], out: MiniTest::Unit.output) + @@try_lsof |= system(*%W[lsof -a -d #{fd_leaked.minmax.uniq.join("-")} -p #$$], out: Test::Unit::Runner.output) end end h.each {|fd, list| @@ -182,7 +182,8 @@ class LeakChecker def find_threads Thread.list.find_all {|t| - t != Thread.current && t.alive? + t != Thread.current && t.alive? && + !(t.thread_variable?(:"\0__detached_thread__") && t.thread_variable_get(:"\0__detached_thread__")) } end @@ -209,15 +210,36 @@ class LeakChecker return leaked end - def find_env - ENV.to_h + e = ENV["_Ruby_Env_Ignorecase_"], ENV["_RUBY_ENV_IGNORECASE_"] + begin + ENV["_Ruby_Env_Ignorecase_"] = ENV["_RUBY_ENV_IGNORECASE_"] = nil + ENV["_RUBY_ENV_IGNORECASE_"] = "ENV_CASE_TEST" + ENV_IGNORECASE = ENV["_Ruby_Env_Ignorecase_"] == "ENV_CASE_TEST" + ensure + ENV["_Ruby_Env_Ignorecase_"], ENV["_RUBY_ENV_IGNORECASE_"] = e + end + + if ENV_IGNORECASE + def find_env + ENV.to_h {|k, v| [k.upcase, v]} + end + else + def find_env + ENV.to_h + end end def check_env(test_name) old_env = @env_info - new_env = ENV.to_h + new_env = find_env return false if old_env == new_env + if defined?(Bundler::EnvironmentPreserver) + bundler_prefix = Bundler::EnvironmentPreserver::BUNDLER_PREFIX + end (old_env.keys | new_env.keys).sort.each {|k| + # Don't report changed environment variables caused by Bundler's backups + next if bundler_prefix and k.start_with?(bundler_prefix) + if old_env.has_key?(k) if new_env.has_key?(k) if old_env[k] != new_env[k] @@ -286,7 +308,7 @@ class LeakChecker end def puts(*a) - output = MiniTest::Unit.output + output = Test::Unit::Runner.output if defined?(output.set_encoding) output.set_encoding(nil, nil) end diff --git a/tool/lib/memory_status.rb b/tool/lib/memory_status.rb index ad002b2dda..60632523a8 100644 --- a/tool/lib/memory_status.rb +++ b/tool/lib/memory_status.rb @@ -12,7 +12,7 @@ module Memory PROC_FILE = procfile VM_PAT = pat def self.read_status - IO.foreach(PROC_FILE, encoding: Encoding::ASCII_8BIT) do |l| + File.foreach(PROC_FILE, encoding: Encoding::ASCII_8BIT) do |l| yield($1.downcase.intern, $2.to_i * 1024) if VM_PAT =~ l end end @@ -56,11 +56,12 @@ module Memory end end - keys << :peak << :size + keys.push(:size, :rss, :peak) def self.read_status if info = Win32.memory_info - yield :peak, info.PeakPagefileUsage yield :size, info.PagefileUsage + yield :rss, info.WorkingSetSize + yield :peak, info.PeakWorkingSetSize end end when (require_relative 'find_executable' @@ -94,6 +95,7 @@ if defined?(Memory::Status) Memory.read_status do |key, val| self[key] = val end + self end unless method_defined?(:_update) Header = members.map {|k| k.to_s.upcase.rjust(6)}.join('') diff --git a/tool/lib/minitest/README.txt b/tool/lib/minitest/README.txt deleted file mode 100644 index 368cc3aa4e..0000000000 --- a/tool/lib/minitest/README.txt +++ /dev/null @@ -1,457 +0,0 @@ -= minitest/{unit,spec,mock,benchmark} - -home :: https://github.com/seattlerb/minitest -rdoc :: http://docs.seattlerb.org/minitest -vim :: https://github.com/sunaku/vim-ruby-minitest - -== DESCRIPTION: - -minitest provides a complete suite of testing facilities supporting -TDD, BDD, mocking, and benchmarking. - - "I had a class with Jim Weirich on testing last week and we were - allowed to choose our testing frameworks. Kirk Haines and I were - paired up and we cracked open the code for a few test - frameworks... - - I MUST say that minitest is *very* readable / understandable - compared to the 'other two' options we looked at. Nicely done and - thank you for helping us keep our mental sanity." - - -- Wayne E. Seguin - -minitest/unit is a small and incredibly fast unit testing framework. -It provides a rich set of assertions to make your tests clean and -readable. - -minitest/spec is a functionally complete spec engine. It hooks onto -minitest/unit and seamlessly bridges test assertions over to spec -expectations. - -minitest/benchmark is an awesome way to assert the performance of your -algorithms in a repeatable manner. Now you can assert that your newb -co-worker doesn't replace your linear algorithm with an exponential -one! - -minitest/mock by Steven Baker, is a beautifully tiny mock (and stub) -object framework. - -minitest/pride shows pride in testing and adds coloring to your test -output. I guess it is an example of how to write IO pipes too. :P - -minitest/unit is meant to have a clean implementation for language -implementors that need a minimal set of methods to bootstrap a working -test suite. For example, there is no magic involved for test-case -discovery. - - "Again, I can't praise enough the idea of a testing/specing - framework that I can actually read in full in one sitting!" - - -- Piotr Szotkowski - -Comparing to rspec: - - rspec is a testing DSL. minitest is ruby. - - -- Adam Hawkins, "Bow Before MiniTest" - -minitest doesn't reinvent anything that ruby already provides, like: -classes, modules, inheritance, methods. This means you only have to -learn ruby to use minitest and all of your regular OO practices like -extract-method refactorings still apply. - -== FEATURES/PROBLEMS: - -* minitest/autorun - the easy and explicit way to run all your tests. -* minitest/unit - a very fast, simple, and clean test system. -* minitest/spec - a very fast, simple, and clean spec system. -* minitest/mock - a simple and clean mock/stub system. -* minitest/benchmark - an awesome way to assert your algorithm's performance. -* minitest/pride - show your pride in testing! -* Incredibly small and fast runner, but no bells and whistles. - -== RATIONALE: - -See design_rationale.rb to see how specs and tests work in minitest. - -== SYNOPSIS: - -Given that you'd like to test the following class: - - class Meme - def i_can_has_cheezburger? - "OHAI!" - end - - def will_it_blend? - "YES!" - end - end - -=== Unit tests - - require 'minitest/autorun' - - class TestMeme < MiniTest::Unit::TestCase - def setup - @meme = Meme.new - end - - def test_that_kitty_can_eat - assert_equal "OHAI!", @meme.i_can_has_cheezburger? - end - - def test_that_it_will_not_blend - refute_match /^no/i, @meme.will_it_blend? - end - - def test_that_will_be_skipped - skip "test this later" - end - end - -=== Specs - - require 'minitest/autorun' - - describe Meme do - before do - @meme = Meme.new - end - - describe "when asked about cheeseburgers" do - it "must respond positively" do - @meme.i_can_has_cheezburger?.must_equal "OHAI!" - end - end - - describe "when asked about blending possibilities" do - it "won't say no" do - @meme.will_it_blend?.wont_match /^no/i - end - end - end - -For matchers support check out: - -https://github.com/zenspider/minitest-matchers - -=== Benchmarks - -Add benchmarks to your regular unit tests. If the unit tests fail, the -benchmarks won't run. - - # optionally run benchmarks, good for CI-only work! - require 'minitest/benchmark' if ENV["BENCH"] - - class TestMeme < MiniTest::Unit::TestCase - # Override self.bench_range or default range is [1, 10, 100, 1_000, 10_000] - def bench_my_algorithm - assert_performance_linear 0.9999 do |n| # n is a range value - @obj.my_algorithm(n) - end - end - end - -Or add them to your specs. If you make benchmarks optional, you'll -need to wrap your benchmarks in a conditional since the methods won't -be defined. - - describe Meme do - if ENV["BENCH"] then - bench_performance_linear "my_algorithm", 0.9999 do |n| - 100.times do - @obj.my_algorithm(n) - end - end - end - end - -outputs something like: - - # Running benchmarks: - - TestBlah 100 1000 10000 - bench_my_algorithm 0.006167 0.079279 0.786993 - bench_other_algorithm 0.061679 0.792797 7.869932 - -Output is tab-delimited to make it easy to paste into a spreadsheet. - -=== Mocks - - class MemeAsker - def initialize(meme) - @meme = meme - end - - def ask(question) - method = question.tr(" ","_") + "?" - @meme.__send__(method) - end - end - - require 'minitest/autorun' - - describe MemeAsker do - before do - @meme = MiniTest::Mock.new - @meme_asker = MemeAsker.new @meme - end - - describe "#ask" do - describe "when passed an unpunctuated question" do - it "should invoke the appropriate predicate method on the meme" do - @meme.expect :will_it_blend?, :return_value - @meme_asker.ask "will it blend" - @meme.verify - end - end - end - end - -=== Stubs - - def test_stale_eh - obj_under_test = Something.new - - refute obj_under_test.stale? - - Time.stub :now, Time.at(0) do # stub goes away once the block is done - assert obj_under_test.stale? - end - end - -A note on stubbing: In order to stub a method, the method must -actually exist prior to stubbing. Use a singleton method to create a -new non-existing method: - - def obj_under_test.fake_method - ... - end - -=== Customizable Test Runner Types: - -MiniTest::Unit.runner=(runner) provides an easy way of creating custom -test runners for specialized needs. Justin Weiss provides the -following real-world example to create an alternative to regular -fixture loading: - - class MiniTestWithHooks::Unit < MiniTest::Unit - def before_suites - end - - def after_suites - end - - def _run_suites(suites, type) - begin - before_suites - super(suites, type) - ensure - after_suites - end - end - - def _run_suite(suite, type) - begin - suite.before_suite - super(suite, type) - ensure - suite.after_suite - end - end - end - - module MiniTestWithTransactions - class Unit < MiniTestWithHooks::Unit - include TestSetupHelper - - def before_suites - super - setup_nested_transactions - # load any data we want available for all tests - end - - def after_suites - teardown_nested_transactions - super - end - end - end - - MiniTest::Unit.runner = MiniTestWithTransactions::Unit.new - -== FAQ - -=== How to test SimpleDelegates? - -The following implementation and test: - - class Worker < SimpleDelegator - def work - end - end - - describe Worker do - before do - @worker = Worker.new(Object.new) - end - - it "must respond to work" do - @worker.must_respond_to :work - end - end - -outputs a failure: - - 1) Failure: - Worker#test_0001_must respond to work [bug11.rb:16]: - Expected #<Object:0x007f9e7184f0a0> (Object) to respond to #work. - -Worker is a SimpleDelegate which in 1.9+ is a subclass of BasicObject. -Expectations are put on Object (one level down) so the Worker -(SimpleDelegate) hits `method_missing` and delegates down to the -`Object.new` instance. That object doesn't respond to work so the test -fails. - -You can bypass `SimpleDelegate#method_missing` by extending the worker -with `MiniTest::Expectations`. You can either do that in your setup at -the instance level, like: - - before do - @worker = Worker.new(Object.new) - @worker.extend MiniTest::Expectations - end - -or you can extend the Worker class (within the test file!), like: - - class Worker - include ::MiniTest::Expectations - end - -== Known Extensions: - -capybara_minitest_spec :: Bridge between Capybara RSpec matchers and MiniTest::Spec expectations (e.g. page.must_have_content('Title')). -minispec-metadata :: Metadata for describe/it blocks - (e.g. `it 'requires JS driver', js: true do`) -minitest-ansi :: Colorize minitest output with ANSI colors. -minitest-around :: Around block for minitest. An alternative to setup/teardown dance. -minitest-capistrano :: Assertions and expectations for testing Capistrano recipes -minitest-capybara :: Capybara matchers support for minitest unit and spec -minitest-chef-handler :: Run Minitest suites as Chef report handlers -minitest-ci :: CI reporter plugin for MiniTest. -minitest-colorize :: Colorize MiniTest output and show failing tests instantly. -minitest-context :: Defines contexts for code reuse in MiniTest - specs that share common expectations. -minitest-debugger :: Wraps assert so failed assertions drop into - the ruby debugger. -minitest-display :: Patches MiniTest to allow for an easily configurable output. -minitest-emoji :: Print out emoji for your test passes, fails, and skips. -minitest-english :: Semantically symmetric aliases for assertions and expectations. -minitest-excludes :: Clean API for excluding certain tests you - don't want to run under certain conditions. -minitest-firemock :: Makes your MiniTest mocks more resilient. -minitest-great_expectations :: Generally useful additions to minitest's assertions and expectations -minitest-growl :: Test notifier for minitest via growl. -minitest-implicit-subject :: Implicit declaration of the test subject. -minitest-instrument :: Instrument ActiveSupport::Notifications when - test method is executed -minitest-instrument-db :: Store information about speed of test - execution provided by minitest-instrument in database -minitest-libnotify :: Test notifier for minitest via libnotify. -minitest-macruby :: Provides extensions to minitest for macruby UI testing. -minitest-matchers :: Adds support for RSpec-style matchers to minitest. -minitest-metadata :: Annotate tests with metadata (key-value). -minitest-mongoid :: Mongoid assertion matchers for MiniTest -minitest-must_not :: Provides must_not as an alias for wont in MiniTest -minitest-nc :: Test notifier for minitest via Mountain Lion's Notification Center -minitest-predicates :: Adds support for .predicate? methods -minitest-rails :: MiniTest integration for Rails 3.x -minitest-rails-capybara :: Capybara integration for MiniTest::Rails -minitest-reporters :: Create customizable MiniTest output formats -minitest-should_syntax :: RSpec-style +x.should == y+ assertions for MiniTest -minitest-shouldify :: Adding all manner of shoulds to MiniTest (bad idea) -minitest-spec-context :: Provides rspec-ish context method to MiniTest::Spec -minitest-spec-magic :: Minitest::Spec extensions for Rails and beyond -minitest-spec-rails :: Drop in MiniTest::Spec superclass for ActiveSupport::TestCase. -minitest-stub-const :: Stub constants for the duration of a block -minitest-tags :: add tags for minitest -minitest-wscolor :: Yet another test colorizer. -minitest_owrapper :: Get tests results as a TestResult object. -minitest_should :: Shoulda style syntax for minitest test::unit. -minitest_tu_shim :: minitest_tu_shim bridges between test/unit and minitest. -mongoid-minitest :: MiniTest matchers for Mongoid. -pry-rescue :: A pry plugin w/ minitest support. See pry-rescue/minitest.rb. - -== Unknown Extensions: - -Authors... Please send me a pull request with a description of your minitest extension. - -* assay-minitest -* detroit-minitest -* em-minitest-spec -* flexmock-minitest -* guard-minitest -* guard-minitest-decisiv -* minitest-activemodel -* minitest-ar-assertions -* minitest-capybara-unit -* minitest-colorer -* minitest-deluxe -* minitest-extra-assertions -* minitest-rails-shoulda -* minitest-spec -* minitest-spec-should -* minitest-sugar -* minitest_should -* mongoid-minitest -* spork-minitest - -== REQUIREMENTS: - -* Ruby 1.8, maybe even 1.6 or lower. No magic is involved. - -== INSTALL: - - sudo gem install minitest - -On 1.9, you already have it. To get newer candy you can still install -the gem, but you'll need to activate the gem explicitly to use it: - - require 'rubygems' - gem 'minitest' # ensures you're using the gem, and not the built in MT - require 'minitest/autorun' - - # ... usual testing stuffs ... - -DO NOTE: There is a serious problem with the way that ruby 1.9/2.0 -packages their own gems. They install a gem specification file, but -don't install the gem contents in the gem path. This messes up -Gem.find_files and many other things (gem which, gem contents, etc). - -Just install minitest as a gem for real and you'll be happier. - -== LICENSE: - -(The MIT License) - -Copyright (c) Ryan Davis, seattle.rb - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/tool/lib/minitest/unit.rb b/tool/lib/minitest/unit.rb deleted file mode 100644 index 2cda29ad7d..0000000000 --- a/tool/lib/minitest/unit.rb +++ /dev/null @@ -1,1479 +0,0 @@ -# encoding: utf-8 -# frozen_string_literal: true - -require "optparse" -require "rbconfig" -require "leakchecker" - -## -# Minimal (mostly drop-in) replacement for test-unit. -# -# :include: README.txt - -module MiniTest - - def self.const_missing name # :nodoc: - case name - when :MINI_DIR then - msg = "MiniTest::MINI_DIR was removed. Don't violate other's internals." - warn "WAR\NING: #{msg}" - warn "WAR\NING: Used by #{caller.first}." - const_set :MINI_DIR, "bad value" - else - super - end - end - - ## - # Assertion base class - - class Assertion < Exception; end - - ## - # Assertion raised when skipping a test - - class Skip < Assertion; end - - class << self - ## - # Filter object for backtraces. - - attr_accessor :backtrace_filter - end - - class BacktraceFilter # :nodoc: - def filter bt - return ["No backtrace"] unless bt - - new_bt = [] - - unless $DEBUG then - bt.each do |line| - break if line =~ /lib\/minitest/ - new_bt << line - end - - new_bt = bt.reject { |line| line =~ /lib\/minitest/ } if new_bt.empty? - new_bt = bt.dup if new_bt.empty? - else - new_bt = bt.dup - end - - new_bt - end - end - - self.backtrace_filter = BacktraceFilter.new - - def self.filter_backtrace bt # :nodoc: - backtrace_filter.filter bt - end - - ## - # MiniTest Assertions. All assertion methods accept a +msg+ which is - # printed if the assertion fails. - - module Assertions - ## - # Returns the diff command to use in #diff. Tries to intelligently - # figure out what diff to use. - - def self.diff - unless defined? @diff - exe = RbConfig::CONFIG['EXEEXT'] - @diff = %W"gdiff#{exe} diff#{exe}".find do |diff| - if system(diff, "-u", __FILE__, __FILE__) - break "#{diff} -u" - end - end - end - - @diff - end - - ## - # Set the diff command to use in #diff. - - def self.diff= o - @diff = o - end - - ## - # Returns a diff between +exp+ and +act+. If there is no known - # diff command or if it doesn't make sense to diff the output - # (single line, short output), then it simply returns a basic - # comparison between the two. - - def diff exp, act - require "tempfile" - - expect = mu_pp_for_diff exp - butwas = mu_pp_for_diff act - result = nil - - need_to_diff = - MiniTest::Assertions.diff && - (expect.include?("\n") || - butwas.include?("\n") || - expect.size > 30 || - butwas.size > 30 || - expect == butwas) - - return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless - need_to_diff - - tempfile_a = nil - tempfile_b = nil - - Tempfile.open("expect") do |a| - tempfile_a = a - a.puts expect - a.flush - - Tempfile.open("butwas") do |b| - tempfile_b = b - b.puts butwas - b.flush - - result = `#{MiniTest::Assertions.diff} #{a.path} #{b.path}` - result.sub!(/^\-\-\- .+/, "--- expected") - result.sub!(/^\+\+\+ .+/, "+++ actual") - - if result.empty? then - klass = exp.class - result = [ - "No visible difference in the #{klass}#inspect output.\n", - "You should look at the implementation of #== on ", - "#{klass} or its members.\n", - expect, - ].join - end - end - end - - result - ensure - tempfile_a.close! if tempfile_a - tempfile_b.close! if tempfile_b - end - - ## - # This returns a human-readable version of +obj+. By default - # #inspect is called. You can override this to use #pretty_print - # if you want. - - def mu_pp obj - s = obj.inspect - s = s.encode Encoding.default_external if defined? Encoding - s - end - - ## - # This returns a diff-able human-readable version of +obj+. This - # differs from the regular mu_pp because it expands escaped - # newlines and makes hex-values generic (like object_ids). This - # uses mu_pp to do the first pass and then cleans it up. - - def mu_pp_for_diff obj - mu_pp(obj).gsub(/(?<!\\)(?:\\\\)*\K\\n/, "\n").gsub(/:0x[a-fA-F0-9]{4,}/m, ':0xXXXXXX') - end - - def _assertions= n # :nodoc: - @_assertions = n - end - alias assertions= _assertions= - - def _assertions # :nodoc: - @_assertions ||= 0 - end - alias assertions _assertions - - ## - # Fails unless +test+ is a true value. - - def assert test, msg = nil - msg ||= "Failed assertion, no message given." - self._assertions += 1 - unless test then - msg = msg.call if Proc === msg - raise MiniTest::Assertion, msg - end - true - end - - ## - # Fails unless +obj+ is empty. - - def assert_empty obj, msg = nil - msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" } - assert_respond_to obj, :empty? - assert obj.empty?, msg - end - - ## - # Fails unless <tt>exp == act</tt> printing the difference between - # the two, if possible. - # - # If there is no visible difference but the assertion fails, you - # should suspect that your #== is buggy, or your inspect output is - # missing crucial details. - # - # For floats use assert_in_delta. - # - # See also: MiniTest::Assertions.diff - - def assert_equal exp, act, msg = nil - msg = message(msg, "") { diff exp, act } - assert exp == act, msg - end - - ## - # For comparing Floats. Fails unless +exp+ and +act+ are within +delta+ - # of each other. - # - # assert_in_delta Math::PI, (22.0 / 7.0), 0.01 - - def assert_in_delta exp, act, delta = 0.001, msg = nil - n = (exp - act).abs - msg = message(msg) { - "Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}" - } - assert delta >= n, msg - end - - ## - # For comparing Floats. Fails unless +exp+ and +act+ have a relative - # error less than +epsilon+. - - def assert_in_epsilon a, b, epsilon = 0.001, msg = nil - assert_in_delta a, b, [a.abs, b.abs].min * epsilon, msg - end - - ## - # Fails unless +collection+ includes +obj+. - - def assert_includes collection, obj, msg = nil - msg = message(msg) { - "Expected #{mu_pp(collection)} to include #{mu_pp(obj)}" - } - assert_respond_to collection, :include? - assert collection.include?(obj), msg - end - - ## - # Fails unless +obj+ is an instance of +cls+. - - def assert_instance_of cls, obj, msg = nil - msg = message(msg) { - "Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}" - } - - assert obj.instance_of?(cls), msg - end - - ## - # Fails unless +obj+ is a kind of +cls+. - - def assert_kind_of cls, obj, msg = nil # TODO: merge with instance_of - msg = message(msg) { - "Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" } - - assert obj.kind_of?(cls), msg - end - - ## - # Fails unless +matcher+ <tt>=~</tt> +obj+. - - def assert_match matcher, obj, msg = nil - msg = message(msg) { "Expected #{mu_pp matcher} to match #{mu_pp obj}" } - assert_respond_to matcher, :"=~" - matcher = Regexp.new Regexp.escape matcher if String === matcher - assert matcher =~ obj, msg - end - - ## - # Fails unless +obj+ is nil - - def assert_nil obj, msg = nil - msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" } - assert obj.nil?, msg - end - - ## - # For testing with binary operators. - # - # assert_operator 5, :<=, 4 - - def assert_operator o1, op, o2 = (predicate = true; nil), msg = nil - return assert_predicate o1, op, msg if predicate - msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" } - assert o1.__send__(op, o2), msg - end - - ## - # Fails if stdout or stderr do not output the expected results. - # Pass in nil if you don't care about that streams output. Pass in - # "" if you require it to be silent. Pass in a regexp if you want - # to pattern match. - # - # NOTE: this uses #capture_io, not #capture_subprocess_io. - # - # See also: #assert_silent - - def assert_output stdout = nil, stderr = nil - out, err = capture_io do - yield - end - - err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr - out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout - - y = send err_msg, stderr, err, "In stderr" if err_msg - x = send out_msg, stdout, out, "In stdout" if out_msg - - (!stdout || x) && (!stderr || y) - end - - ## - # For testing with predicates. - # - # assert_predicate str, :empty? - # - # This is really meant for specs and is front-ended by assert_operator: - # - # str.must_be :empty? - - def assert_predicate o1, op, msg = nil - msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op}" } - assert o1.__send__(op), msg - end - - ## - # Fails unless the block raises one of +exp+. Returns the - # exception matched so you can check the message, attributes, etc. - - def assert_raises *exp - msg = "#{exp.pop}.\n" if String === exp.last - - 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, "#{msg}#{mu_pp(exp)} exception expected, not") - } - - return e - end - - exp = exp.first if exp.size == 1 - - flunk "#{msg}#{mu_pp(exp)} expected but nothing was raised." - end - - ## - # Fails unless +obj+ responds to +meth+. - - def assert_respond_to obj, meth, msg = nil - msg = message(msg) { - "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}" - } - assert obj.respond_to?(meth), msg - end - - ## - # Fails unless +exp+ and +act+ are #equal? - - def assert_same exp, act, msg = nil - msg = message(msg) { - data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id] - "Expected %s (oid=%d) to be the same as %s (oid=%d)" % data - } - assert exp.equal?(act), msg - end - - ## - # +send_ary+ is a receiver, message and arguments. - # - # Fails unless the call returns a true value - # TODO: I should prolly remove this from specs - - def assert_send send_ary, m = nil - recv, msg, *args = send_ary - m = message(m) { - "Expected #{mu_pp(recv)}.#{msg}(*#{mu_pp(args)}) to return true" } - assert recv.__send__(msg, *args), m - end - - ## - # Fails if the block outputs anything to stderr or stdout. - # - # See also: #assert_output - - def assert_silent - assert_output "", "" do - yield - end - end - - ## - # Fails unless the block throws +sym+ - - def assert_throws sym, msg = nil - default = "Expected #{mu_pp(sym)} to have been thrown" - caught = true - catch(sym) do - begin - yield - rescue ThreadError => e # wtf?!? 1.8 + threads == suck - default += ", not \:#{e.message[/uncaught throw \`(\w+?)\'/, 1]}" - rescue ArgumentError => e # 1.9 exception - default += ", not #{e.message.split(/ /).last}" - rescue NameError => e # 1.8 exception - default += ", not #{e.name.inspect}" - end - caught = false - end - - assert caught, message(msg) { default } - end - - def assert_path_exists(path, msg = nil) - msg = message(msg) { "Expected path '#{path}' to exist" } - assert File.exist?(path), msg - end - alias assert_path_exist assert_path_exists - alias refute_path_not_exist assert_path_exists - - def refute_path_exists(path, msg = nil) - msg = message(msg) { "Expected path '#{path}' to not exist" } - refute File.exist?(path), msg - end - alias refute_path_exist refute_path_exists - alias assert_path_not_exist refute_path_exists - - ## - # Captures $stdout and $stderr into strings: - # - # out, err = capture_io do - # puts "Some info" - # warn "You did a bad thing" - # end - # - # assert_match %r%info%, out - # assert_match %r%bad%, err - # - # NOTE: For efficiency, this method uses StringIO and does not - # capture IO for subprocesses. Use #capture_subprocess_io for - # that. - - def capture_io - require 'stringio' - - captured_stdout, captured_stderr = StringIO.new, StringIO.new - - synchronize do - orig_stdout, orig_stderr = $stdout, $stderr - $stdout, $stderr = captured_stdout, captured_stderr - - begin - yield - ensure - $stdout = orig_stdout - $stderr = orig_stderr - end - end - - return captured_stdout.string, captured_stderr.string - end - alias capture_output capture_io - - ## - # Captures $stdout and $stderr into strings, using Tempfile to - # ensure that subprocess IO is captured as well. - # - # out, err = capture_subprocess_io do - # system "echo Some info" - # system "echo You did a bad thing 1>&2" - # end - # - # assert_match %r%info%, out - # assert_match %r%bad%, err - # - # NOTE: This method is approximately 10x slower than #capture_io so - # only use it when you need to test the output of a subprocess. - - def capture_subprocess_io - require 'tempfile' - - captured_stdout, captured_stderr = Tempfile.new("out"), Tempfile.new("err") - - synchronize do - orig_stdout, orig_stderr = $stdout.dup, $stderr.dup - $stdout.reopen captured_stdout - $stderr.reopen captured_stderr - - begin - yield - - $stdout.rewind - $stderr.rewind - - [captured_stdout.read, captured_stderr.read] - ensure - $stdout.reopen orig_stdout - $stderr.reopen orig_stderr - orig_stdout.close - orig_stderr.close - captured_stdout.close! - captured_stderr.close! - end - end - end - - ## - # Returns details for exception +e+ - - def exception_details e, msg - [ - "#{msg}", - "Class: <#{e.class}>", - "Message: <#{e.message.inspect}>", - "---Backtrace---", - "#{MiniTest::filter_backtrace(e.backtrace).join("\n")}", - "---------------", - ].join "\n" - end - - ## - # Fails with +msg+ - - def flunk msg = nil - msg ||= "Epic Fail!" - assert false, msg - end - - ## - # Returns a proc that will output +msg+ along with the default message. - - def message msg = nil, ending = ".", &default - proc { - msg = msg.call.chomp(".") if Proc === msg - custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty? - "#{custom_message}#{default.call}#{ending}" - } - end - - ## - # used for counting assertions - - def pass msg = nil - assert true - end - - ## - # Fails if +test+ is a true value - - def refute test, msg = nil - msg ||= "Failed refutation, no message given" - not assert(! test, msg) - end - - ## - # Fails if +obj+ is empty. - - def refute_empty obj, msg = nil - msg = message(msg) { "Expected #{mu_pp(obj)} to not be empty" } - assert_respond_to obj, :empty? - refute obj.empty?, msg - end - - ## - # Fails if <tt>exp == act</tt>. - # - # For floats use refute_in_delta. - - def refute_equal exp, act, msg = nil - msg = message(msg) { - "Expected #{mu_pp(act)} to not be equal to #{mu_pp(exp)}" - } - refute exp == act, msg - end - - ## - # For comparing Floats. Fails if +exp+ is within +delta+ of +act+. - # - # refute_in_delta Math::PI, (22.0 / 7.0) - - def refute_in_delta exp, act, delta = 0.001, msg = nil - n = (exp - act).abs - msg = message(msg) { - "Expected |#{exp} - #{act}| (#{n}) to not be <= #{delta}" - } - refute delta >= n, msg - end - - ## - # For comparing Floats. Fails if +exp+ and +act+ have a relative error - # less than +epsilon+. - - def refute_in_epsilon a, b, epsilon = 0.001, msg = nil - refute_in_delta a, b, a * epsilon, msg - end - - ## - # Fails if +collection+ includes +obj+. - - def refute_includes collection, obj, msg = nil - msg = message(msg) { - "Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}" - } - assert_respond_to collection, :include? - refute collection.include?(obj), msg - end - - ## - # Fails if +obj+ is an instance of +cls+. - - def refute_instance_of cls, obj, msg = nil - msg = message(msg) { - "Expected #{mu_pp(obj)} to not be an instance of #{cls}" - } - refute obj.instance_of?(cls), msg - end - - ## - # Fails if +obj+ is a kind of +cls+. - - def refute_kind_of cls, obj, msg = nil # TODO: merge with instance_of - msg = message(msg) { "Expected #{mu_pp(obj)} to not be a kind of #{cls}" } - refute obj.kind_of?(cls), msg - end - - ## - # Fails if +matcher+ <tt>=~</tt> +obj+. - - def refute_match matcher, obj, msg = nil - msg = message(msg) {"Expected #{mu_pp matcher} to not match #{mu_pp obj}"} - assert_respond_to matcher, :"=~" - matcher = Regexp.new Regexp.escape matcher if String === matcher - refute matcher =~ obj, msg - end - - ## - # Fails if +obj+ is nil. - - def refute_nil obj, msg = nil - msg = message(msg) { "Expected #{mu_pp(obj)} to not be nil" } - refute obj.nil?, msg - end - - ## - # Fails if +o1+ is not +op+ +o2+. Eg: - # - # refute_operator 1, :>, 2 #=> pass - # refute_operator 1, :<, 2 #=> fail - - def refute_operator o1, op, o2 = (predicate = true; nil), msg = nil - return refute_predicate o1, op, msg if predicate - msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op} #{mu_pp(o2)}"} - refute o1.__send__(op, o2), msg - end - - ## - # For testing with predicates. - # - # refute_predicate str, :empty? - # - # This is really meant for specs and is front-ended by refute_operator: - # - # str.wont_be :empty? - - def refute_predicate o1, op, msg = nil - msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op}" } - refute o1.__send__(op), msg - end - - ## - # Fails if +obj+ responds to the message +meth+. - - def refute_respond_to obj, meth, msg = nil - msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" } - - refute obj.respond_to?(meth), msg - end - - ## - # Fails if +exp+ is the same (by object identity) as +act+. - - def refute_same exp, act, msg = nil - msg = message(msg) { - data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id] - "Expected %s (oid=%d) to not be the same as %s (oid=%d)" % data - } - refute exp.equal?(act), msg - end - - ## - # Skips the current test. Gets listed at the end of the run but - # doesn't cause a failure exit code. - - def skip msg = nil, bt = caller - msg ||= "Skipped, no message given" - @skip = true - raise MiniTest::Skip, msg, bt - end - - alias omit skip - - ## - # Was this testcase skipped? Meant for #teardown. - - def skipped? - defined?(@skip) and @skip - end - - ## - # Takes a block and wraps it with the runner's shared mutex. - - def synchronize - Minitest::Unit.runner.synchronize do - yield - end - end - end - - class Unit # :nodoc: - VERSION = "4.7.5" # :nodoc: - - attr_accessor :report, :failures, :errors, :skips # :nodoc: - attr_accessor :assertion_count # :nodoc: - attr_writer :test_count # :nodoc: - attr_accessor :start_time # :nodoc: - attr_accessor :help # :nodoc: - attr_accessor :verbose # :nodoc: - attr_writer :options # :nodoc: - - ## - # :attr: - # - # if true, installs an "INFO" signal handler (only available to BSD and - # OS X users) which prints diagnostic information about the test run. - # - # This is auto-detected by default but may be overridden by custom - # runners. - - attr_accessor :info_signal - - ## - # Lazy accessor for options. - - def options - @options ||= {seed: 42} - end - - @@installed_at_exit ||= false - @@out = $stdout - @@after_tests = [] - @@current_repeat_count = 0 - - ## - # A simple hook allowing you to run a block of code after _all_ of - # the tests are done. Eg: - # - # MiniTest::Unit.after_tests { p $debugging_info } - - def self.after_tests &block - @@after_tests << block - end - - ## - # Registers MiniTest::Unit to run tests at process exit - - def self.autorun - at_exit { - # don't run if there was a non-exit exception - next if $! and not $!.kind_of? SystemExit - - # the order here is important. The at_exit handler must be - # installed before anyone else gets a chance to install their - # own, that way we can be assured that our exit will be last - # to run (at_exit stacks). - exit_code = nil - - at_exit { - @@after_tests.reverse_each(&:call) - exit false if exit_code && exit_code != 0 - } - - exit_code = MiniTest::Unit.new.run ARGV - } unless @@installed_at_exit - @@installed_at_exit = true - end - - ## - # Returns the stream to use for output. - - def self.output - @@out - end - - ## - # Sets MiniTest::Unit to write output to +stream+. $stdout is the default - # output - - def self.output= stream - @@out = stream - end - - ## - # Tells MiniTest::Unit to delegate to +runner+, an instance of a - # MiniTest::Unit subclass, when MiniTest::Unit#run is called. - - def self.runner= runner - @@runner = runner - end - - ## - # Returns the MiniTest::Unit subclass instance that will be used - # to run the tests. A MiniTest::Unit instance is the default - # runner. - - def self.runner - @@runner ||= self.new - end - - ## - # Return all plugins' run methods (methods that start with "run_"). - - def self.plugins - @@plugins ||= (["run_tests"] + - public_instance_methods(false). - grep(/^run_/).map { |s| s.to_s }).uniq - end - - ## - # Return the IO for output. - - def output - self.class.output - end - - def puts *a # :nodoc: - output.puts(*a) - end - - def print *a # :nodoc: - output.print(*a) - end - - def test_count # :nodoc: - @test_count ||= 0 - end - - ## - # Runner for a given +type+ (eg, test vs bench). - - def self.current_repeat_count - @@current_repeat_count - end - - def _run_anything type - suites = TestCase.send "#{type}_suites" - return if suites.empty? - - puts - puts "# Running #{type}s:" - puts - - @test_count, @assertion_count = 0, 0 - test_count = assertion_count = 0 - sync = output.respond_to? :"sync=" # stupid emacs - old_sync, output.sync = output.sync, true if sync - - @@current_repeat_count = 0 - begin - start = Time.now - - results = _run_suites suites, type - - @test_count = results.inject(0) { |sum, (tc, _)| sum + tc } - @assertion_count = results.inject(0) { |sum, (_, ac)| sum + ac } - test_count += @test_count - assertion_count += @assertion_count - t = Time.now - start - @@current_repeat_count += 1 - unless @repeat_count - puts - puts - end - puts "Finished%s %ss in %.6fs, %.4f tests/s, %.4f assertions/s.\n" % - [(@repeat_count ? "(#{@@current_repeat_count}/#{@repeat_count}) " : ""), type, - t, @test_count.fdiv(t), @assertion_count.fdiv(t)] - end while @repeat_count && @@current_repeat_count < @repeat_count && - report.empty? && failures.zero? && errors.zero? - - output.sync = old_sync if sync - - report.each_with_index do |msg, i| - puts "\n%3d) %s" % [i + 1, msg] - end - - puts - @test_count = test_count - @assertion_count = assertion_count - - status - end - - ## - # Runs all the +suites+ for a given +type+. - # - - def _run_suites suites, type - suites.map { |suite| _run_suite suite, type } - end - - ## - # Run a single +suite+ for a given +type+. - - def _run_suite suite, type - header = "#{type}_suite_header" - puts send(header, suite) if respond_to? header - - filter = options[:filter] || '/./' - filter = Regexp.new $1 if filter =~ /\/(.*)\// - - all_test_methods = suite.send "#{type}_methods" - - filtered_test_methods = all_test_methods.find_all { |m| - filter === m || filter === "#{suite}##{m}" - } - - leakchecker = LeakChecker.new - if ENV["LEAK_CHECKER_TRACE_OBJECT_ALLOCATION"] - require "objspace" - trace = true - end - - assertions = filtered_test_methods.map { |method| - inst = suite.new method - inst._assertions = 0 - - print "#{suite}##{method} = " if @verbose - - start_time = Time.now if @verbose - result = - if trace - ObjectSpace.trace_object_allocations {inst.run self} - else - inst.run self - end - - print "%.2f s = " % (Time.now - start_time) if @verbose - print result - puts if @verbose - $stdout.flush - - unless defined?(RubyVM::JIT) && RubyVM::JIT.enabled? # compiler process is wrongly considered as leak - leakchecker.check("#{inst.class}\##{inst.__name__}") - end - - inst._assertions - } - return assertions.size, assertions.inject(0) { |sum, n| sum + n } - end - - ## - # Record the result of a single test. Makes it very easy to gather - # information. Eg: - # - # class StatisticsRecorder < MiniTest::Unit - # def record suite, method, assertions, time, error - # # ... record the results somewhere ... - # end - # end - # - # MiniTest::Unit.runner = StatisticsRecorder.new - # - # NOTE: record might be sent more than once per test. It will be - # sent once with the results from the test itself. If there is a - # failure or error in teardown, it will be sent again with the - # error or failure. - - def record suite, method, assertions, time, error - end - - def location e # :nodoc: - last_before_assertion = "" - - return '<empty>' 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 - end - last_before_assertion.sub(/:in .*$/, '') - end - - ## - # Writes status for failed test +meth+ in +klass+ which finished with - # exception +e+ - - def puke klass, meth, e - e = case e - when MiniTest::Skip then - @skips += 1 - return "S" unless @verbose - "Skipped:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n" - when MiniTest::Assertion then - @failures += 1 - "Failure:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n" - else - @errors += 1 - bt = MiniTest::filter_backtrace(e.backtrace).join "\n " - "Error:\n#{klass}##{meth}:\n#{e.class}: #{e.message.b}\n #{bt}\n" - end - @report << e - e[0, 1] - end - - def initialize # :nodoc: - @report = [] - @errors = @failures = @skips = 0 - @verbose = false - @mutex = Thread::Mutex.new - @info_signal = Signal.list['INFO'] - @repeat_count = nil - end - - def synchronize # :nodoc: - if @mutex then - @mutex.synchronize { yield } - else - yield - end - end - - def process_args args = [] # :nodoc: - options = {} - orig_args = args.dup - - OptionParser.new do |opts| - opts.banner = 'minitest options:' - opts.version = MiniTest::Unit::VERSION - - opts.on '-h', '--help', 'Display this help.' do - puts opts - exit - end - - opts.on '-s', '--seed SEED', Integer, "Sets random seed" do |m| - options[:seed] = m.to_i - end - - opts.on '-v', '--verbose', "Verbose. Show progress processing files." do - options[:verbose] = true - end - - opts.on '-n', '--name PATTERN', "Filter test names on pattern (e.g. /foo/)" do |a| - options[:filter] = a - end - - opts.parse! args - orig_args -= args - end - - unless options[:seed] then - srand - options[:seed] = srand % 0xFFFF - orig_args << "--seed" << options[:seed].to_s - end - - srand options[:seed] - - self.verbose = options[:verbose] - @help = orig_args.map { |s| s =~ /[\s|&<>$()]/ ? s.inspect : s }.join " " - - options - end - - ## - # Begins the full test run. Delegates to +runner+'s #_run method. - - def run args = [] - self.class.runner._run(args) - end - - ## - # Top level driver, controls all output and filtering. - - def _run args = [] - args = process_args args # ARGH!! blame test/unit process_args - self.options.merge! args - - puts "Run options: #{help}" - - self.class.plugins.each do |plugin| - send plugin - break unless report.empty? - end - - return failures + errors if self.test_count > 0 # or return nil... - rescue Interrupt - abort 'Interrupted' - end - - ## - # Runs test suites matching +filter+. - - def run_tests - _run_anything :test - end - - ## - # Writes status to +io+ - - def status io = self.output - format = "%d tests, %d assertions, %d failures, %d errors, %d skips" - io.puts format % [test_count, assertion_count, failures, errors, skips] - end - - ## - # Provides a simple set of guards that you can use in your tests - # to skip execution if it is not applicable. These methods are - # mixed into TestCase as both instance and class methods so you - # can use them inside or outside of the test methods. - # - # def test_something_for_mri - # skip "bug 1234" if jruby? - # # ... - # end - # - # if windows? then - # # ... lots of test methods ... - # end - - module Guard - - ## - # Is this running on jruby? - - def jruby? platform = RUBY_PLATFORM - "java" == platform - end - - ## - # Is this running on mri? - - def maglev? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE - "maglev" == platform - end - - module_function :maglev? - - ## - # Is this running on mri? - - def mri? platform = RUBY_DESCRIPTION - /^ruby/ =~ platform - end - - ## - # Is this running on rubinius? - - def rubinius? platform = defined?(RUBY_ENGINE) && RUBY_ENGINE - "rbx" == platform - end - - ## - # Is this running on windows? - - def windows? platform = RUBY_PLATFORM - /mswin|mingw/ =~ platform - end - - ## - # Is this running on mingw? - - def mingw? platform = RUBY_PLATFORM - /mingw/ =~ platform - end - - end - - ## - # Provides before/after hooks for setup and teardown. These are - # meant for library writers, NOT for regular test authors. See - # #before_setup for an example. - - module LifecycleHooks - ## - # Runs before every test, after setup. This hook is meant for - # libraries to extend minitest. It is not meant to be used by - # test developers. - # - # See #before_setup for an example. - - def after_setup; end - - ## - # Runs before every test, before setup. This hook is meant for - # libraries to extend minitest. It is not meant to be used by - # test developers. - # - # As a simplistic example: - # - # module MyMinitestPlugin - # def before_setup - # super - # # ... stuff to do before setup is run - # end - # - # def after_setup - # # ... stuff to do after setup is run - # super - # end - # - # def before_teardown - # super - # # ... stuff to do before teardown is run - # end - # - # def after_teardown - # # ... stuff to do after teardown is run - # super - # end - # end - # - # class MiniTest::Unit::TestCase - # include MyMinitestPlugin - # end - - def before_setup; end - - ## - # Runs after every test, before teardown. This hook is meant for - # libraries to extend minitest. It is not meant to be used by - # test developers. - # - # See #before_setup for an example. - - def before_teardown; end - - ## - # Runs after every test, after teardown. This hook is meant for - # libraries to extend minitest. It is not meant to be used by - # test developers. - # - # See #before_setup for an example. - - def after_teardown; end - end - - ## - # Subclass TestCase to create your own tests. Typically you'll want a - # TestCase subclass per implementation class. - # - # See MiniTest::Assertions - - class TestCase - include LifecycleHooks - include Guard - extend Guard - - attr_reader :__name__ # :nodoc: - - PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException, - Interrupt, SystemExit] # :nodoc: - - ## - # Runs the tests reporting the status to +runner+ - - def run runner - trap "INFO" do - runner.report.each_with_index do |msg, i| - warn "\n%3d) %s" % [i + 1, msg] - end - warn '' - time = runner.start_time ? Time.now - runner.start_time : 0 - warn "Current Test: %s#%s %.2fs" % [self.class, self.__name__, time] - runner.status $stderr - end if runner.info_signal - - start_time = Time.now - - result = "" - srand(runner.options[:seed]) - - begin - @passed = nil - self.before_setup - self.setup - self.after_setup - self.run_test self.__name__ - result = "." unless io? - time = Time.now - start_time - runner.record self.class, self.__name__, self._assertions, time, nil - @passed = true - rescue *PASSTHROUGH_EXCEPTIONS - raise - rescue Exception => e - @passed = Skip === e - time = Time.now - start_time - runner.record self.class, self.__name__, self._assertions, time, e - result = runner.puke self.class, self.__name__, e - ensure - %w{ before_teardown teardown after_teardown }.each do |hook| - begin - self.send hook - rescue *PASSTHROUGH_EXCEPTIONS - raise - rescue Exception => e - @passed = false - runner.record self.class, self.__name__, self._assertions, time, e - result = runner.puke self.class, self.__name__, e - end - end - trap 'INFO', 'DEFAULT' if runner.info_signal - end - result - end - - alias :run_test :__send__ - - def initialize name # :nodoc: - @__name__ = name - @__io__ = nil - @passed = nil - @@current = self # FIX: make thread local - end - - def self.current # :nodoc: - @@current # FIX: make thread local - end - - ## - # Return the output IO object - - def io - @__io__ = true - MiniTest::Unit.output - end - - ## - # Have we hooked up the IO yet? - - def io? - @__io__ - end - - def self.reset # :nodoc: - @@test_suites = {} - end - - reset - - ## - # Make diffs for this TestCase use #pretty_inspect so that diff - # in assert_equal can be more details. NOTE: this is much slower - # than the regular inspect but much more usable for complex - # objects. - - def self.make_my_diffs_pretty! - require 'pp' - - define_method :mu_pp do |o| - o.pretty_inspect - end - end - - def self.inherited klass # :nodoc: - @@test_suites[klass] = true - super - end - - def self.test_order # :nodoc: - :sorted - end - - def self.test_suites # :nodoc: - suites = @@test_suites.keys - - case self.test_order - when :random - # shuffle test suites based on CRC32 of their names - salt = "\n" + rand(1 << 32).to_s - crc_tbl = (0..255).map do |i| - (0..7).inject(i) {|c,| (c & 1 == 1) ? (0xEDB88320 ^ (c >> 1)) : (c >> 1) } - end - suites = suites.sort_by do |suite| - crc32 = 0xffffffff - "#{suite.name}#{salt}".each_byte do |data| - crc32 = crc_tbl[(crc32 ^ data) & 0xff] ^ (crc32 >> 8) - end - crc32 ^ 0xffffffff - end - when :nosort - suites - else - suites.sort_by { |ts| ts.name.to_s } - end - end - - def self.test_methods # :nodoc: - methods = public_instance_methods(true).grep(/^test/).map { |m| m.to_s } - - case self.test_order - when :parallel - max = methods.size - ParallelEach.new methods.sort.sort_by { rand max } - when :random then - max = methods.size - methods.sort.sort_by { rand max } - when :alpha, :sorted then - methods.sort - when :nosort - methods - else - raise "Unknown test_order: #{self.test_order.inspect}" - end - end - - ## - # Returns true if the test passed. - - def passed? - @passed - end - - ## - # Runs before every test. Use this to set up before each test - # run. - - def setup; end - - ## - # Runs after every test. Use this to clean up after each test - # run. - - def teardown; end - - include MiniTest::Assertions - end # class TestCase - end # class Unit - - Test = Unit::TestCase -end # module MiniTest - -Minitest = MiniTest # :nodoc: because ugh... I typo this all the time diff --git a/tool/lib/output.rb b/tool/lib/output.rb new file mode 100644 index 0000000000..8cb426ae4a --- /dev/null +++ b/tool/lib/output.rb @@ -0,0 +1,70 @@ +require_relative 'vpath' +require_relative 'colorize' + +class Output + attr_reader :path, :vpath + + def initialize(path: nil, timestamp: nil, ifchange: nil, color: nil, + overwrite: false, create_only: false, vpath: VPath.new) + @path = path + @timestamp = timestamp + @ifchange = ifchange + @color = color + @overwrite = overwrite + @create_only = create_only + @vpath = vpath + end + + COLOR_WHEN = { + 'always' => true, 'auto' => nil, 'never' => false, + nil => true, false => false, + } + + def def_options(opt) + opt.separator(" Output common options:") + opt.on('-o', '--output=PATH') {|v| @path = v} + opt.on('-t', '--timestamp[=PATH]') {|v| @timestamp = v || true} + opt.on('-c', '--[no-]if-change') {|v| @ifchange = v} + opt.on('--[no-]color=[WHEN]', COLOR_WHEN.keys) {|v| @color = COLOR_WHEN[v]} + opt.on('--[no-]create-only') {|v| @create_only = v} + opt.on('--[no-]overwrite') {|v| @overwrite = v} + @vpath.def_options(opt) + end + + def write(data, overwrite: @overwrite, create_only: @create_only) + unless @path + $stdout.print data + return true + end + color = Colorize.new(@color) + unchanged = color.pass("unchanged") + updated = color.fail("updated") + outpath = nil + + if (@ifchange or overwrite or create_only) and (@vpath.open(@path, "rb") {|f| + outpath = f.path + if @ifchange or create_only + original = f.read + (@ifchange and original == data) or (create_only and !original.empty?) + end + } rescue false) + puts "#{outpath} #{unchanged}" + written = false + else + unless overwrite and outpath and (File.binwrite(outpath, data) rescue nil) + File.binwrite(outpath = @path, data) + end + puts "#{outpath} #{updated}" + written = true + end + if timestamp = @timestamp + if timestamp == true + dir, base = File.split(@path) + timestamp = File.join(dir, ".time." + base) + end + File.binwrite(timestamp, '') + File.utime(nil, nil, timestamp) + end + written + end +end diff --git a/tool/lib/path.rb b/tool/lib/path.rb new file mode 100644 index 0000000000..5582b2851e --- /dev/null +++ b/tool/lib/path.rb @@ -0,0 +1,101 @@ +module Path + module_function + + def clean(path) + path = "#{path}/".gsub(/(\A|\/)(?:\.\/)+/, '\1').tr_s('/', '/') + nil while path.sub!(/[^\/]+\/\.\.\//, '') + path + end + + def relative(path, base) + path = clean(path) + base = clean(base) + path, base = [path, base].map{|s|s.split("/")} + until path.empty? or base.empty? or path[0] != base[0] + path.shift + base.shift + end + path, base = [path, base].map{|s|s.join("/")} + if base.empty? + path + elsif base.start_with?("../") or File.absolute_path?(base) + File.expand_path(path) + else + base.gsub!(/[^\/]+/, '..') + File.join(base, path) + end + end + + def clean_link(src, dest) + begin + link = File.readlink(dest) + rescue + else + return if link == src + File.unlink(dest) + end + yield src, dest + end + + # Extensions to FileUtils + + module Mswin + def ln_safe(src, dest, *opt) + cmd = ["mklink", dest.tr("/", "\\"), src.tr("/", "\\")] + cmd[1, 0] = opt + return if system("cmd", "/c", *cmd) + # TODO: use RUNAS or something + puts cmd.join(" ") + end + + def ln_dir_safe(src, dest) + ln_safe(src, dest, "/d") + end + end + + module HardlinkExcutable + def ln_exe(src, dest) + ln(src, dest, force: true) + end + end + + def ln_safe(src, dest) + ln_sf(src, dest) + rescue Errno::ENOENT + # Windows disallows to create broken symboic links, probably because + # it is a kind of reparse points. + raise if File.exist?(src) + end + + alias ln_dir_safe ln_safe + alias ln_exe ln_safe + + def ln_relative(src, dest, executable = false) + return if File.identical?(src, dest) + parent = File.dirname(dest) + File.directory?(parent) or mkdir_p(parent) + if executable + return (ln_exe(src, dest) if File.exist?(src)) + end + clean_link(relative(src, parent), dest) {|s, d| ln_safe(s, d)} + end + + def ln_dir_relative(src, dest) + return if File.identical?(src, dest) + parent = File.dirname(dest) + File.directory?(parent) or mkdir_p(parent) + clean_link(relative(src, parent), dest) {|s, d| ln_dir_safe(s, d)} + end + + case (CROSS_COMPILING || RUBY_PLATFORM) + when /linux|darwin|solaris/ + prepend HardlinkExcutable + extend HardlinkExcutable + when /mingw|mswin/ + unless File.respond_to?(:symlink) + prepend Mswin + extend Mswin + end + else + end +end diff --git a/tool/lib/profile_test_all.rb b/tool/lib/profile_test_all.rb index 2c360d76dd..fb434e314d 100644 --- a/tool/lib/profile_test_all.rb +++ b/tool/lib/profile_test_all.rb @@ -20,7 +20,7 @@ require 'objspace' -class MiniTest::Unit::TestCase +class Test::Unit::TestCase alias orig_run run file = ENV['RUBY_TEST_ALL_PROFILE'] diff --git a/tool/lib/test/unit.rb b/tool/lib/test/unit.rb index 391c065ec1..d758b5fb02 100644 --- a/tool/lib/test/unit.rb +++ b/tool/lib/test/unit.rb @@ -1,23 +1,117 @@ # frozen_string_literal: true -require_relative '../minitest/unit' -require 'test/unit/assertions' +# Enable deprecation warnings for test-all, so deprecated methods/constants/functions are dealt with early. +Warning[:deprecated] = true + +if ENV['BACKTRACE_FOR_DEPRECATION_WARNINGS'] + Warning.extend Module.new { + def warn(message, category: nil, **kwargs) + if category == :deprecated and $stderr.respond_to?(:puts) + $stderr.puts nil, message, caller, nil + else + super + end + end + } +end + require_relative '../envutil' require_relative '../colorize' -require 'test/unit/testcase' +require_relative '../leakchecker' +require_relative '../test/unit/testcase' require 'optparse' # See Test::Unit module Test + ## # Test::Unit is an implementation of the xUnit testing framework for Ruby. - # - # If you are writing new test code, please use MiniTest instead of Test::Unit. - # - # Test::Unit has been left in the standard library to support legacy test - # suites. module Unit - TEST_UNIT_IMPLEMENTATION = 'test/unit compatibility layer using minitest' # :nodoc: + ## + # Assertion base class + + class AssertionFailedError < Exception; end + + ## + # Assertion raised when skipping a test + + class PendedError < AssertionFailedError; end + + module Order + class NoSort + def initialize(seed) + end + + def sort_by_name(list) + list + end + + alias sort_by_string sort_by_name + + def group(list) + list + end + end + + class Alpha < NoSort + def sort_by_name(list) + list.sort_by(&:name) + end + + def sort_by_string(list) + list.sort + end + + end + + # shuffle test suites based on CRC32 of their names + Shuffle = Struct.new(:seed, :salt) do + def initialize(seed) + self.class::CRC_TBL ||= (0..255).map {|i| + (0..7).inject(i) {|c,| (c & 1 == 1) ? (0xEDB88320 ^ (c >> 1)) : (c >> 1) } + }.freeze + + salt = [seed].pack("V").unpack1("H*") + super(seed, "\n#{salt}".freeze).freeze + end + + def sort_by_name(list) + list.sort_by {|e| randomize_key(e.name)} + end + + def sort_by_string(list) + list.sort_by {|e| randomize_key(e)} + end + + def group(list) + list + end + + private + + def crc32(str, crc32 = 0xffffffff) + crc_tbl = self.class::CRC_TBL + str.each_byte do |data| + crc32 = crc_tbl[(crc32 ^ data) & 0xff] ^ (crc32 >> 8) + end + crc32 + end + + def randomize_key(name) + crc32(salt, crc32(name)) ^ 0xffffffff + end + end + + Types = { + random: Shuffle, + alpha: Alpha, + sorted: Alpha, + nosort: NoSort, + } + Types.default_proc = proc {|_, order| + raise "Unknown test_order: #{order.inspect}" + } + end module RunCount # :nodoc: all @@run_count = 0 @@ -65,24 +159,24 @@ module Test order = options[:test_order] if seed = options[:seed] order ||= :random - srand(seed) - else - seed = options[:seed] = srand % 100_000 - srand(seed) + elsif (order ||= :random) == :random + seed = options[:seed] = rand(0x10000) orig_args.unshift "--seed=#{seed}" end - MiniTest::Unit::TestCase.test_order = order if order + Test::Unit::TestCase.test_order = order if order + order = Test::Unit::TestCase.test_order + @order = Test::Unit::Order::Types[order].new(seed) @help = "\n" + orig_args.map { |s| " " + (s =~ /[\s|&<>$()]/ ? s.inspect : s) }.join("\n") + @options = options end private def setup_options(opts, options) - opts.separator 'minitest options:' - opts.version = MiniTest::Unit::VERSION + opts.separator 'test-unit options:' opts.on '-h', '--help', 'Display this help.' do puts opts @@ -102,7 +196,8 @@ module Test (options[:filter] ||= []) << a end - opts.on '--test-order=random|alpha|sorted|nosort', [:random, :alpha, :sorted, :nosort] do |a| + orders = Test::Unit::Order::Types.keys + opts.on "--test-order=#{orders.join('|')}", orders do |a| options[:test_order] = a end end @@ -117,6 +212,9 @@ module Test filter = nil elsif negative.empty? and positive.size == 1 and pos_pat !~ positive[0] filter = positive[0] + unless /\A[A-Z]\w*(?:::[A-Z]\w*)*#/ =~ filter + filter = /##{Regexp.quote(filter)}\z/ + end else filter = Regexp.union(*positive.map! {|s| Regexp.new(s[pos_pat, 1] || "\\A#{Regexp.quote(s)}\\z")}) end @@ -124,13 +222,6 @@ module Test negative = Regexp.union(*negative.map! {|s| Regexp.new(s[neg_pat, 1])}) filter = /\A(?=.*#{filter})(?!.*#{negative})/ end - if Regexp === filter - filter = filter.dup - # bypass conversion in minitest - def filter.=~(other) # :nodoc: - super unless Regexp === other - end - end options[:filter] = filter end true @@ -151,10 +242,16 @@ module Test @jobserver = nil makeflags = ENV.delete("MAKEFLAGS") if !options[:parallel] and - /(?:\A|\s)--jobserver-(?:auth|fds)=(\d+),(\d+)/ =~ makeflags + /(?:\A|\s)--jobserver-(?:auth|fds)=(?:(\d+),(\d+)|fifo:((?:\\.|\S)+))/ =~ makeflags begin - r = IO.for_fd($1.to_i(10), "rb", autoclose: false) - w = IO.for_fd($2.to_i(10), "wb", autoclose: false) + if fifo = $3 + fifo.gsub!(/\\(?=.)/, '') + r = File.open(fifo, IO::RDONLY|IO::NONBLOCK|IO::BINARY) + w = File.open(fifo, IO::WRONLY|IO::NONBLOCK|IO::BINARY) + else + r = IO.for_fd($1.to_i(10), "rb", autoclose: false) + w = IO.for_fd($2.to_i(10), "wb", autoclose: false) + end rescue r.close if r nil @@ -162,9 +259,10 @@ module Test r.close_on_exec = true w.close_on_exec = true @jobserver = [r, w] - options[:parallel] ||= 1 + options[:parallel] ||= 256 # number of tokens to acquire first end end + @worker_timeout = EnvUtil.apply_timeout_scale(options[:worker_timeout] || 180) super end @@ -187,6 +285,10 @@ module Test options[:parallel] = a.to_i end + opts.on '--worker-timeout=N', Integer, "Timeout workers not responding in N seconds" do |a| + options[:worker_timeout] = a + end + opts.on '--separate', "Restart job process after one testcase has done" do options[:parallel] ||= 1 options[:separate] = true @@ -200,7 +302,8 @@ module Test options[:retry] = false end - opts.on '--ruby VAL', "Path to ruby which is used at -j option" do |a| + opts.on '--ruby VAL', "Path to ruby which is used at -j option", + "Also used as EnvUtil.rubybin by some assertion methods" do |a| options[:ruby] = a.split(/ /).reject(&:empty?) end @@ -213,7 +316,7 @@ module Test def self.launch(ruby,args=[]) scale = EnvUtil.timeout_scale io = IO.popen([*ruby, "-W1", - "#{File.dirname(__FILE__)}/unit/parallel.rb", + "#{__dir__}/unit/parallel.rb", *("--timeout-scale=#{scale}" if scale), *args], "rb+") new(io, io.pid, :waiting) @@ -221,6 +324,8 @@ module Test attr_reader :quit_called attr_accessor :start_time + attr_accessor :response_at + attr_accessor :current @@worker_number = 0 @@ -234,6 +339,7 @@ module Test @loadpath = [] @hooks = {} @quit_called = false + @response_at = nil end def name @@ -253,6 +359,7 @@ module Test puts "run #{task} #{type}" @status = :prepare @start_time = Time.now + @response_at = @start_time rescue Errno::EPIPE died rescue IOError @@ -269,6 +376,7 @@ module Test def read res = (@status == :quit) ? @io.read : @io.gets + @response_at = Time.now res && res.chomp end @@ -341,8 +449,8 @@ module Test real_file = worker.real_file and warn "running file: #{real_file}" @need_quit = true warn "" - warn "Some worker was crashed. It seems ruby interpreter's bug" - warn "or, a bug of test/unit/parallel.rb. try again without -j" + warn "A test worker crashed. It might be an interpreter bug or" + warn "a bug in test/unit/parallel.rb. Try again without the -j" warn "option." warn "" if File.exist?('core') @@ -399,9 +507,11 @@ module Test @ios.delete worker.io end - def quit_workers + def quit_workers(&cond) return if @workers.empty? + closed = [] if cond @workers.reject! do |worker| + next unless cond&.call(worker) begin Timeout.timeout(1) do worker.quit @@ -409,20 +519,33 @@ module Test rescue Errno::EPIPE rescue Timeout::Error end - worker.close + closed&.push worker + begin + Timeout.timeout(0.2) do + worker.close + end + rescue Timeout::Error + worker.kill + retry + end + @ios.delete worker.io end - return if @workers.empty? + return if (closed ||= @workers).empty? + pids = closed.map(&:pid) begin - Timeout.timeout(0.2 * @workers.size) do + Timeout.timeout(0.2 * closed.size) do Process.waitall end rescue Timeout::Error - @workers.each do |worker| - worker.kill + if pids + Process.kill(:KILL, *pids) rescue nil + pids = nil + retry end - @worker.clear end + @workers.clear unless cond + closed end FakeClass = Struct.new(:name) @@ -456,11 +579,13 @@ module Test @test_count += 1 jobs_status(worker) + when /^start (.+?)$/ + worker.current = Marshal.load($1.unpack1("m")) when /^done (.+?)$/ begin - r = Marshal.load($1.unpack("m")[0]) + r = Marshal.load($1.unpack1("m")) rescue - print "unknown object: #{$1.unpack("m")[0].dump}" + print "unknown object: #{$1.unpack1("m").dump}" return true end result << r[0..1] unless r[0..1] == [nil,nil] @@ -471,7 +596,7 @@ module Test return true when /^record (.+?)$/ begin - r = Marshal.load($1.unpack("m")[0]) + r = Marshal.load($1.unpack1("m")) suite = r.first key = [worker.name, suite] @@ -481,18 +606,18 @@ module Test @records[key] = [worker.start_time, Time.now] end rescue => e - print "unknown record: #{e.message} #{$1.unpack("m")[0].dump}" + print "unknown record: #{e.message} #{$1.unpack1("m").dump}" return true end record(fake_class(r[0]), *r[1..-1]) when /^p (.+?)$/ del_jobs_status - print $1.unpack("m")[0] + print $1.unpack1("m") jobs_status(worker) if @options[:job_status] == :replace when /^after (.+?)$/ - @warnings << Marshal.load($1.unpack("m")[0]) + @warnings << Marshal.load($1.unpack1("m")) when /^bye (.+?)$/ - after_worker_down worker, Marshal.load($1.unpack("m")[0]) + after_worker_down worker, Marshal.load($1.unpack1("m")) when /^bye$/, nil if shutting_down || worker.quit_called after_worker_quit worker @@ -515,16 +640,7 @@ module Test # Require needed thing for parallel running require 'timeout' - @tasks = @files.dup # Array of filenames. - - case MiniTest::Unit::TestCase.test_order - when :random - @tasks.shuffle! - else - # JIT first - ts = @tasks.group_by{|e| /test_jit/ =~ e ? 0 : 1} - @tasks = ts[0] + ts[1] if ts.size == 2 - end + @tasks = @order.group(@order.sort_by_string(@files)) # Array of filenames. @need_quit = false @dead_workers = [] # Array of dead workers. @@ -537,20 +653,34 @@ module Test @ios = [] # Array of worker IOs @job_tokens = String.new(encoding: Encoding::ASCII_8BIT) if @jobserver begin - [@tasks.size, @options[:parallel]].min.times {launch_worker} + while true + newjobs = [@tasks.size, @options[:parallel]].min - @workers.size + if newjobs > 0 + if @jobserver + t = @jobserver[0].read_nonblock(newjobs, exception: false) + @job_tokens << t if String === t + newjobs = @job_tokens.size + 1 - @workers.size + end + newjobs.times {launch_worker} + end - while _io = IO.select(@ios)[0] - break if _io.any? do |io| + timeout = [(@workers.filter_map {|w| w.response_at}.min&.-(Time.now) || 0), 0].max + @worker_timeout + + if !(_io = IO.select(@ios, nil, nil, timeout)) + timeout = Time.now - @worker_timeout + quit_workers {|w| w.response_at&.<(timeout) }&.map {|w| + rep << {file: w.real_file, result: nil, testcase: w.current[0], error: w.current} + } + elsif _io.first.any? {|io| @need_quit or (deal(io, type, result, rep).nil? and !@workers.any? {|x| [:running, :prepare].include? x.status}) + } + break end - if @jobserver and @job_tokens and !@tasks.empty? and !@workers.any? {|x| x.status == :ready} - t = @jobserver[0].read_nonblock([@tasks.size, @options[:parallel]].min, exception: false) - if String === t - @job_tokens << t - t.size.times {launch_worker} - end + if @tasks.empty? + break if @workers.empty? + next # wait for all workers to finish end end rescue Interrupt => ex @@ -558,7 +688,7 @@ module Test return result ensure if file = @options[:timetable_data] - open(file, 'w'){|f| + File.open(file, 'w'){|f| @records.each{|(worker, suite), (st, ed)| f.puts '[' + [worker.dump, suite.dump, st.to_f * 1_000, ed.to_f * 1_000].join(", ") + '],' } @@ -578,15 +708,50 @@ module Test unless @interrupt || !@options[:retry] || @need_quit parallel = @options[:parallel] @options[:parallel] = false - suites, rep = rep.partition {|r| r[:testcase] && r[:file] && r[:report].any? {|e| !e[2].is_a?(MiniTest::Skip)}} + suites, rep = rep.partition {|r| + r[:testcase] && r[:file] && + (!r.key?(:report) || r[:report].any? {|e| !e[2].is_a?(Test::Unit::PendedError)}) + } suites.map {|r| File.realpath(r[:file])}.uniq.each {|file| require file} - suites.map! {|r| eval("::"+r[:testcase])} del_status_line or puts + error, suites = suites.partition {|r| r[:error]} unless suites.empty? - puts "\n""Retrying..." + puts "\n" + @failed_output.puts "Failed tests:" + suites.each {|r| + r[:report].each {|c, m, e| + @failed_output.puts "#{c}##{m}: #{e&.class}: #{e&.message&.slice(/\A.*/)}" + } + } + @failed_output.puts "\n" + puts "Retrying..." @verbose = options[:verbose] + suites.map! {|r| ::Object.const_get(r[:testcase])} _run_suites(suites, type) end + unless error.empty? + puts "\n""Retrying hung up testcases..." + error = error.map do |r| + begin + ::Object.const_get(r[:testcase]) + rescue NameError + # testcase doesn't specify the correct case, so show `r` for information + require 'pp' + + $stderr.puts "Retrying is failed because the file and testcase is not consistent:" + PP.pp r, $stderr + @errors += 1 + nil + end + end.compact + verbose = @verbose + job_status = options[:job_status] + options[:verbose] = @verbose = true + options[:job_status] = :normal + result.concat _run_suites(error, type) + options[:verbose] = @verbose = verbose + options[:job_status] = job_status + end @options[:parallel] = parallel end unless @options[:retry] @@ -594,20 +759,28 @@ module Test end unless rep.empty? rep.each do |r| - r[:report].each do |f| + if r[:error] + puke(*r[:error], Timeout::Error.new) + next + end + r[:report]&.each do |f| puke(*f) if f end end if @options[:retry] - @errors += rep.map{|x| x[:result][0] }.inject(:+) - @failures += rep.map{|x| x[:result][1] }.inject(:+) - @skips += rep.map{|x| x[:result][2] }.inject(:+) + rep.each do |x| + (e, f, s = x[:result]) or next + @errors += e + @failures += f + @skips += s + end end end unless @warnings.empty? warn "" @warnings.uniq! {|w| w[1].message} @warnings.each do |w| + @errors += 1 warn "#{w[0]}: #{w[1].message} (#{w[1].class})" end warn "" @@ -677,7 +850,7 @@ module Test end end - def record(suite, method, assertions, time, error) + def record(suite, method, assertions, time, error, source_location = nil) if @options.values_at(:longest, :most_asserted).any? @tops ||= {} rec = [suite.name, method, assertions, time, error] @@ -756,7 +929,7 @@ module Test end def jobs_status(worker) - return if !@options[:job_status] or @options[:verbose] + return if !@options[:job_status] or @verbose if @options[:job_status] == :replace status_line = @workers.map(&:to_s).join(" ") else @@ -775,7 +948,7 @@ module Test end def _prepare_run(suites, type) - options[:job_status] ||= :replace if @tty && !@verbose + options[:job_status] ||= @tty ? :replace : :normal unless @verbose case options[:color] when :always color = true @@ -791,11 +964,14 @@ module Test @output = Output.new(self) unless @options[:testing] filter = options[:filter] type = "#{type}_methods" - total = if filter - suites.inject(0) {|n, suite| n + suite.send(type).grep(filter).size} - else - suites.inject(0) {|n, suite| n + suite.send(type).size} - end + total = suites.sum {|suite| + methods = suite.send(type) + if filter + methods.count {|method| filter === "#{suite}##{method}"} + else + methods.size + end + } @test_count = 0 @total_tests = total.to_s(10) end @@ -833,7 +1009,7 @@ module Test end first, msg = msg.split(/$/, 2) first = sprintf("%3d) %s", @report_count += 1, first) - $stdout.print(sep, @colorize.decorate(first, color), msg, "\n") + @failed_output.print(sep, @colorize.decorate(first, color), msg, "\n") sep = nil end report.clear @@ -889,7 +1065,7 @@ module Test runner.add_status(" = #$1") when /\A\.+\z/ runner.succeed - when /\A[EFS]\z/ + when /\A\.*[EFST][EFST.]*\z/ runner.failed(s) else $stdout.print(s) @@ -994,6 +1170,28 @@ module Test end end + module OutputOption # :nodoc: all + def setup_options(parser, options) + super + parser.separator "output options:" + + options[:failed_output] = $stdout + parser.on '--stderr-on-failure', 'Use stderr to print failure messages' do + options[:failed_output] = $stderr + end + parser.on '--stdout-on-failure', 'Use stdout to print failure messages', '(default)' do + options[:failed_output] = $stdout + end + end + + def process_args(args = []) + return @options if @options + options = super + @failed_output = options[:failed_output] + options + end + end + module GCOption # :nodoc: all def setup_options(parser, options) super @@ -1008,7 +1206,7 @@ module Test def non_options(files, options) if options.delete(:gc_stress) - MiniTest::Unit::TestCase.class_eval do + Test::Unit::TestCase.class_eval do oldrun = instance_method(:run) define_method(:run) do |runner| begin @@ -1021,7 +1219,7 @@ module Test end end if options.delete(:gc_compact) - MiniTest::Unit::TestCase.class_eval do + Test::Unit::TestCase.class_eval do oldrun = instance_method(:run) define_method(:run) do |runner| begin @@ -1055,8 +1253,13 @@ module Test puts "#{f}: #{$!}" end } + @load_failed = errors.size.nonzero? result end + + def run(*) + super or @load_failed + end end module RepeatOption # :nodoc: all @@ -1158,30 +1361,523 @@ module Test end end - class Runner < MiniTest::Unit # :nodoc: all - include Test::Unit::Options - include Test::Unit::StatusLine - include Test::Unit::Parallel - include Test::Unit::Statistics - include Test::Unit::Skipping - include Test::Unit::GlobOption - include Test::Unit::RepeatOption - include Test::Unit::LoadPathOption - include Test::Unit::GCOption - include Test::Unit::ExcludesOption - include Test::Unit::TimeoutOption - include Test::Unit::RunCount - - def run(argv) + module LaunchableOption + module Nothing + private + def setup_options(opts, options) + super + opts.define_tail 'Launchable options:' + # This is expected to be called by Test::Unit::Worker. + opts.on_tail '--launchable-test-reports=PATH', String, 'Do nothing' + end + end + + def record(suite, method, assertions, time, error, source_location = nil) + if writer = @options[:launchable_test_reports] + if loc = (source_location || suite.instance_method(method).source_location) + path, lineno = loc + # Launchable JSON schema is defined at + # https://github.com/search?q=repo%3Alaunchableinc%2Fcli+https%3A%2F%2Flaunchableinc.com%2Fschema%2FRecordTestInput&type=code. + e = case error + when nil + status = 'TEST_PASSED' + nil + when Test::Unit::PendedError + status = 'TEST_SKIPPED' + "Skipped:\n#{suite.name}##{method} [#{location error}]:\n#{error.message}\n" + when Test::Unit::AssertionFailedError + status = 'TEST_FAILED' + "Failure:\n#{suite.name}##{method} [#{location error}]:\n#{error.message}\n" + when Timeout::Error + status = 'TEST_FAILED' + "Timeout:\n#{suite.name}##{method}\n" + else + status = 'TEST_FAILED' + bt = Test::filter_backtrace(error.backtrace).join "\n " + "Error:\n#{suite.name}##{method}:\n#{error.class}: #{error.message.b}\n #{bt}\n" + end + repo_path = File.expand_path("#{__dir__}/../../../") + relative_path = path.delete_prefix("#{repo_path}/") + # The test path is a URL-encoded representation. + # https://github.com/launchableinc/cli/blob/v1.81.0/launchable/testpath.py#L18 + test_path = {file: relative_path, class: suite.name, testcase: method}.map{|key, val| + "#{encode_test_path_component(key)}=#{encode_test_path_component(val)}" + }.join('#') + end + end + super + ensure + if writer && test_path && status + # Occasionally, the file writing operation may be paused, especially when `--repeat-count` is specified. + # In such cases, we proceed to execute the operation here. + writer.write_object( + { + testPath: test_path, + status: status, + duration: time, + createdAt: Time.now.to_s, + stderr: e, + stdout: nil, + data: { + lineNumber: lineno + } + } + ) + end + end + + private + def setup_options(opts, options) super + opts.on_tail '--launchable-test-reports=PATH', String, 'Report test results in Launchable JSON format' do |path| + require 'json' + require 'uri' + options[:launchable_test_reports] = writer = JsonStreamWriter.new(path) + writer.write_array('testCases') + main_pid = Process.pid + at_exit { + # This block is executed when the fork block in a test is completed. + # Therefore, we need to verify whether all tests have been completed. + stack = caller + if stack.size == 0 && main_pid == Process.pid && $!.is_a?(SystemExit) + writer.close + end + } + end + + def encode_test_path_component component + component.to_s.gsub('%', '%25').gsub('=', '%3D').gsub('#', '%23').gsub('&', '%26') + end + end + + ## + # JsonStreamWriter writes a JSON file using a stream. + # By utilizing a stream, we can minimize memory usage, especially for large files. + class JsonStreamWriter + def initialize(path) + @file = File.open(path, "w") + @file.write("{") + @indent_level = 0 + @is_first_key_val = true + @is_first_obj = true + write_new_line + end + + def write_object obj + if @is_first_obj + @is_first_obj = false + else + write_comma + write_new_line + end + @indent_level += 1 + @file.write(to_json_str(obj)) + @indent_level -= 1 + @is_first_key_val = true + # Occasionally, invalid JSON will be created as shown below, especially when `--repeat-count` is specified. + # { + # "testPath": "file=test%2Ftest_timeout.rb&class=TestTimeout&testcase=test_allows_zero_seconds", + # "status": "TEST_PASSED", + # "duration": 2.7e-05, + # "createdAt": "2024-02-09 12:21:07 +0000", + # "stderr": null, + # "stdout": null + # }: null <- here + # }, + # To prevent this, IO#flush is called here. + @file.flush + end + + def write_array(key) + @indent_level += 1 + @file.write(to_json_str(key)) + write_colon + @file.write(" ", "[") + write_new_line + end + + def close + return if @file.closed? + close_array + @indent_level -= 1 + write_new_line + @file.write("}", "\n") + @file.flush + @file.close + end + + private + def to_json_str(obj) + json = JSON.pretty_generate(obj) + json.gsub(/^/, ' ' * (2 * @indent_level)) + end + + def write_indent + @file.write(" " * 2 * @indent_level) + end + + def write_new_line + @file.write("\n") + end + + def write_comma + @file.write(',') + end + + def write_colon + @file.write(":") + end + + def close_array + write_new_line + write_indent + @file.write("]") + @indent_level -= 1 + end + end + end + + class Runner # :nodoc: all + + attr_accessor :report, :failures, :errors, :skips # :nodoc: + attr_accessor :assertion_count # :nodoc: + attr_writer :test_count # :nodoc: + attr_accessor :start_time # :nodoc: + attr_accessor :help # :nodoc: + attr_accessor :verbose # :nodoc: + attr_writer :options # :nodoc: + + ## + # :attr: + # + # if true, installs an "INFO" signal handler (only available to BSD and + # OS X users) which prints diagnostic information about the test run. + # + # This is auto-detected by default but may be overridden by custom + # runners. + + attr_accessor :info_signal + + ## + # Lazy accessor for options. + + def options + @options ||= {seed: 42} + end + + @@installed_at_exit ||= false + @@out = $stdout + @@after_tests = [] + @@current_repeat_count = 0 + + ## + # A simple hook allowing you to run a block of code after _all_ of + # the tests are done. Eg: + # + # Test::Unit::Runner.after_tests { p $debugging_info } + + def self.after_tests &block + @@after_tests << block + end + + ## + # Returns the stream to use for output. + + def self.output + @@out + end + + ## + # Sets Test::Unit::Runner to write output to +stream+. $stdout is the default + # output + + def self.output= stream + @@out = stream + end + + ## + # Tells Test::Unit::Runner to delegate to +runner+, an instance of a + # Test::Unit::Runner subclass, when Test::Unit::Runner#run is called. + + def self.runner= runner + @@runner = runner + end + + ## + # Returns the Test::Unit::Runner subclass instance that will be used + # to run the tests. A Test::Unit::Runner instance is the default + # runner. + + def self.runner + @@runner ||= self.new + end + + ## + # Return all plugins' run methods (methods that start with "run_"). + + def self.plugins + @@plugins ||= (["run_tests"] + + public_instance_methods(false). + grep(/^run_/).map { |s| s.to_s }).uniq + end + + ## + # Return the IO for output. + + def output + self.class.output + end + + def puts *a # :nodoc: + output.puts(*a) + end + + def print *a # :nodoc: + output.print(*a) + end + + def test_count # :nodoc: + @test_count ||= 0 + end + + ## + # Runner for a given +type+ (eg, test vs bench). + + def self.current_repeat_count + @@current_repeat_count + end + + def _run_anything type + suites = Test::Unit::TestCase.send "#{type}_suites" + return if suites.empty? + + suites = @order.sort_by_name(suites) + + puts + puts "# Running #{type}s:" + puts + + @test_count, @assertion_count = 0, 0 + test_count = assertion_count = 0 + sync = output.respond_to? :"sync=" # stupid emacs + old_sync, output.sync = output.sync, true if sync + + @@current_repeat_count = 0 + begin + start = Time.now + + results = _run_suites suites, type + + @test_count = results.inject(0) { |sum, (tc, _)| sum + tc } + @assertion_count = results.inject(0) { |sum, (_, ac)| sum + ac } + test_count += @test_count + assertion_count += @assertion_count + t = Time.now - start + @@current_repeat_count += 1 + unless @repeat_count + puts + puts + end + puts "Finished%s %ss in %.6fs, %.4f tests/s, %.4f assertions/s.\n" % + [(@repeat_count ? "(#{@@current_repeat_count}/#{@repeat_count}) " : ""), type, + t, @test_count.fdiv(t), @assertion_count.fdiv(t)] + end while @repeat_count && @@current_repeat_count < @repeat_count && + report.empty? && failures.zero? && errors.zero? + + output.sync = old_sync if sync + + report.each_with_index do |msg, i| + puts "\n%3d) %s" % [i + 1, msg] + end + + puts + @test_count = test_count + @assertion_count = assertion_count + + status + end + + ## + # Run a single +suite+ for a given +type+. + + def _run_suite suite, type + header = "#{type}_suite_header" + puts send(header, suite) if respond_to? header + + filter = options[:filter] + + all_test_methods = suite.send "#{type}_methods" + if filter + all_test_methods.select! {|method| + filter === "#{suite}##{method}" + } + end + all_test_methods = @order.sort_by_name(all_test_methods) + + leakchecker = LeakChecker.new + if ENV["LEAK_CHECKER_TRACE_OBJECT_ALLOCATION"] + require "objspace" + trace = true + end + + assertions = all_test_methods.map { |method| + + inst = suite.new method + _start_method(inst) + inst._assertions = 0 + + print "#{suite}##{method.inspect.sub(/\A:/, '')} = " if @verbose + + start_time = Time.now if @verbose + result = + if trace + ObjectSpace.trace_object_allocations {inst.run self} + else + inst.run self + end + + print "%.2f s = " % (Time.now - start_time) if @verbose + print result + puts if @verbose + $stdout.flush + + unless defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? # compiler process is wrongly considered as leak + leakchecker.check("#{inst.class}\##{inst.__name__}") + end + + _end_method(inst) + + inst._assertions + } + return assertions.size, assertions.inject(0) { |sum, n| sum + n } + end + + def _start_method(inst) + end + def _end_method(inst) + end + + ## + # Record the result of a single test. Makes it very easy to gather + # information. Eg: + # + # class StatisticsRecorder < Test::Unit::Runner + # def record suite, method, assertions, time, error + # # ... record the results somewhere ... + # end + # end + # + # Test::Unit::Runner.runner = StatisticsRecorder.new + # + # NOTE: record might be sent more than once per test. It will be + # sent once with the results from the test itself. If there is a + # failure or error in teardown, it will be sent again with the + # error or failure. + + def record suite, method, assertions, time, error, source_location = nil + end + + def location e # :nodoc: + last_before_assertion = "" + + return '<empty>' unless e&.backtrace # SystemStackError can return nil. + + e.backtrace.reverse_each do |s| + break if s =~ /in .(?:Test::Unit::(?:Core)?Assertions#)?(assert|refute|flunk|pass|fail|raise|must|wont)/ + last_before_assertion = s + end + last_before_assertion.sub(/:in .*$/, '') + end + + ## + # Writes status for failed test +meth+ in +klass+ which finished with + # exception +e+ + + def initialize # :nodoc: + @report = [] + @errors = @failures = @skips = 0 + @verbose = false + @mutex = Thread::Mutex.new + @info_signal = Signal.list['INFO'] + @repeat_count = nil + end + + def synchronize # :nodoc: + if @mutex then + @mutex.synchronize { yield } + else + yield + end + end + + def inspect + "#<#{self.class.name}: " << + instance_variables.filter_map do |var| + next if var == :@option_parser # too big + "#{var}=#{instance_variable_get(var).inspect}" + end.join(", ") << ">" + end + + ## + # Top level driver, controls all output and filtering. + + def _run args = [] + args = process_args args # ARGH!! blame test/unit process_args + self.options.merge! args + + puts "Run options: #{help}" + + self.class.plugins.each do |plugin| + send plugin + break unless report.empty? + end + + return (failures + errors).nonzero? # or return nil... + rescue Interrupt + abort 'Interrupted' + end + + ## + # Runs test suites matching +filter+. + + def run_tests + _run_anything :test + end + + ## + # Writes status to +io+ + + def status io = self.output + format = "%d tests, %d assertions, %d failures, %d errors, %d skips" + io.puts format % [test_count, assertion_count, failures, errors, skips] + end + + prepend Test::Unit::Options + prepend Test::Unit::StatusLine + prepend Test::Unit::Parallel + prepend Test::Unit::Statistics + prepend Test::Unit::Skipping + prepend Test::Unit::GlobOption + prepend Test::Unit::OutputOption + prepend Test::Unit::RepeatOption + prepend Test::Unit::LoadPathOption + prepend Test::Unit::GCOption + prepend Test::Unit::ExcludesOption + prepend Test::Unit::TimeoutOption + prepend Test::Unit::RunCount + prepend Test::Unit::LaunchableOption::Nothing + + ## + # Begins the full test run. Delegates to +runner+'s #_run method. + + def run(argv = []) + self.class.runner._run(argv) 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 def self.autorun at_exit { @@ -1192,16 +1888,30 @@ module Test @@installed_at_exit = true end - alias mini_run_suite _run_suite + alias orig_run_suite _run_suite - # Overriding of MiniTest::Unit#puke + # Overriding of Test::Unit::Runner#puke def puke klass, meth, e - # TODO: - # this overriding is for minitest feature that skip messages are - # hidden when not verbose (-v), note this is temporally. n = report.size - rep = super - if MiniTest::Skip === e and /no message given\z/ =~ e.message + e = case e + when Test::Unit::PendedError then + @skips += 1 + return "S" unless @verbose + "Skipped:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n" + when Test::Unit::AssertionFailedError then + @failures += 1 + "Failure:\n#{klass}##{meth} [#{location e}]:\n#{e.message}\n" + when Timeout::Error + @errors += 1 + "Timeout:\n#{klass}##{meth}\n" + else + @errors += 1 + bt = Test::filter_backtrace(e.backtrace).join "\n " + "Error:\n#{klass}##{meth}:\n#{e.class}: #{e.message.b}\n #{bt}\n" + end + @report << e + rep = e[0, 1] + if Test::Unit::PendedError === e and /no message given\z/ =~ e.message report.slice!(n..-1) rep = "." end @@ -1212,6 +1922,7 @@ module Test class AutoRunner # :nodoc: all class Runner < Test::Unit::Runner include Test::Unit::RequireFiles + include Test::Unit::LaunchableOption end attr_accessor :to_run, :options @@ -1262,30 +1973,4 @@ module Test end end -module MiniTest # :nodoc: all - class Unit - end -end - -class MiniTest::Unit::TestCase # :nodoc: all - test_order = self.test_order - class << self - attr_writer :test_order - undef test_order - end - def self.test_order - defined?(@test_order) ? @test_order : superclass.test_order - end - self.test_order = test_order - undef run_test - RUN_TEST_TRACE = "#{__FILE__}:#{__LINE__+3}:in `run_test'".freeze - def run_test(name) - progname, $0 = $0, "#{$0}: #{self.class}##{name}" - self.__send__(name) - ensure - $@.delete(RUN_TEST_TRACE) if $@ - $0 = progname - end -end - Test::Unit::Runner.autorun diff --git a/tool/lib/test/unit/assertions.rb b/tool/lib/test/unit/assertions.rb index c61f296da9..b4f1dbc176 100644 --- a/tool/lib/test/unit/assertions.rb +++ b/tool/lib/test/unit/assertions.rb @@ -1,12 +1,553 @@ # frozen_string_literal: true -require 'minitest/unit' -require_relative '../../core_assertions' require 'pp' module Test module Unit module Assertions - include Test::Unit::CoreAssertions + + ## + # Returns the diff command to use in #diff. Tries to intelligently + # figure out what diff to use. + + def self.diff + unless defined? @diff + exe = RbConfig::CONFIG['EXEEXT'] + @diff = %W"gdiff#{exe} diff#{exe}".find do |diff| + if system(diff, "-u", __FILE__, __FILE__) + break "#{diff} -u" + end + end + end + + @diff + end + + ## + # Set the diff command to use in #diff. + + def self.diff= o + @diff = o + end + + ## + # Returns a diff between +exp+ and +act+. If there is no known + # diff command or if it doesn't make sense to diff the output + # (single line, short output), then it simply returns a basic + # comparison between the two. + + def diff exp, act + require "tempfile" + + expect = mu_pp_for_diff exp + butwas = mu_pp_for_diff act + result = nil + + need_to_diff = + self.class.diff && + (expect.include?("\n") || + butwas.include?("\n") || + expect.size > 30 || + butwas.size > 30 || + expect == butwas) + + return "Expected: #{mu_pp exp}\n Actual: #{mu_pp act}" unless + need_to_diff + + tempfile_a = nil + tempfile_b = nil + + Tempfile.open("expect") do |a| + tempfile_a = a + a.puts expect + a.flush + + Tempfile.open("butwas") do |b| + tempfile_b = b + b.puts butwas + b.flush + + result = `#{self.class.diff} #{a.path} #{b.path}` + result.sub!(/^\-\-\- .+/, "--- expected") + result.sub!(/^\+\+\+ .+/, "+++ actual") + + if result.empty? then + klass = exp.class + result = [ + "No visible difference in the #{klass}#inspect output.\n", + "You should look at the implementation of #== on ", + "#{klass} or its members.\n", + expect, + ].join + end + end + end + + result + ensure + tempfile_a.close! if tempfile_a + tempfile_b.close! if tempfile_b + end + + ## + # This returns a diff-able human-readable version of +obj+. This + # differs from the regular mu_pp because it expands escaped + # newlines and makes hex-values generic (like object_ids). This + # uses mu_pp to do the first pass and then cleans it up. + + def mu_pp_for_diff obj + mu_pp(obj).gsub(/(?<!\\)(?:\\\\)*\K\\n/, "\n").gsub(/:0x[a-fA-F0-9]{4,}/m, ':0xXXXXXX') + end + + ## + # Fails unless +test+ is a true value. + + def assert test, msg = nil + msg ||= "Failed assertion, no message given." + self._assertions += 1 + unless test then + msg = msg.call if Proc === msg + raise Test::Unit::AssertionFailedError, msg + end + true + end + + ## + # Fails unless +obj+ is empty. + + def assert_empty obj, msg = nil + msg = message(msg) { "Expected #{mu_pp(obj)} to be empty" } + assert_respond_to obj, :empty? + assert obj.empty?, msg + end + + ## + # For comparing Floats. Fails unless +exp+ and +act+ are within +delta+ + # of each other. + # + # assert_in_delta Math::PI, (22.0 / 7.0), 0.01 + + def assert_in_delta exp, act, delta = 0.001, msg = nil + n = (exp - act).abs + msg = message(msg) { + "Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}" + } + assert delta >= n, msg + end + + ## + # For comparing Floats. Fails unless +exp+ and +act+ have a relative + # error less than +epsilon+. + + def assert_in_epsilon a, b, epsilon = 0.001, msg = nil + assert_in_delta a, b, [a.abs, b.abs].min * epsilon, msg + end + + ## + # Fails unless +collection+ includes +obj+. + + def assert_includes collection, obj, msg = nil + msg = message(msg) { + "Expected #{mu_pp(collection)} to include #{mu_pp(obj)}" + } + assert_respond_to collection, :include? + assert collection.include?(obj), msg + end + + ## + # Fails unless +obj+ is an instance of +cls+. + + def assert_instance_of cls, obj, msg = nil + msg = message(msg) { + "Expected #{mu_pp(obj)} to be an instance of #{cls}, not #{obj.class}" + } + + assert obj.instance_of?(cls), msg + end + + ## + # Fails unless +obj+ is a kind of +cls+. + + def assert_kind_of cls, obj, msg = nil # TODO: merge with instance_of + msg = message(msg) { + "Expected #{mu_pp(obj)} to be a kind of #{cls}, not #{obj.class}" } + + assert obj.kind_of?(cls), msg + end + + ## + # Fails unless +matcher+ <tt>=~</tt> +obj+. + + def assert_match matcher, obj, msg = nil + msg = message(msg) { "Expected #{mu_pp matcher} to match #{mu_pp obj}" } + assert_respond_to matcher, :"=~" + matcher = Regexp.new Regexp.escape matcher if String === matcher + assert matcher =~ obj, msg + end + + ## + # Fails unless +obj+ is nil + + def assert_nil obj, msg = nil + msg = message(msg) { "Expected #{mu_pp(obj)} to be nil" } + assert obj.nil?, msg + end + + ## + # Fails unless +obj+ is true + + def assert_true obj, msg = nil + msg = message(msg) { "Expected #{mu_pp(obj)} to be true" } + assert obj == true, msg + end + + ## + # Fails unless +obj+ is false + + def assert_false obj, msg = nil + msg = message(msg) { "Expected #{mu_pp(obj)} to be false" } + assert obj == false, msg + end + + ## + # For testing with binary operators. + # + # assert_operator 5, :<=, 4 + + def assert_operator o1, op, o2 = (predicate = true; nil), msg = nil + return assert_predicate o1, op, msg if predicate + msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op} #{mu_pp(o2)}" } + assert o1.__send__(op, o2), msg + end + + ## + # Fails if stdout or stderr do not output the expected results. + # Pass in nil if you don't care about that streams output. Pass in + # "" if you require it to be silent. Pass in a regexp if you want + # to pattern match. + # + # NOTE: this uses #capture_io, not #capture_subprocess_io. + # + # See also: #assert_silent + + def assert_output stdout = nil, stderr = nil + out, err = capture_output do + yield + end + + err_msg = Regexp === stderr ? :assert_match : :assert_equal if stderr + out_msg = Regexp === stdout ? :assert_match : :assert_equal if stdout + + y = send err_msg, stderr, err, "In stderr" if err_msg + x = send out_msg, stdout, out, "In stdout" if out_msg + + (!stdout || x) && (!stderr || y) + end + + ## + # For testing with predicates. + # + # assert_predicate str, :empty? + # + # This is really meant for specs and is front-ended by assert_operator: + # + # str.must_be :empty? + + def assert_predicate o1, op, msg = nil + msg = message(msg) { "Expected #{mu_pp(o1)} to be #{op}" } + assert o1.__send__(op), msg + end + + ## + # Fails unless +obj+ responds to +meth+. + + def assert_respond_to obj, meth, msg = nil + msg = message(msg) { + "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}" + } + assert obj.respond_to?(meth), msg + end + + ## + # Fails unless +exp+ and +act+ are #equal? + + def assert_same exp, act, msg = nil + msg = message(msg) { + data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id] + "Expected %s (oid=%d) to be the same as %s (oid=%d)" % data + } + assert exp.equal?(act), msg + end + + ## + # Fails if the block outputs anything to stderr or stdout. + # + # See also: #assert_output + + def assert_silent + assert_output "", "" do + yield + end + end + + ## + # Fails unless the block throws +sym+ + + def assert_throws sym, msg = nil + default = "Expected #{mu_pp(sym)} to have been thrown" + caught = true + catch(sym) do + begin + yield + rescue ThreadError => e # wtf?!? 1.8 + threads == suck + default += ", not \:#{e.message[/uncaught throw \`(\w+?)\'/, 1]}" + rescue ArgumentError => e # 1.9 exception + default += ", not #{e.message.split(/ /).last}" + rescue NameError => e # 1.8 exception + default += ", not #{e.name.inspect}" + end + caught = false + end + + assert caught, message(msg) { default } + end + + def assert_path_exists(path, msg = nil) + msg = message(msg) { "Expected path '#{path}' to exist" } + assert File.exist?(path), msg + end + alias assert_path_exist assert_path_exists + alias refute_path_not_exist assert_path_exists + + def refute_path_exists(path, msg = nil) + msg = message(msg) { "Expected path '#{path}' to not exist" } + refute File.exist?(path), msg + end + alias refute_path_exist refute_path_exists + alias assert_path_not_exist refute_path_exists + + ## + # Captures $stdout and $stderr into strings: + # + # out, err = capture_output do + # puts "Some info" + # warn "You did a bad thing" + # end + # + # assert_match %r%info%, out + # assert_match %r%bad%, err + + def capture_output + require 'stringio' + + captured_stdout, captured_stderr = StringIO.new, StringIO.new + + synchronize do + orig_stdout, orig_stderr = $stdout, $stderr + $stdout, $stderr = captured_stdout, captured_stderr + + begin + yield + ensure + $stdout = orig_stdout + $stderr = orig_stderr + end + end + + return captured_stdout.string, captured_stderr.string + end + + def capture_io + raise NoMethodError, "use capture_output" + end + + ## + # Fails with +msg+ + + def flunk msg = nil + msg ||= "Epic Fail!" + assert false, msg + end + + ## + # used for counting assertions + + def pass msg = nil + assert true + end + + ## + # Fails if +test+ is a true value + + def refute test, msg = nil + msg ||= "Failed refutation, no message given" + not assert(! test, msg) + end + + ## + # Fails if +obj+ is empty. + + def refute_empty obj, msg = nil + msg = message(msg) { "Expected #{mu_pp(obj)} to not be empty" } + assert_respond_to obj, :empty? + refute obj.empty?, msg + end + + ## + # Fails if <tt>exp == act</tt>. + # + # For floats use refute_in_delta. + + def refute_equal exp, act, msg = nil + msg = message(msg) { + "Expected #{mu_pp(act)} to not be equal to #{mu_pp(exp)}" + } + refute exp == act, msg + end + + ## + # For comparing Floats. Fails if +exp+ is within +delta+ of +act+. + # + # refute_in_delta Math::PI, (22.0 / 7.0) + + def refute_in_delta exp, act, delta = 0.001, msg = nil + n = (exp - act).abs + msg = message(msg) { + "Expected |#{exp} - #{act}| (#{n}) to not be <= #{delta}" + } + refute delta >= n, msg + end + + ## + # For comparing Floats. Fails if +exp+ and +act+ have a relative error + # less than +epsilon+. + + def refute_in_epsilon a, b, epsilon = 0.001, msg = nil + refute_in_delta a, b, a * epsilon, msg + end + + ## + # Fails if +collection+ includes +obj+. + + def refute_includes collection, obj, msg = nil + msg = message(msg) { + "Expected #{mu_pp(collection)} to not include #{mu_pp(obj)}" + } + assert_respond_to collection, :include? + refute collection.include?(obj), msg + end + + ## + # Fails if +obj+ is an instance of +cls+. + + def refute_instance_of cls, obj, msg = nil + msg = message(msg) { + "Expected #{mu_pp(obj)} to not be an instance of #{cls}" + } + refute obj.instance_of?(cls), msg + end + + ## + # Fails if +obj+ is a kind of +cls+. + + def refute_kind_of cls, obj, msg = nil # TODO: merge with instance_of + msg = message(msg) { "Expected #{mu_pp(obj)} to not be a kind of #{cls}" } + refute obj.kind_of?(cls), msg + end + + ## + # Fails if +matcher+ <tt>=~</tt> +obj+. + + def refute_match matcher, obj, msg = nil + msg = message(msg) {"Expected #{mu_pp matcher} to not match #{mu_pp obj}"} + assert_respond_to matcher, :"=~" + matcher = Regexp.new Regexp.escape matcher if String === matcher + refute matcher =~ obj, msg + end + + ## + # Fails if +obj+ is nil. + + def refute_nil obj, msg = nil + msg = message(msg) { "Expected #{mu_pp(obj)} to not be nil" } + refute obj.nil?, msg + end + + ## + # Fails if +o1+ is not +op+ +o2+. Eg: + # + # refute_operator 1, :>, 2 #=> pass + # refute_operator 1, :<, 2 #=> fail + + def refute_operator o1, op, o2 = (predicate = true; nil), msg = nil + return refute_predicate o1, op, msg if predicate + msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op} #{mu_pp(o2)}"} + refute o1.__send__(op, o2), msg + end + + ## + # For testing with predicates. + # + # refute_predicate str, :empty? + # + # This is really meant for specs and is front-ended by refute_operator: + # + # str.wont_be :empty? + + def refute_predicate o1, op, msg = nil + msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op}" } + refute o1.__send__(op), msg + end + + ## + # Fails if +obj+ responds to the message +meth+. + + def refute_respond_to obj, meth, msg = nil + msg = message(msg) { "Expected #{mu_pp(obj)} to not respond to #{meth}" } + + refute obj.respond_to?(meth), msg + end + + ## + # Fails if +exp+ is the same (by object identity) as +act+. + + def refute_same exp, act, msg = nil + msg = message(msg) { + data = [mu_pp(act), act.object_id, mu_pp(exp), exp.object_id] + "Expected %s (oid=%d) to not be the same as %s (oid=%d)" % data + } + refute exp.equal?(act), msg + end + + ## + # Skips the current test. Gets listed at the end of the run but + # doesn't cause a failure exit code. + + def pend msg = nil, bt = caller + msg ||= "Skipped, no message given" + @skip = true + raise Test::Unit::PendedError, msg, bt + end + alias omit pend + + def skip(msg = nil, bt = caller) + raise NoMethodError, "use omit or pend", caller + end + + ## + # Was this testcase skipped? Meant for #teardown. + + def skipped? + defined?(@skip) and @skip + end + + ## + # Takes a block and wraps it with the runner's shared mutex. + + def synchronize + Test::Unit::Runner.runner.synchronize do + yield + end + end # :call-seq: # assert_block( failure_message = nil ) @@ -22,10 +563,6 @@ module Test assert yield, *msgs end - def assert_raises(*exp, &b) - raise NoMethodError, "use assert_raise", caller - end - # :call-seq: # assert_nothing_thrown( failure_message = nil, &block ) # @@ -225,9 +762,6 @@ EOT assert(failed.empty?, message(m) {failed.pretty_inspect}) end - # compatibility with test-unit - alias pend skip - def assert_syntax_error(code, error, *args, **opt) prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg| yield if defined?(yield) @@ -253,7 +787,7 @@ EOT # kernel resolution can limit the minimum time we can measure # [ruby-core:81540] - MIN_HZ = MiniTest::Unit::TestCase.windows? ? 67 : 100 + MIN_HZ = /mswin|mingw/ =~ RUBY_PLATFORM ? 67 : 100 MIN_MEASURABLE = 1.0 / MIN_HZ def assert_cpu_usage_low(msg = nil, pct: 0.05, wait: 1.0, stop: nil) @@ -290,14 +824,6 @@ EOT assert(1.0/f == -Float::INFINITY, "#{f} is not -0.0") end - 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 build_message(head, template=nil, *arguments) #:nodoc: template &&= template.chomp template.gsub(/\G((?:[^\\]|\\.)*?)(\\)?\?/) { $1 + ($2 ? "?" : mu_pp(arguments.shift)) } diff --git a/tool/lib/test/unit/parallel.rb b/tool/lib/test/unit/parallel.rb index ccaf1a913a..ac297d4a0e 100644 --- a/tool/lib/test/unit/parallel.rb +++ b/tool/lib/test/unit/parallel.rb @@ -1,12 +1,6 @@ # frozen_string_literal: true -$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../.." -require 'test/unit' -require "profile_test_all" if ENV.key?('RUBY_TEST_ALL_PROFILE') -require "tracepointchecker" -require "zombie_hunter" -require "iseq_loader_checker" -require "gc_compact_checker" +require_relative "../../../test/init" module Test module Unit @@ -15,7 +9,6 @@ module Test undef autorun end - alias orig_run_suite mini_run_suite undef _run_suite undef _run_suites undef run @@ -32,12 +25,16 @@ module Test end end + def _start_method(inst) + _report "start", Marshal.dump([inst.class.name, inst.__name__]) + end + def _run_suite(suite, type) # :nodoc: @partial_report = [] - orig_testout = MiniTest::Unit.output + orig_testout = Test::Unit::Runner.output i,o = IO.pipe - MiniTest::Unit.output = o + Test::Unit::Runner.output = o orig_stdin, orig_stdout = $stdin, $stdout th = Thread.new do @@ -58,7 +55,7 @@ module Test result = [nil,nil] end - MiniTest::Unit.output = orig_testout + Test::Unit::Runner.output = orig_testout $stdin = orig_stdin $stdout = orig_stdout @@ -79,7 +76,7 @@ module Test _report "done", Marshal.dump(result) return result ensure - MiniTest::Unit.output = orig_stdout + Test::Unit::Runner.output = orig_stdout $stdin = orig_stdin if orig_stdin $stdout = orig_stdout if orig_stdout o.close if o && !o.closed? @@ -108,12 +105,12 @@ module Test case buf.chomp when /^loadpath (.+?)$/ @old_loadpath = $:.dup - $:.push(*Marshal.load($1.unpack("m")[0].force_encoding("ASCII-8BIT"))).uniq! + $:.push(*Marshal.load($1.unpack1("m").force_encoding("ASCII-8BIT"))).uniq! when /^run (.+?) (.+?)$/ _report "okay" @options = @opts.dup - suites = MiniTest::Unit::TestCase.test_suites + suites = Test::Unit::TestCase.test_suites begin require File.realpath($1) @@ -122,7 +119,7 @@ module Test _report "ready" next end - _run_suites MiniTest::Unit::TestCase.test_suites-suites, $2.to_sym + _run_suites Test::Unit::TestCase.test_suites-suites, $2.to_sym if @need_exit _report "bye" @@ -160,21 +157,21 @@ module Test end def puke(klass, meth, e) # :nodoc: - if e.is_a?(MiniTest::Skip) - new_e = MiniTest::Skip.new(e.message) + if e.is_a?(Test::Unit::PendedError) + new_e = Test::Unit::PendedError.new(e.message) new_e.set_backtrace(e.backtrace) e = new_e end - @partial_report << [klass.name, meth, e.is_a?(MiniTest::Assertion) ? e : ProxyError.new(e)] + @partial_report << [klass.name, meth, e.is_a?(Test::Unit::AssertionFailedError) ? e : ProxyError.new(e)] super end def record(suite, method, assertions, time, error) # :nodoc: case error when nil - when MiniTest::Assertion, MiniTest::Skip + when Test::Unit::AssertionFailedError, Test::Unit::PendedError case error.cause - when nil, MiniTest::Assertion, MiniTest::Skip + when nil, Test::Unit::AssertionFailedError, Test::Unit::PendedError else bt = error.backtrace error = error.class.new(error.message) @@ -183,7 +180,7 @@ module Test else error = ProxyError.new(error) end - _report "record", Marshal.dump([suite.name, method, assertions, time, error]) + _report "record", Marshal.dump([suite.name, method, assertions, time, error, suite.instance_method(method).source_location]) super end end @@ -193,7 +190,7 @@ end if $0 == __FILE__ module Test module Unit - class TestCase < MiniTest::Unit::TestCase # :nodoc: all + class TestCase # :nodoc: all undef on_parallel_worker? def on_parallel_worker? true @@ -205,5 +202,9 @@ if $0 == __FILE__ end end require 'rubygems' + begin + require 'rake' + rescue LoadError + end Test::Unit::Worker.new.run(ARGV) end diff --git a/tool/lib/test/unit/testcase.rb b/tool/lib/test/unit/testcase.rb index 68149a4880..51ffff37eb 100644 --- a/tool/lib/test/unit/testcase.rb +++ b/tool/lib/test/unit/testcase.rb @@ -1,21 +1,287 @@ # frozen_string_literal: true -require 'test/unit/assertions' +require_relative 'assertions' +require_relative '../../core_assertions' module Test module Unit - # remove silly TestCase class - remove_const(:TestCase) if defined?(self::TestCase) - class TestCase < MiniTest::Unit::TestCase # :nodoc: all - include Assertions + ## + # Provides a simple set of guards that you can use in your tests + # to skip execution if it is not applicable. These methods are + # mixed into TestCase as both instance and class methods so you + # can use them inside or outside of the test methods. + # + # def test_something_for_mri + # skip "bug 1234" if jruby? + # # ... + # end + # + # if windows? then + # # ... lots of test methods ... + # end - def on_parallel_worker? - false + module Guard + + ## + # Is this running on jruby? + + def jruby? platform = RUBY_PLATFORM + "java" == platform end + ## + # Is this running on mri? + + def mri? platform = RUBY_DESCRIPTION + /^ruby/ =~ platform + end + + ## + # Is this running on windows? + + def windows? platform = RUBY_PLATFORM + /mswin|mingw/ =~ platform + end + + ## + # Is this running on mingw? + + def mingw? platform = RUBY_PLATFORM + /mingw/ =~ platform + end + + end + + ## + # Provides before/after hooks for setup and teardown. These are + # meant for library writers, NOT for regular test authors. See + # #before_setup for an example. + + module LifecycleHooks + ## + # Runs before every test, after setup. This hook is meant for + # libraries to extend Test::Unit. It is not meant to be used by + # test developers. + # + # See #before_setup for an example. + + def after_setup; end + + ## + # Runs before every test, before setup. This hook is meant for + # libraries to extend Test::Unit. It is not meant to be used by + # test developers. + # + # As a simplistic example: + # + # module MyTestUnitPlugin + # def before_setup + # super + # # ... stuff to do before setup is run + # end + # + # def after_setup + # # ... stuff to do after setup is run + # super + # end + # + # def before_teardown + # super + # # ... stuff to do before teardown is run + # end + # + # def after_teardown + # # ... stuff to do after teardown is run + # super + # end + # end + # + # class Test::Unit::Runner::TestCase + # include MyTestUnitPlugin + # end + + def before_setup; end + + ## + # Runs after every test, before teardown. This hook is meant for + # libraries to extend Test::Unit. It is not meant to be used by + # test developers. + # + # See #before_setup for an example. + + def before_teardown; end + + ## + # Runs after every test, after teardown. This hook is meant for + # libraries to extend Test::Unit. It is not meant to be used by + # test developers. + # + # See #before_setup for an example. + + def after_teardown; end + end + + ## + # Subclass TestCase to create your own tests. Typically you'll want a + # TestCase subclass per implementation class. + # + # See <code>Test::Unit::AssertionFailedError</code>s + + class TestCase + include Assertions + include CoreAssertions + + include LifecycleHooks + include Guard + extend Guard + + attr_reader :__name__ # :nodoc: + + # Method name of this test. + alias method_name __name__ + + PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException, + Interrupt, SystemExit] # :nodoc: + + ## + # Runs the tests reporting the status to +runner+ + def run runner - @options = runner.options - super runner + @__runner_options__ = runner.options + trap "INFO" do + runner.report.each_with_index do |msg, i| + warn "\n%3d) %s" % [i + 1, msg] + end + warn '' + time = runner.start_time ? Time.now - runner.start_time : 0 + warn "Current Test: %s#%s %.2fs" % [self.class, self.__name__, time] + runner.status $stderr + end if runner.info_signal + + start_time = Time.now + + result = "" + + begin + @__passed__ = nil + self.before_setup + self.setup + self.after_setup + self.run_test self.__name__ + result = "." unless io? + time = Time.now - start_time + runner.record self.class, self.__name__, self._assertions, time, nil + @__passed__ = true + rescue *PASSTHROUGH_EXCEPTIONS + raise + rescue Exception => e + @__passed__ = Test::Unit::PendedError === e + time = Time.now - start_time + runner.record self.class, self.__name__, self._assertions, time, e + result = runner.puke self.class, self.__name__, e + ensure + %w{ before_teardown teardown after_teardown }.each do |hook| + begin + self.send hook + rescue *PASSTHROUGH_EXCEPTIONS + raise + rescue Exception => e + @__passed__ = false + runner.record self.class, self.__name__, self._assertions, time, e + result = runner.puke self.class, self.__name__, e + end + end + trap 'INFO', 'DEFAULT' if runner.info_signal + end + result + end + + RUN_TEST_TRACE = "#{__FILE__}:#{__LINE__+3}:in `run_test'".freeze + def run_test(name) + progname, $0 = $0, "#{$0}: #{self.class}##{name}" + self.__send__(name) + ensure + $@.delete(RUN_TEST_TRACE) if $@ + $0 = progname + end + + def initialize name # :nodoc: + @__name__ = name + @__io__ = nil + @__passed__ = nil + @@__current__ = self # FIX: make thread local + end + + def self.current # :nodoc: + @@__current__ # FIX: make thread local + end + + ## + # Return the output IO object + + def io + @__io__ = true + Test::Unit::Runner.output + end + + ## + # Have we hooked up the IO yet? + + def io? + @__io__ + end + + def self.reset # :nodoc: + @@test_suites = {} + @@test_suites[self] = true + end + + reset + + def self.inherited klass # :nodoc: + @@test_suites[klass] = true + super + end + + @test_order = :sorted + + class << self + attr_writer :test_order + end + + def self.test_order + defined?(@test_order) ? @test_order : superclass.test_order + end + + def self.test_suites # :nodoc: + @@test_suites.keys + end + + def self.test_methods # :nodoc: + public_instance_methods(true).grep(/^test/) + end + + ## + # Returns true if the test passed. + + def passed? + @__passed__ + end + + ## + # Runs before every test. Use this to set up before each test + # run. + + def setup; end + + ## + # Runs after every test. Use this to clean up after each test + # run. + + def teardown; end + + def on_parallel_worker? + false end def self.method_added(name) @@ -23,7 +289,7 @@ module Test return unless name.to_s.start_with?("test_") @test_methods ||= {} if @test_methods[name] - warn "test/unit warning: method #{ self }##{ name } is redefined" + raise AssertionFailedError, "test/unit: method #{ self }##{ name } is redefined" end @test_methods[name] = true end diff --git a/tool/lib/vcs.rb b/tool/lib/vcs.rb index 5d78d94c26..3894f9c8e8 100644 --- a/tool/lib/vcs.rb +++ b/tool/lib/vcs.rb @@ -1,6 +1,8 @@ # vcs require 'fileutils' require 'optparse' +require 'pp' +require 'tempfile' # This library is used by several other tools/ scripts to detect the current # VCS in use (e.g. SVN, Git) or to interact with that VCS. @@ -9,6 +11,22 @@ ENV.delete('PWD') class VCS DEBUG_OUT = STDERR.dup + + def self.dump(obj, pre = nil) + out = DEBUG_OUT + @pp ||= PP.new(out) + @pp.guard_inspect_key do + if pre + @pp.group(pre.size, pre) { + obj.pretty_print(@pp) + } + else + obj.pretty_print(@pp) + end + @pp.flush + out << "\n" + end + end end unless File.respond_to? :realpath @@ -19,14 +37,14 @@ unless File.respond_to? :realpath end def IO.pread(*args) - VCS::DEBUG_OUT.puts(args.inspect) if $DEBUG + VCS.dump(args, "args: ") if $DEBUG popen(*args) {|f|f.read} end module DebugPOpen refine IO.singleton_class do def popen(*args) - VCS::DEBUG_OUT.puts args.inspect if $DEBUG + VCS.dump(args, "args: ") if $DEBUG super end end @@ -34,7 +52,7 @@ end using DebugPOpen module DebugSystem def system(*args) - VCS::DEBUG_OUT.puts args.inspect if $DEBUG + VCS.dump(args, "args: ") if $DEBUG exception = false opts = Hash.try_convert(args[-1]) if RUBY_VERSION >= "2.6" @@ -69,6 +87,9 @@ class VCS begin @@dirs.each do |dir, klass, pred| if pred ? pred[curr, dir] : File.directory?(File.join(curr, dir)) + if klass.const_defined?(:COMMAND) + IO.pread([{'LANG' => 'C', 'LC_ALL' => 'C'}, klass::COMMAND, "--version"]) rescue next + end vcs = klass.new(curr) vcs.define_options(parser) if parser vcs.set_options(options) @@ -92,9 +113,23 @@ class VCS parser.separator(" VCS common options:") parser.define("--[no-]dryrun") {|v| opts[:dryrun] = v} parser.define("--[no-]debug") {|v| opts[:debug] = v} + parser.define("-z", "--zone=OFFSET", /\A[-+]\d\d:\d\d\z/) {|v| opts[:zone] = v} opts end + def release_date(time) + t = time.getlocal(@zone) + [ + t.strftime('#define RUBY_RELEASE_YEAR %Y'), + t.strftime('#define RUBY_RELEASE_MONTH %-m'), + t.strftime('#define RUBY_RELEASE_DAY %-d'), + ] + end + + def self.short_revision(rev) + rev + end + attr_reader :srcdir def initialize(path) @@ -112,14 +147,14 @@ class VCS def set_options(opts) @debug = opts.fetch(:debug) {$DEBUG} @dryrun = opts.fetch(:dryrun) {@debug} + @zone = opts.fetch(:zone) {'+09:00'} end attr_reader :dryrun, :debug alias dryrun? dryrun alias debug? debug - NullDevice = defined?(IO::NULL) ? IO::NULL : - %w[/dev/null NUL NIL: NL:].find {|dev| File.exist?(dev)} + NullDevice = IO::NULL # returns # * the last revision of the current branch @@ -132,7 +167,7 @@ class VCS end last, changed, modified, *rest = ( begin - if NullDevice + if NullDevice and !debug? save_stderr = STDERR.dup STDERR.reopen NullDevice, 'w' end @@ -159,6 +194,7 @@ class VCS rescue ArgumentError modified = Time.utc(*$~[1..6]) + $7.to_i * 3600 + $8.to_i * 60 end + modified = modified.getlocal(@zone) end return last, changed, modified, *rest end @@ -190,6 +226,7 @@ class VCS def after_export(dir) FileUtils.rm_rf(Dir.glob("#{dir}/.git*")) + FileUtils.rm_rf(Dir.glob("#{dir}/.mailmap")) end def revision_handler(rev) @@ -204,6 +241,36 @@ class VCS revision_handler(rev).short_revision(rev) end + # make-snapshot generates only release_date whereas file2lastrev generates both release_date and release_datetime + def revision_header(last, release_date, release_datetime = nil, branch = nil, title = nil, limit: 20) + short = short_revision(last) + if /[^\x00-\x7f]/ =~ title and title.respond_to?(:force_encoding) + title = title.dup.force_encoding("US-ASCII") + end + code = [ + "#define RUBY_REVISION #{short.inspect}", + ] + unless short == last + code << "#define RUBY_FULL_REVISION #{last.inspect}" + end + if branch + e = '..' + name = branch.sub(/\A(.{#{limit-e.size}}).{#{e.size+1},}/o) {$1+e} + name = name.dump.sub(/\\#/, '#') + code << "#define RUBY_BRANCH_NAME #{name}" + end + if title + title = title.dump.sub(/\\#/, '#') + code << "#define RUBY_LAST_COMMIT_TITLE #{title}" + end + if release_datetime + t = release_datetime.utc + code << t.strftime('#define RUBY_RELEASE_DATETIME "%FT%TZ"') + end + code += self.release_date(release_date) + code + end + class SVN < self register(".svn") COMMAND = ENV['SVN'] || 'svn' @@ -212,10 +279,6 @@ class VCS "r#{rev}" end - def self.short_revision(rev) - rev - end - def _get_revisions(path, srcdir = nil) if srcdir and self.class.local_path?(path) path = File.join(srcdir, path) @@ -350,7 +413,7 @@ class VCS def commit args = %W"#{COMMAND} commit" if dryrun? - VCS::DEBUG_OUT.puts(args.inspect) + VCS.dump(args, "commit: ") return true end system(*args) @@ -358,8 +421,21 @@ class VCS end class GIT < self - register(".git") {|path, dir| File.exist?(File.join(path, dir))} - COMMAND = ENV["GIT"] || 'git' + register(".git") do |path, dir| + SAFE_DIRECTORIES ||= + begin + command = ENV["GIT"] || 'git' + dirs = IO.popen(%W"#{command} config --global --get-all safe.directory", &:read).split("\n") + rescue + command = nil + dirs = [] + ensure + VCS.dump(dirs, "safe.directory: ") if $DEBUG + COMMAND = command + end + + COMMAND and File.exist?(File.join(path, dir)) + end def cmd_args(cmds, srcdir = nil) (opts = cmds.last).kind_of?(Hash) or cmds << (opts = {}) @@ -367,7 +443,7 @@ class VCS if srcdir opts[:chdir] ||= srcdir end - VCS::DEBUG_OUT.puts cmds.inspect if debug? + VCS.dump(cmds, "cmds: ") if debug? and !$DEBUG cmds end @@ -377,7 +453,7 @@ class VCS def cmd_read_at(srcdir, cmds) result = without_gitconfig { IO.pread(*cmd_args(cmds, srcdir)) } - VCS::DEBUG_OUT.puts result.inspect if debug? + VCS.dump(result, "result: ") if debug? result end @@ -398,7 +474,14 @@ class VCS def _get_revisions(path, srcdir = nil) ref = Branch === path ? path.to_str : 'HEAD' gitcmd = [COMMAND] - last = cmd_read_at(srcdir, [[*gitcmd, 'rev-parse', ref]]).rstrip + last = nil + IO.pipe do |r, w| + last = cmd_read_at(srcdir, [[*gitcmd, 'rev-parse', ref, err: w]]).rstrip + w.close + unless r.eof? + raise "#{COMMAND} rev-parse failed\n#{r.read.gsub(/^(?=\s*\S)/, ' ')}" + end + end log = cmd_read_at(srcdir, [[*gitcmd, 'log', '-n1', '--date=iso', '--pretty=fuller', *path]]) changed = log[/\Acommit (\h+)/, 1] modified = log[/^CommitDate:\s+(.*)/, 1] @@ -460,16 +543,35 @@ class VCS end def without_gitconfig - home = ENV.delete('HOME') + envs = (%w'HOME XDG_CONFIG_HOME' + ENV.keys.grep(/\AGIT_/)).each_with_object({}) do |v, h| + h[v] = ENV.delete(v) + end + ENV['GIT_CONFIG_SYSTEM'] = NullDevice + ENV['GIT_CONFIG_GLOBAL'] = global_config yield ensure - ENV['HOME'] = home if home + ENV.update(envs) + end + + def global_config + return NullDevice if SAFE_DIRECTORIES.empty? + unless @gitconfig + @gitconfig = Tempfile.new(%w"vcs_ .gitconfig") + @gitconfig.close + ENV['GIT_CONFIG_GLOBAL'] = @gitconfig.path + SAFE_DIRECTORIES.each do |dir| + system(*%W[#{COMMAND} config --global --add safe.directory #{dir}]) + end + VCS.dump(`#{COMMAND} config --global --get-all safe.directory`, "safe.directory: ") if debug? + end + @gitconfig.path end def initialize(*) super @srcdir = File.realpath(@srcdir) - VCS::DEBUG_OUT.puts @srcdir.inspect if debug? + @gitconfig = nil + VCS.dump(@srcdir, "srcdir: ") if debug? self end @@ -575,11 +677,16 @@ class VCS end end + LOG_FIX_REGEXP_SEPARATORS = '/!:;|,#%&' + def format_changelog(path, arg, base_url = nil) env = {'TZ' => 'JST-9', 'LANG' => 'C', 'LC_ALL' => 'C'} - cmd = %W"#{COMMAND} log --format=fuller --notes=commits --notes=log-fix --topo-order --no-merges" + cmd = %W[#{COMMAND} log + --format=fuller --notes=commits --notes=log-fix --topo-order --no-merges + --fixed-strings --invert-grep --grep=[ci\ skip] --grep=[skip\ ci] + ] date = "--date=iso-local" - unless system(env, *cmd, date, chdir: @srcdir, out: NullDevice, exception: false) + unless system(env, *cmd, date, "-1", chdir: @srcdir, out: NullDevice, exception: false) date = "--date=iso" end cmd << date @@ -590,19 +697,51 @@ class VCS cmd_pipe(env, cmd, chdir: @srcdir) do |r| while s = r.gets("\ncommit ") h, s = s.split(/^$/, 2) + + next if /^Author: *dependabot\[bot\]/ =~ h + h.gsub!(/^(?:(?:Author|Commit)(?:Date)?|Date): /, ' \&') if s.sub!(/\nNotes \(log-fix\):\n((?: +.*\n)+)/, '') fix = $1 s = s.lines fix.each_line do |x| + next unless x.sub!(/^(\s+)(?:(\d+)|\$(?:-\d+)?)/, '') + b = ($2&.to_i || (s.size - 1 + $3.to_i)) + sp = $1 + if x.sub!(/^,(?:(\d+)|\$(?:-\d+)?)/, '') + range = b..($1&.to_i || (s.size - 1 + $2.to_i)) + else + range = b..b + end case x - when %r[^ +(\d+)s/(.+)/(.*)/] - begin - s[$1.to_i][$2] = $3 - rescue IndexError - message = ["format_changelog failed to replace #{$2.dump} with #{$3.dump} at #$1\n"] - from = [1, $1.to_i-2].max - to = [s.size-1, $1.to_i+2].min + when %r[^s([#{LOG_FIX_REGEXP_SEPARATORS}])(.+)\1(.*)\1([gr]+)?]o + wrong = $2 + correct = $3 + if opt = $4 and opt.include?("r") # regexp + wrong = Regexp.new(wrong) + correct.gsub!(/(?<!\\)(?:\\\\)*\K(?:\\n)+/) {"\n" * ($&.size / 2)} + sub = opt.include?("g") ? :gsub! : :sub! + else + sub = false + end + range.each do |n| + if sub + ss = s[n].sub(/^#{sp}/, "") # un-indent for /^/ + if ss.__send__(sub, wrong, correct) + s[n, 1] = ss.lines.map {|l| "#{sp}#{l}"} + next + end + else + begin + s[n][wrong] = correct + rescue IndexError + else + next + end + end + message = ["format_changelog failed to replace #{wrong.dump} with #{correct.dump} at #{n}\n"] + from = [1, n-2].max + to = [s.size-1, n+2].min s.each_with_index do |e, i| next if i < from break if to < i @@ -610,16 +749,23 @@ class VCS end raise message.join('') end - when %r[^( +)(\d+)i/(.*)/] - s[$2.to_i, 0] = "#{$1}#{$3}\n" - when %r[^ +(\d+)(?:,(\d+))?d] - n = $1.to_i - e = $2 - s[n..(e ? e.to_i : n)] = [] + when %r[^i([#{LOG_FIX_REGEXP_SEPARATORS}])(.*)\1]o + insert = "#{sp}#{$2}\n" + range.reverse_each do |n| + s[n, 0] = insert + end + when %r[^d] + s[range] = [] end end s = s.join('') end + + if %r[^ +(https://github\.com/[^/]+/[^/]+/)commit/\h+\n(?=(?: +\n(?i: +Co-authored-by: .*\n)+)?(?:\n|\Z))] =~ s + issue = "#{$1}pull/" + s.gsub!(/\b(?:(?i:fix(?:e[sd])?) +|GH-)\K#(?=\d+\b)|\(\K#(?=\d+\))/) {issue} + end + s.gsub!(/ +\n/, "\n") s.sub!(/^Notes:/, ' \&') w.print h, s @@ -663,13 +809,13 @@ class VCS def commit(opts = {}) args = [COMMAND, "push"] - args << "-n" if dryrun + args << "-n" if dryrun? remote, branch = upstream args << remote branches = %W[refs/notes/commits:refs/notes/commits HEAD:#{branch}] if dryrun? branches.each do |b| - VCS::DEBUG_OUT.puts((args + [b]).inspect) + VCS.dump(args + [b], "commit: ") end return true end @@ -699,7 +845,7 @@ class VCS commits.each_with_index do |l, i| r, a, c = l.split(' ') dcommit = [COMMAND, "svn", "dcommit"] - dcommit.insert(-2, "-n") if dryrun + dcommit.insert(-2, "-n") if dryrun? dcommit << "--add-author-from" unless a == c dcommit << r system(*dcommit) or return false @@ -719,4 +865,15 @@ class VCS true end end + + class Null < self + def get_revisions(path, srcdir = nil) + @modified ||= Time.now - 10 + return nil, nil, @modified + end + + def revision_header(last, release_date, release_datetime = nil, branch = nil, title = nil, limit: 20) + self.release_date(release_date) + end + end end diff --git a/tool/lib/vpath.rb b/tool/lib/vpath.rb index 48ab148405..fa819f3242 100644 --- a/tool/lib/vpath.rb +++ b/tool/lib/vpath.rb @@ -53,10 +53,11 @@ class VPath end def def_options(opt) + opt.separator(" VPath common options:") opt.on("-I", "--srcdir=DIR", "add a directory to search path") {|dir| @additional << dir } - opt.on("-L", "--vpath=PATH LIST", "add directories to search path") {|dirs| + opt.on("-L", "--vpath=PATH-LIST", "add directories to search path") {|dirs| @additional << [dirs] } opt.on("--path-separator=SEP", /\A(?:\W\z|\.(\W).+)/, "separator for vpath") {|sep, vsep| @@ -80,6 +81,10 @@ class VPath @list end + def add(path) + @additional << path + end + def strip(path) prefix = list.map {|dir| Regexp.quote(dir)} path.sub(/\A#{prefix.join('|')}(?:\/|\z)/, '') diff --git a/tool/lib/webrick/httprequest.rb b/tool/lib/webrick/httprequest.rb index d34eac7ecf..258ee37a38 100644 --- a/tool/lib/webrick/httprequest.rb +++ b/tool/lib/webrick/httprequest.rb @@ -402,7 +402,7 @@ module WEBrick # This method provides the metavariables defined by the revision 3 # of "The WWW Common Gateway Interface Version 1.1" # To browse the current document of CGI Version 1.1, see below: - # http://tools.ietf.org/html/rfc3875 + # https://www.rfc-editor.org/rfc/rfc3875 def meta_vars meta = Hash.new diff --git a/tool/lib/webrick/httpserver.rb b/tool/lib/webrick/httpserver.rb index e85d059319..f3f948da3b 100644 --- a/tool/lib/webrick/httpserver.rb +++ b/tool/lib/webrick/httpserver.rb @@ -9,7 +9,6 @@ # # $IPR: httpserver.rb,v 1.63 2002/10/01 17:16:32 gotoyuzo Exp $ -require 'io/wait' require_relative 'server' require_relative 'httputils' require_relative 'httpstatus' diff --git a/tool/lib/webrick/httputils.rb b/tool/lib/webrick/httputils.rb index f1b9ddf9f0..e21284ee7f 100644 --- a/tool/lib/webrick/httputils.rb +++ b/tool/lib/webrick/httputils.rb @@ -112,7 +112,7 @@ module WEBrick def load_mime_types(file) # note: +file+ may be a "| command" for now; some people may # rely on this, but currently we do not use this method by default. - open(file){ |io| + File.open(file){ |io| hash = Hash.new io.each{ |line| next if /^#/ =~ line |