diff --git a/Rakefile b/Rakefile
index 5d512c8..1c6c202 100644
--- a/Rakefile
+++ b/Rakefile
@@ -2,7 +2,7 @@ require "bundler/gem_tasks"
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
- t.libs << "test/lib"
+ t.libs << "tool/lib"
t.ruby_opts << "-rhelper"
t.test_files = FileList["test/**/test_*.rb"]
end
diff --git a/tool/lib/colorize.rb b/tool/lib/colorize.rb
new file mode 100644
index 0000000..7494a21
--- /dev/null
+++ b/tool/lib/colorize.rb
@@ -0,0 +1,51 @@
+# frozen-string-literal: true
+
+class Colorize
+ def initialize(color = nil, opts = ((_, color = color, nil)[0] if Hash === color))
+ @colors = @reset = nil
+ if color or (color == nil && STDOUT.tty?)
+ if (/\A\e\[.*m\z/ =~ IO.popen("tput smso", "r", :err => IO::NULL, &:read) rescue nil)
+ @beg = "\e["
+ colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {}
+ if opts and colors_file = opts[:colors_file]
+ begin
+ File.read(colors_file).scan(/(\w+)=([^:\n]*)/) do |n, c|
+ colors[n] ||= c
+ end
+ rescue Errno::ENOENT
+ end
+ end
+ @colors = colors
+ @reset = "#{@beg}m"
+ end
+ end
+ self
+ end
+
+ DEFAULTS = {
+ "pass"=>"32", "fail"=>"31;1", "skip"=>"33;1",
+ "black"=>"30", "red"=>"31", "green"=>"32", "yellow"=>"33",
+ "blue"=>"34", "magenta"=>"35", "cyan"=>"36", "white"=>"37",
+ "bold"=>"1", "underline"=>"4", "reverse"=>"7",
+ }
+
+ def decorate(str, name)
+ if @colors and color = (@colors[name] || DEFAULTS[name])
+ "#{@beg}#{color}m#{str}#{@reset}"
+ else
+ str
+ end
+ end
+
+ DEFAULTS.each_key do |name|
+ define_method(name) {|str|
+ decorate(str, name)
+ }
+ end
+end
+
+if $0 == __FILE__
+ colorize = Colorize.new
+ col = ARGV.shift
+ ARGV.each {|str| puts colorize.decorate(str, col)}
+end
diff --git a/test/lib/core_assertions.rb b/tool/lib/core_assertions.rb
similarity index 100%
rename from test/lib/core_assertions.rb
rename to tool/lib/core_assertions.rb
diff --git a/test/lib/envutil.rb b/tool/lib/envutil.rb
similarity index 93%
rename from test/lib/envutil.rb
rename to tool/lib/envutil.rb
index 2faf483..0bd8bdb 100644
--- a/test/lib/envutil.rb
+++ b/tool/lib/envutil.rb
@@ -86,7 +86,20 @@ def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1)
when nil, false
pgroup = pid
end
+
+ lldb = true if /darwin/ =~ RUBY_PLATFORM
+
while signal = signals.shift
+
+ if lldb and [:ABRT, :KILL].include?(signal)
+ lldb = false
+ # sudo -n: --non-interactive
+ # lldb -p: attach
+ # -o: run command
+ system(*%W[sudo -n lldb -p #{pid} --batch -o bt\ all -o call\ rb_vmdebug_stack_dump_all_threads() -o quit])
+ true
+ end
+
begin
Process.kill signal, pgroup
rescue Errno::EINVAL
@@ -100,6 +113,8 @@ def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1)
begin
Timeout.timeout(reprieve) {Process.wait(pid)}
rescue Timeout::Error
+ else
+ break
end
end
end
@@ -137,8 +152,10 @@ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr =
args = [args] if args.kind_of?(String)
pid = spawn(child_env, *precommand, rubybin, *args, **opt)
in_c.close
- out_c.close if capture_stdout
- err_c.close if capture_stderr && capture_stderr != :merge_to_stdout
+ out_c&.close
+ out_c = nil
+ err_c&.close
+ err_c = nil
if block_given?
return yield in_p, out_p, err_p, pid
else
@@ -242,7 +259,11 @@ def with_default_internal(enc)
def labeled_module(name, &block)
Module.new do
- singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s}
+ singleton_class.class_eval {
+ define_method(:to_s) {name}
+ alias inspect to_s
+ alias name to_s
+ }
class_eval(&block) if block
end
end
@@ -250,7 +271,11 @@ def labeled_module(name, &block)
def labeled_class(name, superclass = Object, &block)
Class.new(superclass) do
- singleton_class.class_eval {define_method(:to_s) {name}; alias inspect to_s}
+ singleton_class.class_eval {
+ define_method(:to_s) {name}
+ alias inspect to_s
+ alias name to_s
+ }
class_eval(&block) if block
end
end
diff --git a/test/lib/find_executable.rb b/tool/lib/find_executable.rb
similarity index 100%
rename from test/lib/find_executable.rb
rename to tool/lib/find_executable.rb
diff --git a/test/lib/helper.rb b/tool/lib/helper.rb
similarity index 63%
rename from test/lib/helper.rb
rename to tool/lib/helper.rb
index 909f8f9..9b9bc6a 100644
--- a/test/lib/helper.rb
+++ b/tool/lib/helper.rb
@@ -1,4 +1,4 @@
require "test/unit"
-require_relative "core_assertions"
+require_relative "test/unit/core_assertions"
Test::Unit::TestCase.include Test::Unit::CoreAssertions
diff --git a/tool/lib/leakchecker.rb b/tool/lib/leakchecker.rb
new file mode 100644
index 0000000..e7967e4
--- /dev/null
+++ b/tool/lib/leakchecker.rb
@@ -0,0 +1,302 @@
+# frozen_string_literal: true
+class LeakChecker
+ @@try_lsof = nil # not-tried-yet
+
+ def initialize
+ @fd_info = find_fds
+ @@skip = false
+ @tempfile_info = find_tempfiles
+ @thread_info = find_threads
+ @env_info = find_env
+ @encoding_info = find_encodings
+ @old_verbose = $VERBOSE
+ @old_warning_flags = find_warning_flags
+ end
+
+ def check(test_name)
+ if /i386-solaris/ =~ RUBY_PLATFORM && /TestGem/ =~ test_name
+ GC.verify_internal_consistency
+ end
+
+ leaks = [
+ check_fd_leak(test_name),
+ check_thread_leak(test_name),
+ check_tempfile_leak(test_name),
+ check_env(test_name),
+ check_encodings(test_name),
+ check_verbose(test_name),
+ check_warning_flags(test_name),
+ ]
+ GC.start if leaks.any?
+ end
+
+ def check_verbose test_name
+ puts "#{test_name}: $VERBOSE == #{$VERBOSE}" unless @old_verbose == $VERBOSE
+ end
+
+ def find_fds
+ if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
+ m[:close]
+ end
+ %w"/proc/self/fd /dev/fd".each do |fd_dir|
+ if File.directory?(fd_dir)
+ fds = Dir.open(fd_dir) {|d|
+ a = d.grep(/\A\d+\z/, &:to_i)
+ if d.respond_to? :fileno
+ a -= [d.fileno]
+ end
+ a
+ }
+ return fds.sort
+ end
+ end
+ []
+ end
+
+ def check_fd_leak(test_name)
+ leaked = false
+ live1 = @fd_info
+ live2 = find_fds
+ fd_closed = live1 - live2
+ if !fd_closed.empty?
+ fd_closed.each {|fd|
+ puts "Closed file descriptor: #{test_name}: #{fd}"
+ }
+ end
+ fd_leaked = live2 - live1
+ if !@@skip && !fd_leaked.empty?
+ leaked = true
+ h = {}
+ ObjectSpace.each_object(IO) {|io|
+ inspect = io.inspect
+ begin
+ autoclose = io.autoclose?
+ fd = io.fileno
+ rescue IOError # closed IO object
+ next
+ end
+ (h[fd] ||= []) << [io, autoclose, inspect]
+ }
+ fd_leaked.select! {|fd|
+ str = ''.dup
+ pos = nil
+ if h[fd]
+ str << ' :'
+ h[fd].map {|io, autoclose, inspect|
+ if ENV["LEAK_CHECKER_TRACE_OBJECT_ALLOCATION"]
+ pos = "#{ObjectSpace.allocation_sourcefile(io)}:#{ObjectSpace.allocation_sourceline(io)}"
+ end
+ s = ' ' + inspect
+ s << "(not-autoclose)" if !autoclose
+ s
+ }.sort.each {|s|
+ str << s
+ }
+ else
+ begin
+ io = IO.for_fd(fd, autoclose: false)
+ s = io.stat
+ rescue Errno::EBADF
+ # something un-stat-able
+ next
+ else
+ next if /darwin/ =~ RUBY_PLATFORM and [0, -1].include?(s.dev)
+ str << ' ' << s.inspect
+ ensure
+ io&.close
+ end
+ end
+ puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
+ puts " The IO was created at #{pos}" if pos
+ true
+ }
+ unless fd_leaked.empty?
+ unless @@try_lsof == false
+ @@try_lsof |= system("lsof -p #$$", out: MiniTest::Unit.output)
+ end
+ end
+ h.each {|fd, list|
+ next if list.length <= 1
+ if 1 < list.count {|io, autoclose, inspect| autoclose }
+ str = list.map {|io, autoclose, inspect| " #{inspect}" + (autoclose ? "(autoclose)" : "") }.sort.join
+ puts "Multiple autoclose IO objects for a file descriptor in: #{test_name}: #{str}"
+ end
+ }
+ end
+ @fd_info = live2
+ @@skip = false
+ return leaked
+ end
+
+ def extend_tempfile_counter
+ return if defined? LeakChecker::TempfileCounter
+ m = Module.new {
+ @count = 0
+ class << self
+ attr_accessor :count
+ end
+
+ def new(data)
+ LeakChecker::TempfileCounter.count += 1
+ super(data)
+ end
+ }
+ LeakChecker.const_set(:TempfileCounter, m)
+
+ class << Tempfile::Remover
+ prepend LeakChecker::TempfileCounter
+ end
+ end
+
+ def find_tempfiles(prev_count=-1)
+ return [prev_count, []] unless defined? Tempfile
+ extend_tempfile_counter
+ count = TempfileCounter.count
+ if prev_count == count
+ [prev_count, []]
+ else
+ tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t|
+ t.instance_variable_defined?(:@tmpfile) and t.path
+ }
+ [count, tempfiles]
+ end
+ end
+
+ def check_tempfile_leak(test_name)
+ return false unless defined? Tempfile
+ count1, initial_tempfiles = @tempfile_info
+ count2, current_tempfiles = find_tempfiles(count1)
+ leaked = false
+ tempfiles_leaked = current_tempfiles - initial_tempfiles
+ if !tempfiles_leaked.empty?
+ leaked = true
+ list = tempfiles_leaked.map {|t| t.inspect }.sort
+ list.each {|str|
+ puts "Leaked tempfile: #{test_name}: #{str}"
+ }
+ tempfiles_leaked.each {|t| t.close! }
+ end
+ @tempfile_info = [count2, initial_tempfiles]
+ return leaked
+ end
+
+ def find_threads
+ Thread.list.find_all {|t|
+ t != Thread.current && t.alive?
+ }
+ end
+
+ def check_thread_leak(test_name)
+ live1 = @thread_info
+ live2 = find_threads
+ thread_finished = live1 - live2
+ leaked = false
+ if !thread_finished.empty?
+ list = thread_finished.map {|t| t.inspect }.sort
+ list.each {|str|
+ puts "Finished thread: #{test_name}: #{str}"
+ }
+ end
+ thread_leaked = live2 - live1
+ if !thread_leaked.empty?
+ leaked = true
+ list = thread_leaked.map {|t| t.inspect }.sort
+ list.each {|str|
+ puts "Leaked thread: #{test_name}: #{str}"
+ }
+ end
+ @thread_info = live2
+ return leaked
+ end
+
+ def find_env
+ ENV.to_h
+ end
+
+ def check_env(test_name)
+ old_env = @env_info
+ new_env = ENV.to_h
+ return false if old_env == new_env
+ (old_env.keys | new_env.keys).sort.each {|k|
+ if old_env.has_key?(k)
+ if new_env.has_key?(k)
+ if old_env[k] != new_env[k]
+ puts "Environment variable changed: #{test_name} : #{k.inspect} changed : #{old_env[k].inspect} -> #{new_env[k].inspect}"
+ end
+ else
+ puts "Environment variable changed: #{test_name} : #{k.inspect} deleted"
+ end
+ else
+ if new_env.has_key?(k)
+ puts "Environment variable changed: #{test_name} : #{k.inspect} added"
+ else
+ flunk "unreachable"
+ end
+ end
+ }
+ @env_info = new_env
+ return true
+ end
+
+ def find_encodings
+ {
+ 'Encoding.default_internal' => Encoding.default_internal,
+ 'Encoding.default_external' => Encoding.default_external,
+ 'STDIN.internal_encoding' => STDIN.internal_encoding,
+ 'STDIN.external_encoding' => STDIN.external_encoding,
+ 'STDOUT.internal_encoding' => STDOUT.internal_encoding,
+ 'STDOUT.external_encoding' => STDOUT.external_encoding,
+ 'STDERR.internal_encoding' => STDERR.internal_encoding,
+ 'STDERR.external_encoding' => STDERR.external_encoding,
+ }
+ end
+
+ def check_encodings(test_name)
+ old_encoding_info = @encoding_info
+ @encoding_info = find_encodings
+ leaked = false
+ @encoding_info.each do |key, new_encoding|
+ old_encoding = old_encoding_info[key]
+ if new_encoding != old_encoding
+ leaked = true
+ puts "#{key} changed: #{test_name} : #{old_encoding.inspect} to #{new_encoding.inspect}"
+ end
+ end
+ leaked
+ end
+
+ WARNING_CATEGORIES = %i[deprecated experimental].freeze
+
+ def find_warning_flags
+ return unless Warning.respond_to?(:[])
+ WARNING_CATEGORIES.to_h do |category|
+ [category, Warning[category]]
+ end
+ end
+
+ def check_warning_flags(test_name)
+ unless new_warning_flags = find_warning_flags
+ return false
+ end
+ leaked = false
+ WARNING_CATEGORIES.each do |category|
+ if new_warning_flags[category] != @old_warning_flags[category]
+ leaked = true
+ puts "Warning[#{category.inspect}] changed: #{test_name} : #{@old_warning_flags[category]} to #{new_warning_flags[category]}"
+ end
+ end
+ return leaked
+ end
+
+ def puts(*a)
+ output = MiniTest::Unit.output
+ if defined?(output.set_encoding)
+ output.set_encoding(nil, nil)
+ end
+ output.puts(*a)
+ end
+
+ def self.skip
+ @@skip = true
+ end
+end
diff --git a/tool/lib/minitest/unit.rb b/tool/lib/minitest/unit.rb
new file mode 100644
index 0000000..c325134
--- /dev/null
+++ b/tool/lib/minitest/unit.rb
@@ -0,0 +1,1474 @@
+# 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(/(?exp == act 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+ =~ +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
+
+ def refute_path_exists(path, msg = nil)
+ msg = message(msg) { "Expected path '#{path}' to not exist" }
+ refute File.exist?(path), msg
+ end
+
+ ##
+ # 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 exp == act.
+ #
+ # 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+ =~ +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
+
+ continuation = proc do
+ 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 = inst.run self
+
+ print "%.2f s = " % (Time.now - start_time) if @verbose
+ print result
+ puts if @verbose
+ $stdout.flush
+
+ unless defined?(RubyVM::MJIT) && RubyVM::MJIT.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
+
+ if ENV["LEAK_CHECKER_TRACE_OBJECT_ALLOCATION"]
+ require "objspace"
+ ObjectSpace.trace_object_allocations(&continuation)
+ else
+ continuation.call
+ end
+ 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 '' 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/test/unit.rb b/tool/lib/test/unit.rb
new file mode 100644
index 0000000..0cba377
--- /dev/null
+++ b/tool/lib/test/unit.rb
@@ -0,0 +1,1238 @@
+# frozen_string_literal: true
+begin
+ gem 'minitest', '< 5.0.0' if defined? Gem
+rescue Gem::LoadError
+end
+require 'minitest/unit'
+require 'test/unit/assertions'
+require_relative '../envutil'
+require_relative '../colorize'
+require '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:
+
+ module RunCount # :nodoc: all
+ @@run_count = 0
+
+ def self.have_run?
+ @@run_count.nonzero?
+ end
+
+ def run(*)
+ @@run_count += 1
+ super
+ end
+
+ def run_once
+ return if have_run?
+ return if $! # don't run if there was an exception
+ yield
+ end
+ module_function :run_once
+ end
+
+ module Options # :nodoc: all
+ def initialize(*, &block)
+ @init_hook = block
+ @options = nil
+ super(&nil)
+ end
+
+ def option_parser
+ @option_parser ||= OptionParser.new
+ end
+
+ def process_args(args = [])
+ return @options if @options
+ orig_args = args.dup
+ options = {}
+ opts = option_parser
+ setup_options(opts, options)
+ opts.parse!(args)
+ orig_args -= args
+ args = @init_hook.call(args, options) if @init_hook
+ non_options(args, options)
+ @run_options = orig_args
+
+ if seed = options[:seed]
+ srand(seed)
+ else
+ seed = options[:seed] = srand % 100_000
+ srand(seed)
+ orig_args.unshift "--seed=#{seed}"
+ end
+
+ @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.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
+ self.verbose = options[:verbose]
+ end
+
+ opts.on '-n', '--name PATTERN', "Filter test method names on pattern: /REGEXP/, !/REGEXP/ or STRING" do |a|
+ (options[:filter] ||= []) << a
+ end
+
+ opts.on '--test-order=random|alpha|sorted|nosort', [:random, :alpha, :sorted, :nosort] do |a|
+ MiniTest::Unit::TestCase.test_order = a
+ end
+ end
+
+ def non_options(files, options)
+ filter = options[:filter]
+ if filter
+ pos_pat = /\A\/(.*)\/\z/
+ neg_pat = /\A!\/(.*)\/\z/
+ negative, positive = filter.partition {|s| neg_pat =~ s}
+ if positive.empty?
+ filter = nil
+ elsif negative.empty? and positive.size == 1 and pos_pat !~ positive[0]
+ filter = positive[0]
+ else
+ filter = Regexp.union(*positive.map! {|s| Regexp.new(s[pos_pat, 1] || "\\A#{Regexp.quote(s)}\\z")})
+ end
+ unless negative.empty?
+ negative = Regexp.union(*negative.map! {|s| Regexp.new(s[neg_pat, 1])})
+ filter = /\A(?=.*#{filter})(?!.*#{negative})/
+ end
+ if Regexp === filter
+ # bypass conversion in minitest
+ def filter.=~(other) # :nodoc:
+ super unless Regexp === other
+ end
+ end
+ options[:filter] = filter
+ end
+ true
+ end
+ end
+
+ module Parallel # :nodoc: all
+ def process_args(args = [])
+ return @options if @options
+ options = super
+ if @options[:parallel]
+ @files = args
+ end
+ options
+ end
+
+ def non_options(files, options)
+ @jobserver = nil
+ makeflags = ENV.delete("MAKEFLAGS")
+ if !options[:parallel] and
+ /(?:\A|\s)--jobserver-(?:auth|fds)=(\d+),(\d+)/ =~ makeflags
+ begin
+ r = IO.for_fd($1.to_i(10), "rb", autoclose: false)
+ w = IO.for_fd($2.to_i(10), "wb", autoclose: false)
+ rescue
+ r.close if r
+ nil
+ else
+ r.close_on_exec = true
+ w.close_on_exec = true
+ @jobserver = [r, w]
+ options[:parallel] ||= 1
+ end
+ end
+ super
+ end
+
+ def status(*args)
+ result = super
+ raise @interrupt if @interrupt
+ result
+ end
+
+ private
+ def setup_options(opts, options)
+ super
+
+ opts.separator "parallel test options:"
+
+ options[:retry] = true
+
+ opts.on '-j N', '--jobs N', /\A(t)?(\d+)\z/, "Allow run tests with N jobs at once" do |_, t, a|
+ options[:testing] = true & t # For testing
+ options[:parallel] = a.to_i
+ end
+
+ opts.on '--separate', "Restart job process after one testcase has done" do
+ options[:parallel] ||= 1
+ options[:separate] = true
+ end
+
+ opts.on '--retry', "Retry running testcase when --jobs specified" do
+ options[:retry] = true
+ end
+
+ opts.on '--no-retry', "Disable --retry" do
+ options[:retry] = false
+ end
+
+ opts.on '--ruby VAL', "Path to ruby which is used at -j option" do |a|
+ options[:ruby] = a.split(/ /).reject(&:empty?)
+ end
+ end
+
+ class Worker
+ def self.launch(ruby,args=[])
+ scale = EnvUtil.timeout_scale
+ io = IO.popen([*ruby, "-W1",
+ "#{File.dirname(__FILE__)}/unit/parallel.rb",
+ *("--timeout-scale=#{scale}" if scale),
+ *args], "rb+")
+ new(io, io.pid, :waiting)
+ end
+
+ attr_reader :quit_called
+
+ def initialize(io, pid, status)
+ @io = io
+ @pid = pid
+ @status = status
+ @file = nil
+ @real_file = nil
+ @loadpath = []
+ @hooks = {}
+ @quit_called = false
+ end
+
+ def puts(*args)
+ @io.puts(*args)
+ end
+
+ def run(task,type)
+ @file = File.basename(task, ".rb")
+ @real_file = task
+ begin
+ puts "loadpath #{[Marshal.dump($:-@loadpath)].pack("m0")}"
+ @loadpath = $:.dup
+ puts "run #{task} #{type}"
+ @status = :prepare
+ rescue Errno::EPIPE
+ died
+ rescue IOError
+ raise unless /stream closed|closed stream/ =~ $!.message
+ died
+ end
+ end
+
+ def hook(id,&block)
+ @hooks[id] ||= []
+ @hooks[id] << block
+ self
+ end
+
+ def read
+ res = (@status == :quit) ? @io.read : @io.gets
+ res && res.chomp
+ end
+
+ def close
+ @io.close unless @io.closed?
+ self
+ rescue IOError
+ end
+
+ def quit
+ return if @io.closed?
+ @quit_called = true
+ @io.puts "quit"
+ rescue Errno::EPIPE => e
+ warn "#{@pid}:#{@status.to_s.ljust(7)}:#{@file}: #{e.message}"
+ end
+
+ def kill
+ Process.kill(:KILL, @pid)
+ rescue Errno::ESRCH
+ end
+
+ def died(*additional)
+ @status = :quit
+ @io.close
+ status = $?
+ if status and status.signaled?
+ additional[0] ||= SignalException.new(status.termsig)
+ end
+
+ call_hook(:dead,*additional)
+ end
+
+ def to_s
+ if @file and @status != :ready
+ "#{@pid}=#{@file}"
+ else
+ "#{@pid}:#{@status.to_s.ljust(7)}"
+ end
+ end
+
+ attr_reader :io, :pid
+ attr_accessor :status, :file, :real_file, :loadpath
+
+ private
+
+ def call_hook(id,*additional)
+ @hooks[id] ||= []
+ @hooks[id].each{|hook| hook[self,additional] }
+ self
+ end
+
+ end
+
+ def flush_job_tokens
+ if @jobserver
+ r, w = @jobserver.shift(2)
+ @jobserver = nil
+ w << @job_tokens.slice!(0..-1)
+ r.close
+ w.close
+ end
+ end
+
+ def after_worker_down(worker, e=nil, c=false)
+ return unless @options[:parallel]
+ return if @interrupt
+ flush_job_tokens
+ warn e if e
+ 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 "option."
+ warn ""
+ STDERR.flush
+ exit c
+ end
+
+ def after_worker_quit(worker)
+ return unless @options[:parallel]
+ return if @interrupt
+ worker.close
+ if @jobserver and (token = @job_tokens.slice!(0))
+ @jobserver[1] << token
+ end
+ @workers.delete(worker)
+ @dead_workers << worker
+ @ios = @workers.map(&:io)
+ end
+
+ def launch_worker
+ begin
+ worker = Worker.launch(@options[:ruby], @run_options)
+ rescue => e
+ abort "ERROR: Failed to launch job process - #{e.class}: #{e.message}"
+ end
+ worker.hook(:dead) do |w,info|
+ after_worker_quit w
+ after_worker_down w, *info if !info.empty? && !worker.quit_called
+ end
+ @workers << worker
+ @ios << worker.io
+ @workers_hash[worker.io] = worker
+ worker
+ end
+
+ def delete_worker(worker)
+ @workers_hash.delete worker.io
+ @workers.delete worker
+ @ios.delete worker.io
+ end
+
+ def quit_workers
+ return if @workers.empty?
+ @workers.reject! do |worker|
+ begin
+ Timeout.timeout(1) do
+ worker.quit
+ end
+ rescue Errno::EPIPE
+ rescue Timeout::Error
+ end
+ worker.close
+ end
+
+ return if @workers.empty?
+ begin
+ Timeout.timeout(0.2 * @workers.size) do
+ Process.waitall
+ end
+ rescue Timeout::Error
+ @workers.each do |worker|
+ worker.kill
+ end
+ @worker.clear
+ end
+ end
+
+ FakeClass = Struct.new(:name)
+ def fake_class(name)
+ (@fake_classes ||= {})[name] ||= FakeClass.new(name)
+ end
+
+ def deal(io, type, result, rep, shutting_down = false)
+ worker = @workers_hash[io]
+ cmd = worker.read
+ cmd.sub!(/\A\.+/, '') if cmd # read may return nil
+ case cmd
+ when ''
+ # just only dots, ignore
+ when /^okay$/
+ worker.status = :running
+ when /^ready(!)?$/
+ bang = $1
+ worker.status = :ready
+
+ unless task = @tasks.shift
+ worker.quit
+ return nil
+ end
+ if @options[:separate] and not bang
+ worker.quit
+ worker = launch_worker
+ end
+ worker.run(task, type)
+ @test_count += 1
+
+ jobs_status(worker)
+ when /^done (.+?)$/
+ begin
+ r = Marshal.load($1.unpack("m")[0])
+ rescue
+ print "unknown object: #{$1.unpack("m")[0].dump}"
+ return true
+ end
+ result << r[0..1] unless r[0..1] == [nil,nil]
+ rep << {file: worker.real_file, report: r[2], result: r[3], testcase: r[5]}
+ $:.push(*r[4]).uniq!
+ jobs_status(worker) if @options[:job_status] == :replace
+ return true
+ when /^record (.+?)$/
+ begin
+ r = Marshal.load($1.unpack("m")[0])
+ rescue => e
+ print "unknown record: #{e.message} #{$1.unpack("m")[0].dump}"
+ return true
+ end
+ record(fake_class(r[0]), *r[1..-1])
+ when /^p (.+?)$/
+ del_jobs_status
+ print $1.unpack("m")[0]
+ jobs_status(worker) if @options[:job_status] == :replace
+ when /^after (.+?)$/
+ @warnings << Marshal.load($1.unpack("m")[0])
+ when /^bye (.+?)$/
+ after_worker_down worker, Marshal.load($1.unpack("m")[0])
+ when /^bye$/, nil
+ if shutting_down || worker.quit_called
+ after_worker_quit worker
+ else
+ after_worker_down worker
+ end
+ else
+ print "unknown command: #{cmd.dump}\n"
+ end
+ return false
+ end
+
+ def _run_parallel suites, type, result
+ if @options[:parallel] < 1
+ warn "Error: parameter of -j option should be greater than 0."
+ return
+ end
+
+ # 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
+ # sorted
+ end
+
+ @need_quit = false
+ @dead_workers = [] # Array of dead workers.
+ @warnings = []
+ @total_tests = @tasks.size.to_s(10)
+ rep = [] # FIXME: more good naming
+
+ @workers = [] # Array of workers.
+ @workers_hash = {} # out-IO => worker
+ @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 _io = IO.select(@ios)[0]
+ break if _io.any? do |io|
+ @need_quit or
+ (deal(io, type, result, rep).nil? and
+ !@workers.any? {|x| [:running, :prepare].include? x.status})
+ 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
+ end
+ end
+ rescue Interrupt => ex
+ @interrupt = ex
+ return result
+ ensure
+ if @interrupt
+ @ios.select!{|x| @workers_hash[x].status == :running }
+ while !@ios.empty? && (__io = IO.select(@ios,[],[],10))
+ __io[0].reject! {|io| deal(io, type, result, rep, true)}
+ end
+ end
+
+ quit_workers
+ flush_job_tokens
+
+ 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.map {|r| File.realpath(r[:file])}.uniq.each {|file| require file}
+ suites.map! {|r| eval("::"+r[:testcase])}
+ del_status_line or puts
+ unless suites.empty?
+ puts "\n""Retrying..."
+ _run_suites(suites, type)
+ end
+ @options[:parallel] = parallel
+ end
+ unless @options[:retry]
+ del_status_line or puts
+ end
+ unless rep.empty?
+ rep.each do |r|
+ 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(:+)
+ end
+ end
+ unless @warnings.empty?
+ warn ""
+ @warnings.uniq! {|w| w[1].message}
+ @warnings.each do |w|
+ warn "#{w[0]}: #{w[1].message} (#{w[1].class})"
+ end
+ warn ""
+ end
+ end
+ end
+
+ def _run_suites suites, type
+ _prepare_run(suites, type)
+ @interrupt = nil
+ result = []
+ GC.start
+ if @options[:parallel]
+ _run_parallel suites, type, result
+ else
+ suites.each {|suite|
+ begin
+ result << _run_suite(suite, type)
+ rescue Interrupt => e
+ @interrupt = e
+ break
+ end
+ }
+ end
+ del_status_line
+ result
+ end
+ end
+
+ module Skipping # :nodoc: all
+ def failed(s)
+ super if !s or @options[:hide_skip]
+ end
+
+ private
+ def setup_options(opts, options)
+ super
+
+ opts.separator "skipping options:"
+
+ options[:hide_skip] = true
+
+ opts.on '-q', '--hide-skip', 'Hide skipped tests' do
+ options[:hide_skip] = true
+ end
+
+ opts.on '--show-skip', 'Show skipped tests' do
+ options[:hide_skip] = false
+ end
+ end
+
+ def _run_suites(suites, type)
+ result = super
+ report.reject!{|r| r.start_with? "Skipped:" } if @options[:hide_skip]
+ report.sort_by!{|r| r.start_with?("Skipped:") ? 0 : \
+ (r.start_with?("Failure:") ? 1 : 2) }
+ failed(nil)
+ result
+ end
+ end
+
+ module Statistics
+ def update_list(list, rec, max)
+ if i = list.empty? ? 0 : list.bsearch_index {|*a| yield(*a)}
+ list[i, 0] = [rec]
+ list[max..-1] = [] if list.size >= max
+ end
+ end
+
+ def record(suite, method, assertions, time, error)
+ if @options.values_at(:longest, :most_asserted).any?
+ @tops ||= {}
+ rec = [suite.name, method, assertions, time, error]
+ if max = @options[:longest]
+ update_list(@tops[:longest] ||= [], rec, max) {|_,_,_,t,_|t