summaryrefslogtreecommitdiff
path: root/tool/lib
diff options
context:
space:
mode:
Diffstat (limited to 'tool/lib')
-rw-r--r--tool/lib/-test-/integer.rb4
-rw-r--r--tool/lib/_tmpdir.rb100
-rw-r--r--tool/lib/bundle_env.rb4
-rw-r--r--tool/lib/bundled_gem.rb126
-rw-r--r--tool/lib/colorize.rb48
-rw-r--r--tool/lib/core_assertions.rb1033
-rw-r--r--tool/lib/dump.gdb17
-rw-r--r--tool/lib/dump.lldb13
-rw-r--r--tool/lib/envutil.rb254
-rw-r--r--tool/lib/gc_checker.rb36
-rw-r--r--tool/lib/gem_env.rb1
-rw-r--r--tool/lib/iseq_loader_checker.rb19
-rw-r--r--tool/lib/launchable.rb91
-rw-r--r--tool/lib/leakchecker.rb163
-rw-r--r--tool/lib/memory_status.rb102
-rw-r--r--tool/lib/minitest/README.txt457
-rw-r--r--tool/lib/minitest/autorun.rb14
-rw-r--r--tool/lib/minitest/benchmark.rb418
-rw-r--r--tool/lib/minitest/mock.rb196
-rw-r--r--tool/lib/minitest/unit.rb1420
-rw-r--r--tool/lib/output.rb70
-rw-r--r--tool/lib/path.rb101
-rw-r--r--tool/lib/profile_test_all.rb2
-rw-r--r--tool/lib/test/jobserver.rb47
-rw-r--r--tool/lib/test/unit.rb1015
-rw-r--r--tool/lib/test/unit/assertions.rb1029
-rw-r--r--tool/lib/test/unit/core_assertions.rb216
-rw-r--r--tool/lib/test/unit/parallel.rb91
-rw-r--r--tool/lib/test/unit/testcase.rb284
-rw-r--r--tool/lib/tracepointchecker.rb2
-rw-r--r--tool/lib/vcs.rb745
-rw-r--r--tool/lib/vpath.rb7
32 files changed, 4210 insertions, 3915 deletions
diff --git a/tool/lib/-test-/integer.rb b/tool/lib/-test-/integer.rb
index a224148f24..e60abf03a0 100644
--- a/tool/lib/-test-/integer.rb
+++ b/tool/lib/-test-/integer.rb
@@ -4,11 +4,11 @@ require '-test-/integer.so'
module Test::Unit::Assertions
def assert_fixnum(v, msg=nil)
assert_instance_of(Integer, v, msg)
- assert_predicate(v, :fixnum?, msg)
+ assert_send([Bug::Integer, :fixnum?, v], msg)
end
def assert_bignum(v, msg=nil)
assert_instance_of(Integer, v, msg)
- assert_predicate(v, :bignum?, msg)
+ assert_send([Bug::Integer, :bignum?, v], msg)
end
end
diff --git a/tool/lib/_tmpdir.rb b/tool/lib/_tmpdir.rb
new file mode 100644
index 0000000000..daa1a1f235
--- /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, in spite of UNIX socket
+ # path length being limited.
+ #
+ # Also Rubygems creates its own temporary directory per tests, and
+ # some tests copy the full path of gemhome there. In that case, 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/bundle_env.rb b/tool/lib/bundle_env.rb
new file mode 100644
index 0000000000..9ad5ea220b
--- /dev/null
+++ b/tool/lib/bundle_env.rb
@@ -0,0 +1,4 @@
+ENV["GEM_HOME"] = File.expand_path("../../.bundle", __dir__)
+ENV["BUNDLE_APP_CONFIG"] = File.expand_path("../../.bundle", __dir__)
+ENV["BUNDLE_PATH__SYSTEM"] = "true"
+ENV["BUNDLE_WITHOUT"] = "lint doc"
diff --git a/tool/lib/bundled_gem.rb b/tool/lib/bundled_gem.rb
new file mode 100644
index 0000000000..d2ed61a508
--- /dev/null
+++ b/tool/lib/bundled_gem.rb
@@ -0,0 +1,126 @@
+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
+ "strscan", # rexml
+ "psych" # rdoc
+ ]
+
+ module_function
+
+ def unpack(file, *rest)
+ pkg = Gem::Package.new(file)
+ prepare_test(pkg.spec, *rest) do |dir|
+ pkg.extract_files(dir)
+ FileUtils.rm_rf(Dir.glob(".git*", base: dir).map {|n| File.join(dir, n)})
+ end
+ 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] ||
+ begin File.read("lib/#{s.name}/version.rb")[/VERSION = "(.+?)"/, 1]; rescue; nil; end ||
+ raise("cannot find the version of #{ s.name } gem")
+ 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 855e1331ad..0904312119 100644
--- a/tool/lib/colorize.rb
+++ b/tool/lib/colorize.rb
@@ -1,10 +1,14 @@
# frozen-string-literal: true
class Colorize
+ # call-seq:
+ # Colorize.new(colorize = nil)
+ # Colorize.new(color: color, colors_file: colors_file)
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)
+ @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]*)/)] : {}
if opts and colors_file = opts[:colors_file]
@@ -23,19 +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 decorate(str, name)
- if @colors and color = (@colors[name] || DEFAULTS[name])
+ def coloring?
+ STDOUT.tty? && (!(nc = ENV['NO_COLOR']) || nc.empty?)
+ end
+
+ # colorize.decorate(str, name = color_name)
+ def decorate(str, name = @color)
+ 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)
@@ -44,7 +77,6 @@ class Colorize
end
if $0 == __FILE__
- colorize = Colorize.new
- col = ARGV.shift
- ARGV.each {|str| puts colorize.decorate(str, col)}
+ colorize = Colorize.new(ARGV.shift)
+ ARGV.each {|str| puts colorize.decorate(str)}
end
diff --git a/tool/lib/core_assertions.rb b/tool/lib/core_assertions.rb
new file mode 100644
index 0000000000..e29a0e3c25
--- /dev/null
+++ b/tool/lib/core_assertions.rb
@@ -0,0 +1,1033 @@
+# 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
+
+ def _assertions # :nodoc:
+ @_assertions ||= 0
+ end
+
+ ##
+ # Returns a proc that will output +msg+ along with the default message.
+
+ def message msg = nil, ending = nil, &default
+ proc {
+ ending ||= (ending_pattern = /(?<!\.)\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
+ require_relative 'envutil'
+ require 'pp'
+ begin
+ require '-test-/sanitizers'
+ rescue LoadError
+ # in test-unit-ruby-core gem
+ def sanitizers
+ nil
+ end
+ else
+ def sanitizers
+ Test::Sanitizers
+ end
+ end
+ module_function :sanitizers
+
+ nil.pretty_inspect
+
+ def mu_pp(obj) #:nodoc:
+ obj.pretty_inspect.chomp
+ end
+
+ def assert_file
+ AssertFile
+ end
+
+ FailDesc = proc do |status, message = "", out = ""|
+ now = Time.now
+ proc do
+ EnvUtil.failure_description(status, now, message, out)
+ end
+ end
+
+ def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil,
+ success: nil, failed: nil, gems: false, **opt)
+ args = Array(args).dup
+ unless gems.nil?
+ args.insert((Hash === args[0] ? 1 : 0), "--#{gems ? 'enable' : 'disable'}=gems")
+ end
+ stdout, stderr, status = EnvUtil.invoke_ruby(args, test_stdin, true, true, **opt)
+ desc = failed[status, message, stderr] if failed
+ desc ||= FailDesc[status, message, stderr]
+ if block_given?
+ raise "test_stdout ignored, use block only or without block" if test_stdout != []
+ raise "test_stderr ignored, use block only or without block" if test_stderr != []
+ yield(stdout.lines.map {|l| l.chomp }, stderr.lines.map {|l| l.chomp }, status)
+ else
+ all_assertions(desc) do |a|
+ [["stdout", test_stdout, stdout], ["stderr", test_stderr, stderr]].each do |key, exp, act|
+ a.for(key) do
+ if exp.is_a?(Regexp)
+ assert_match(exp, act)
+ elsif exp.all? {|e| String === e}
+ assert_equal(exp, act.lines.map {|l| l.chomp })
+ else
+ assert_pattern_list(exp, act)
+ end
+ end
+ end
+ unless success.nil?
+ a.for("success?") do
+ if success
+ assert_predicate(status, :success?)
+ else
+ assert_not_predicate(status, :success?)
+ end
+ end
+ end
+ end
+ status
+ end
+ end
+
+ if defined?(RubyVM::InstructionSequence)
+ def syntax_check(code, fname, line)
+ code = code.dup.force_encoding(Encoding::UTF_8)
+ RubyVM::InstructionSequence.compile(code, fname, fname, line)
+ :ok
+ ensure
+ raise if SyntaxError === $!
+ end
+ else
+ def syntax_check(code, fname, line)
+ code = code.b
+ code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) {
+ "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n"
+ }
+ code = code.force_encoding(Encoding::UTF_8)
+ catch {|tag| eval(code, binding, fname, line - 1)}
+ end
+ end
+
+ def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt)
+ # TODO: consider choosing some appropriate limit for 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 sanitizers&.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__),
+ *args,
+ "-v", "-",
+ ]
+ if defined? Memory::NO_MEMORY_LEAK_ENVS then
+ envs ||= {}
+ newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break }
+ envs = newenvs if newenvs
+ end
+ args.unshift(envs) if envs
+ cmd = [
+ 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}',
+ prepare,
+ 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")',
+ '$initial_size = $initial_status.size',
+ code,
+ 'GC.start',
+ ].join("\n")
+ _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt)
+ before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1)
+ after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1)
+ assert(status.success?, FailDesc[status, message, err])
+ ([:size, (rss && :rss)] & after.members).each do |n|
+ b = before[n]
+ a = after[n]
+ next unless a > 0 and b > 0
+ assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"})
+ end
+ rescue LoadError
+ pend
+ end
+
+ # :call-seq:
+ # assert_nothing_raised( *args, &block )
+ #
+ #If any exceptions are given as arguments, the assertion will
+ #fail if one of those exceptions are raised. Otherwise, the test fails
+ #if any exceptions are raised.
+ #
+ #The final argument may be a failure message.
+ #
+ # assert_nothing_raised RuntimeError do
+ # raise Exception #Assertion passes, Exception is not a RuntimeError
+ # end
+ #
+ # assert_nothing_raised do
+ # raise Exception #Assertion fails
+ # end
+ def assert_nothing_raised(*args)
+ self._assertions += 1
+ if Module === args.last
+ msg = nil
+ else
+ msg = args.pop
+ end
+ begin
+ yield
+ rescue Test::Unit::PendedError, *(Test::Unit::AssertionFailedError if args.empty?)
+ raise
+ rescue *(args.empty? ? Exception : args) => e
+ msg = message(msg) {
+ "Exception raised:\n<#{mu_pp(e)}>\n""Backtrace:\n" <<
+ Test.filter_backtrace(e.backtrace).map{|frame| " #{frame}"}.join("\n")
+ }
+ raise Test::Unit::AssertionFailedError, msg.call, e.backtrace
+ end
+ end
+
+ def prepare_syntax_check(code, fname = nil, mesg = nil, verbose: nil)
+ fname ||= caller_locations(2, 1)[0]
+ mesg ||= fname.to_s
+ verbose, $VERBOSE = $VERBOSE, verbose
+ case
+ when Array === fname
+ fname, line = *fname
+ when defined?(fname.path) && defined?(fname.lineno)
+ fname, line = fname.path, fname.lineno
+ else
+ line = 1
+ end
+ yield(code, fname, line, message(mesg) {
+ if code.end_with?("\n")
+ "```\n#{code}```\n"
+ else
+ "```\n#{code}\n```\n""no-newline"
+ end
+ })
+ ensure
+ $VERBOSE = verbose
+ end
+
+ def assert_valid_syntax(code, *args, **opt)
+ prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg|
+ yield if defined?(yield)
+ assert_nothing_raised(SyntaxError, mesg) do
+ assert_equal(:ok, syntax_check(src, fname, line), mesg)
+ end
+ end
+ end
+
+ def assert_normal_exit(testsrc, message = '', child_env: nil, **opt)
+ assert_valid_syntax(testsrc, caller_locations(1, 1)[0])
+ if child_env
+ child_env = [child_env]
+ else
+ child_env = []
+ end
+ out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt)
+ assert !status.signaled?, FailDesc[status, message, out]
+ end
+
+ def assert_ruby_status(args, test_stdin="", message=nil, **opt)
+ out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt)
+ desc = FailDesc[status, message, out]
+ assert(!status.signaled?, desc)
+ message ||= "ruby exit status is not success:"
+ assert(status.success?, desc)
+ end
+
+ ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM")
+
+ def separated_runner(token, out = nil)
+ include(*Test::Unit::TestCase.ancestors.select {|c| !c.is_a?(Class) })
+
+ out = out ? IO.new(out, 'w') : STDOUT
+
+ # avoid method redefinitions
+ out_write = out.method(:write)
+ integer_to_s = Integer.instance_method(:to_s)
+ array_pack = Array.instance_method(:pack)
+ marshal_dump = Marshal.method(:dump)
+ assertions_ivar_set = Test::Unit::Assertions.method(:instance_variable_set)
+ assertions_ivar_get = Test::Unit::Assertions.method(:instance_variable_get)
+ Test::Unit::Assertions.module_eval do
+ @_assertions = 0
+
+ undef _assertions=
+ define_method(:_assertions=, ->(n) {assertions_ivar_set.call(:@_assertions, n)})
+
+ undef _assertions
+ define_method(:_assertions, -> {assertions_ivar_get.call(:@_assertions)})
+ end
+ # assume Method#call and UnboundMethod#bind_call need to work as the original
+
+ at_exit {
+ assertions = assertions_ivar_get.call(:@_assertions)
+ out_write.call <<~OUT
+ <error id="#{token}" assertions=#{integer_to_s.bind_call(assertions)}>
+ #{array_pack.bind_call([marshal_dump.call($!)], 'm0')}
+ </error id="#{token}">
+ OUT
+ }
+ if defined?(Test::Unit::Runner)
+ Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true)
+ elsif defined?(Test::Unit::AutoRunner)
+ Test::Unit::AutoRunner.need_auto_run = false
+ end
+ end
+
+ def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt)
+ unless file and line
+ loc, = caller_locations(1,1)
+ file ||= loc.path
+ line ||= loc.lineno
+ end
+ capture_stdout = true
+ unless /mswin|mingw/ =~ RbConfig::CONFIG['host_os']
+ capture_stdout = false
+ opt[:out] = Test::Unit::Runner.output if defined?(Test::Unit::Runner)
+ res_p, res_c = IO.pipe
+ opt[:ios] = [res_c]
+ end
+ token_dump, token_re = new_test_token
+ src = <<eom
+# -*- coding: #{line += __LINE__; src.encoding}; -*-
+BEGIN {
+ 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
+ # power_assert 3 requires ruby 3.1 or later
+ args << "-W:no-experimental" if RUBY_VERSION < "3.1."
+ stdout, stderr, status = EnvUtil.invoke_ruby(args, src, capture_stdout, true, **opt)
+
+ if sanitizers&.lsan_enabled?
+ # LSAN may output messages like the following line into stderr. We should ignore it.
+ # ==276855==Running thread 276851 was not suspended. False leaks are possible.
+ # See https://github.com/google/sanitizers/issues/1479
+ stderr.gsub!(/==\d+==Running thread \d+ was not suspended\. False leaks are possible\.\n/, "")
+ end
+ ensure
+ if res_c
+ res_c.close
+ res = res_p.read
+ res_p.close
+ else
+ res = stdout
+ end
+ raise if $!
+ abort = status.coredump? || (status.signaled? && ABORT_SIGNALS.include?(status.termsig))
+ marshal_error = nil
+ assert(!abort, FailDesc[status, nil, stderr])
+ res.scan(/^<error id="#{token_re}" assertions=(\d+)>\n(.*?)\n(?=<\/error id="#{token_re}">$)/m) do
+ self._assertions += $1.to_i
+ res = Marshal.load($2.unpack1("m")) or next
+ rescue => marshal_error
+ ignore_stderr = nil
+ res = nil
+ else
+ next if SystemExit === res
+ if bt = res.backtrace
+ bt.each do |l|
+ l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"}
+ end
+ bt.concat(caller)
+ else
+ res.set_backtrace(caller)
+ end
+ raise res
+ end
+
+ # really did it succeed?
+ unless ignore_stderr
+ # the body of assert_separately must not output anything to detect error
+ assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr])
+ end
+ assert(status.success?, FailDesc[status, "assert_separately failed", stderr])
+ raise marshal_error if marshal_error
+ end
+
+ # Run Ractor-related test without influencing the main test suite
+ def assert_ractor(src, args: [], require: nil, require_relative: nil, file: nil, line: nil, ignore_stderr: nil, **opt)
+ omit unless defined?(Ractor)
+
+ # https://bugs.ruby-lang.org/issues/21262
+ shim_value = "class Ractor; alias value take; end" unless Ractor.method_defined?(:value)
+ shim_join = "class Ractor; alias join take; end" unless Ractor.method_defined?(:join)
+
+ if require
+ require = [require] unless require.is_a?(Array)
+ require = require.map {|r| "require #{r.inspect}"}.join("\n")
+ end
+
+ if require_relative
+ dir = File.dirname(caller_locations[0,1][0].absolute_path)
+ full_path = File.expand_path(require_relative, dir)
+ require = "#{require}; require #{full_path.inspect}"
+ end
+
+ assert_separately(args, file, line, <<~RUBY, ignore_stderr: ignore_stderr, **opt)
+ #{shim_value}
+ #{shim_join}
+ #{require}
+ previous_verbose = $VERBOSE
+ $VERBOSE = nil
+ Ractor.new {} # trigger initial warning
+ $VERBOSE = previous_verbose
+ #{src}
+ RUBY
+ end
+
+ # :call-seq:
+ # assert_throw( tag, failure_message = nil, &block )
+ #
+ #Fails unless the given block throws +tag+, returns the caught
+ #value otherwise.
+ #
+ #An optional failure message may be provided as the final argument.
+ #
+ # tag = Object.new
+ # assert_throw(tag, "#{tag} was not thrown!") do
+ # throw tag
+ # end
+ def assert_throw(tag, msg = nil)
+ ret = catch(tag) do
+ begin
+ yield(tag)
+ rescue UncaughtThrowError => e
+ thrown = e.tag
+ end
+ msg = message(msg) {
+ "Expected #{mu_pp(tag)} to have been thrown"\
+ "#{%Q[, not #{thrown}] if thrown}"
+ }
+ assert(false, msg)
+ end
+ assert(true)
+ ret
+ end
+
+ # :call-seq:
+ # assert_raise( *args, &block )
+ #
+ #Tests if the given block raises an exception. Acceptable exception
+ #types may be given as optional arguments. If the last argument is a
+ #String, it will be used as the error message.
+ #
+ # assert_raise do #Fails, no Exceptions are raised
+ # end
+ #
+ # assert_raise NameError do
+ # puts x #Raises NameError, so assertion succeeds
+ # end
+ def assert_raise(*exp, &b)
+ case exp.last
+ when String, Proc
+ msg = exp.pop
+ end
+
+ begin
+ yield
+ rescue Test::Unit::PendedError => e
+ return e if exp.include? Test::Unit::PendedError
+ raise e
+ rescue Exception => e
+ expected = exp.any? { |ex|
+ if ex.instance_of? Module then
+ e.kind_of? ex
+ else
+ e.instance_of? ex
+ end
+ }
+
+ assert expected, proc {
+ flunk(message(msg) {"#{mu_pp(exp)} exception expected, not #{mu_pp(e)}"})
+ }
+
+ return e
+ ensure
+ unless e
+ exp = exp.first if exp.size == 1
+
+ flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"})
+ end
+ end
+ end
+
+ # :call-seq:
+ # assert_raise_with_message(exception, expected, msg = nil, &block)
+ #
+ #Tests if the given block raises an exception with the expected
+ #message.
+ #
+ # assert_raise_with_message(RuntimeError, "foo") do
+ # nil #Fails, no Exceptions are raised
+ # end
+ #
+ # assert_raise_with_message(RuntimeError, "foo") do
+ # raise ArgumentError, "foo" #Fails, different Exception is raised
+ # end
+ #
+ # assert_raise_with_message(RuntimeError, "foo") do
+ # raise "bar" #Fails, RuntimeError is raised but the message differs
+ # end
+ #
+ # assert_raise_with_message(RuntimeError, "foo") do
+ # raise "foo" #Raises RuntimeError with the message, so assertion succeeds
+ # end
+ def assert_raise_with_message(exception, expected, msg = nil, &block)
+ case expected
+ when String
+ assert = :assert_equal
+ else
+ assert_respond_to(expected, :===)
+ assert = :assert_match
+ end
+
+ ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do
+ yield
+ end
+ m = ex.message
+ msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"}
+
+ if assert == :assert_equal
+ assert_equal(expected, m, msg)
+ else
+ msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" }
+ assert expected =~ m, msg
+ block.binding.eval("proc{|_|$~=_}").call($~)
+ end
+ ex
+ end
+
+ # :call-seq:
+ # assert_raise_kind_of(*args, &block)
+ #
+ #Tests if the given block raises one of the given exceptions or
+ #sub exceptions of the given exceptions. If the last argument
+ #is a String, it will be used as the error message.
+ #
+ # assert_raise do #Fails, no Exceptions are raised
+ # end
+ #
+ # assert_raise SystemCallErr do
+ # Dir.chdir(__FILE__) #Raises Errno::ENOTDIR, so assertion succeeds
+ # end
+ def assert_raise_kind_of(*exp, &b)
+ case exp.last
+ when String, Proc
+ msg = exp.pop
+ end
+
+ begin
+ yield
+ rescue Test::Unit::PendedError => e
+ raise e unless exp.include? Test::Unit::PendedError
+ rescue *exp => e
+ pass
+ rescue Exception => e
+ flunk(message(msg) {"#{mu_pp(exp)} family exception expected, not #{mu_pp(e)}"})
+ ensure
+ unless e
+ exp = exp.first if exp.size == 1
+
+ flunk(message(msg) {"#{mu_pp(exp)} family expected but nothing was raised"})
+ end
+ end
+ e
+ end
+
+ TEST_DIR = File.join(__dir__, "test/unit") #:nodoc:
+
+ # :call-seq:
+ # assert(test, [failure_message])
+ #
+ #Tests if +test+ is true.
+ #
+ #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used
+ #as the failure message. Otherwise, the result of calling +msg+ will be
+ #used as the message if the assertion fails.
+ #
+ #If no +msg+ is given, a default message will be used.
+ #
+ # assert(false, "This was expected to be true")
+ def assert(test, *msgs)
+ case msg = msgs.first
+ when String, Proc
+ when nil
+ msgs.shift
+ else
+ bt = caller.reject { |s| s.start_with?(TEST_DIR) }
+ raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt
+ end unless msgs.empty?
+ super
+ end
+
+ # :call-seq:
+ # assert_respond_to( object, method, failure_message = nil )
+ #
+ #Tests if the given Object responds to +method+.
+ #
+ #An optional failure message may be provided as the final argument.
+ #
+ # assert_respond_to("hello", :reverse) #Succeeds
+ # assert_respond_to("hello", :does_not_exist) #Fails
+ def assert_respond_to(obj, (meth, *priv), msg = nil)
+ unless priv.empty?
+ msg = message(msg) {
+ "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}"
+ }
+ return assert obj.respond_to?(meth, *priv), msg
+ end
+ #get rid of overcounting
+ if caller_locations(1, 1)[0].path.start_with?(TEST_DIR)
+ return if obj.respond_to?(meth)
+ end
+ super(obj, meth, msg)
+ end
+
+ # :call-seq:
+ # assert_not_respond_to( object, method, failure_message = nil )
+ #
+ #Tests if the given Object does not respond to +method+.
+ #
+ #An optional failure message may be provided as the final argument.
+ #
+ # assert_not_respond_to("hello", :reverse) #Fails
+ # assert_not_respond_to("hello", :does_not_exist) #Succeeds
+ def assert_not_respond_to(obj, (meth, *priv), msg = nil)
+ unless priv.empty?
+ msg = message(msg) {
+ "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}"
+ }
+ return assert !obj.respond_to?(meth, *priv), msg
+ end
+ #get rid of overcounting
+ if caller_locations(1, 1)[0].path.start_with?(TEST_DIR)
+ return unless obj.respond_to?(meth)
+ end
+ refute_respond_to(obj, meth, msg)
+ end
+
+ # pattern_list is an array which contains regexp, string and :*.
+ # :* means any sequence.
+ #
+ # pattern_list is anchored.
+ # Use [:*, regexp/string, :*] for non-anchored match.
+ def assert_pattern_list(pattern_list, actual, message=nil)
+ rest = actual
+ anchored = true
+ pattern_list.each_with_index {|pattern, i|
+ if pattern == :*
+ anchored = false
+ else
+ if anchored
+ match = rest.rindex(pattern, 0)
+ else
+ match = rest.index(pattern)
+ end
+ if match
+ post_match = $~ ? $~.post_match : rest[match+pattern.size..-1]
+ else
+ msg = message(msg) {
+ expect_msg = "Expected #{mu_pp pattern}\n"
+ if /\n[^\n]/ =~ rest
+ actual_mesg = +"to match\n"
+ rest.scan(/.*\n+/) {
+ actual_mesg << ' ' << $&.inspect << "+\n"
+ }
+ actual_mesg.sub!(/\+\n\z/, '')
+ else
+ actual_mesg = "to match " + mu_pp(rest)
+ end
+ actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters"
+ expect_msg + actual_mesg
+ }
+ assert false, msg
+ end
+ rest = post_match
+ anchored = true
+ end
+ }
+ if anchored
+ assert_equal("", rest)
+ end
+ end
+
+ def assert_warning(pat, msg = nil)
+ result = nil
+ stderr = EnvUtil.with_default_internal(of: pat) {
+ EnvUtil.verbose_warning {
+ result = yield
+ }
+ }
+ msg = message(msg) {diff pat, stderr}
+ assert(pat === stderr, msg)
+ result
+ end
+
+ def assert_warn(*args)
+ assert_warning(*args) {$VERBOSE = false; yield}
+ end
+
+ def assert_deprecated_warning(mesg = /deprecated/, &block)
+ assert_warning(mesg) do
+ EnvUtil.deprecation_warning(&block)
+ end
+ end
+
+ def assert_deprecated_warn(mesg = /deprecated/, &block)
+ assert_warn(mesg) do
+ EnvUtil.deprecation_warning(&block)
+ end
+ end
+
+ class << (AssertFile = Struct.new(:failure_message).new)
+ include Assertions
+ include CoreAssertions
+ def assert_file_predicate(predicate, *args)
+ if /\Anot_/ =~ predicate
+ predicate = $'
+ neg = " not"
+ end
+ result = File.__send__(predicate, *args)
+ result = !result if neg
+ mesg = "Expected file ".dup << args.shift.inspect
+ mesg << "#{neg} to be #{predicate}"
+ mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty?
+ mesg << " #{failure_message}" if failure_message
+ assert(result, mesg)
+ end
+ alias method_missing assert_file_predicate
+
+ def for(message)
+ clone.tap {|a| a.failure_message = message}
+ end
+ end
+
+ class AllFailures
+ attr_reader :failures
+
+ def initialize
+ @count = 0
+ @failures = {}
+ end
+
+ def for(key)
+ @count += 1
+ yield key
+ rescue Exception => e
+ @failures[key] = [@count, e]
+ end
+
+ def foreach(*keys)
+ keys.each do |key|
+ @count += 1
+ begin
+ yield key
+ rescue Exception => e
+ @failures[key] = [@count, e]
+ end
+ end
+ end
+
+ def message
+ i = 0
+ total = @count.to_s
+ fmt = "%#{total.size}d"
+ @failures.map {|k, (n, v)|
+ v = v.message
+ "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.b.gsub(/^/, ' | ').force_encoding(v.encoding)}"
+ }.join("\n")
+ end
+
+ def pass?
+ @failures.empty?
+ end
+ end
+
+ # threads should respond to shift method.
+ # Array can be used.
+ def assert_join_threads(threads, message = nil)
+ errs = []
+ values = []
+ while th = threads.shift
+ begin
+ values << th.value
+ rescue Exception
+ errs << [th, $!]
+ th = nil
+ end
+ end
+ values
+ ensure
+ if th&.alive?
+ th.raise(Timeout::Error.new)
+ th.join rescue errs << [th, $!]
+ end
+ if !errs.empty?
+ msg = "exceptions on #{errs.length} threads:\n" +
+ errs.map {|t, err|
+ "#{t.inspect}:\n" +
+ (err.respond_to?(:full_message) ? err.full_message(highlight: false, order: :top) : err.message)
+ }.join("\n---\n")
+ if message
+ msg = "#{message}\n#{msg}"
+ end
+ raise Test::Unit::AssertionFailedError, msg
+ end
+ end
+
+ def assert_all?(obj, m = nil, &blk)
+ failed = []
+ obj.each do |*a, &b|
+ unless blk.call(*a, &b)
+ failed << (a.size > 1 ? a : a[0])
+ end
+ end
+ assert(failed.empty?, message(m) {failed.pretty_inspect})
+ end
+
+ def assert_all_assertions(msg = nil)
+ all = AllFailures.new
+ yield all
+ ensure
+ assert(all.pass?, message(msg) {all.message.chomp(".")})
+ end
+ alias all_assertions assert_all_assertions
+
+ def assert_all_assertions_foreach(msg = nil, *keys, &block)
+ all = AllFailures.new
+ all.foreach(*keys, &block)
+ ensure
+ assert(all.pass?, message(msg) {all.message.chomp(".")})
+ end
+ alias all_assertions_foreach assert_all_assertions_foreach
+
+ %w[
+ CLOCK_THREAD_CPUTIME_ID CLOCK_PROCESS_CPUTIME_ID
+ CLOCK_MONOTONIC
+ ].find do |c|
+ if Process.const_defined?(c)
+ [c.to_sym, Process.const_get(c)].find do |clk|
+ begin
+ Process.clock_gettime(clk)
+ rescue
+ # Constants may be defined but not implemented, e.g., mingw.
+ else
+ unless Process.clock_getres(clk) < 1.0e-03
+ next # needs msec precision
+ end
+ PERFORMANCE_CLOCK = clk
+ end
+ end
+ end
+ end
+
+ # Expect +seq+ to respond to +first+ and +each+ methods, e.g.,
+ # Array, Range, Enumerator::ArithmeticSequence and other
+ # Enumerable-s, and each elements should be size factors.
+ #
+ # :yield: each elements of +seq+.
+ def assert_linear_performance(seq, rehearsal: nil, pre: ->(n) {n})
+ pend "No PERFORMANCE_CLOCK found" unless defined?(PERFORMANCE_CLOCK)
+
+ # Timeout testing generally doesn't work when RJIT compilation happens.
+ rjit_enabled = defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled?
+ measure = proc do |arg, message|
+ st = Process.clock_gettime(PERFORMANCE_CLOCK)
+ yield(*arg)
+ t = (Process.clock_gettime(PERFORMANCE_CLOCK) - st)
+ assert_operator 0, :<=, t, message unless rjit_enabled
+ t
+ end
+
+ first = seq.first
+ *arg = pre.call(first)
+ times = (0..(rehearsal || (2 * first))).map do
+ measure[arg, "rehearsal"].nonzero?
+ end
+ times.compact!
+ tmin, tmax = times.minmax
+
+ # safe_factor * tmax * rehearsal_time_variance_factor(equals to 1 when variance is small)
+ tbase = 10 * tmax * [(tmax / tmin) ** 2 / 4, 1].max
+ info = "(tmin: #{tmin}, tmax: #{tmax}, tbase: #{tbase})"
+
+ seq.each do |i|
+ next if i == first
+ t = tbase * i.fdiv(first)
+ *arg = pre.call(i)
+ message = "[#{i}]: in #{t}s #{info}"
+ Timeout.timeout(t, Timeout::Error, message) do
+ measure[arg, message]
+ end
+ end
+ end
+
+ def diff(exp, act)
+ require 'pp'
+ q = PP.new(+"")
+ q.guard_inspect_key do
+ q.group(2, "expected: ") do
+ q.pp exp
+ end
+ q.text q.newline
+ q.group(2, "actual: ") do
+ q.pp act
+ end
+ q.flush
+ end
+ q.output
+ end
+
+ def new_test_token
+ token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m"
+ return token.dump, Regexp.quote(token)
+ end
+
+ # Platform predicates
+
+ def self.mswin?
+ defined?(@mswin) ? @mswin : @mswin = RUBY_PLATFORM.include?('mswin')
+ end
+ private def mswin?
+ CoreAssertions.mswin?
+ end
+
+ def self.mingw?
+ defined?(@mingw) ? @mingw : @mingw = RUBY_PLATFORM.include?('mingw')
+ end
+ private def mingw?
+ CoreAssertions.mingw?
+ end
+
+ module_function def windows?
+ mswin? or mingw?
+ end
+
+ def self.version_compare(expected, actual)
+ expected.zip(actual).each {|e, a| z = (e <=> a); return z if z.nonzero?}
+ 0
+ end
+
+ def self.version_match?(expected, actual)
+ if !actual
+ false
+ elsif expected.empty?
+ true
+ elsif expected.size == 1 and Range === (range = expected.first)
+ b, e = range.begin, range.end
+ return false if b and (c = version_compare(Array(b), actual)) > 0
+ return false if e and (c = version_compare(Array(e), actual)) < 0
+ return false if e and range.exclude_end? and c == 0
+ true
+ else
+ version_compare(expected, actual).zero?
+ end
+ end
+
+ def self.linux?(*ver)
+ unless defined?(@linux)
+ @linux = RUBY_PLATFORM.include?('linux') && `uname -r`.scan(/\d+/).map(&:to_i)
+ end
+ version_match? ver, @linux
+ end
+ private def linux?(*ver)
+ CoreAssertions.linux?(*ver)
+ end
+
+ def self.glibc?(*ver)
+ unless defined?(@glibc)
+ libc = `/usr/bin/ldd /bin/sh`[/^\s*libc.*=> *\K\S*/]
+ if libc and /version (\d+)\.(\d+)\.$/ =~ IO.popen([libc], &:read)[]
+ @glibc = [$1.to_i, $2.to_i]
+ else
+ @glibc = false
+ end
+ end
+ version_match? ver, @glibc
+ end
+ private def glibc?(*ver)
+ CoreAssertions.glibc?(*ver)
+ end
+
+ def self.macos?(*ver)
+ unless defined?(@macos)
+ @macos = RUBY_PLATFORM.include?('darwin') && `sw_vers -productVersion`.scan(/\d+/).map(&:to_i)
+ end
+ version_match? ver, @macos
+ end
+ private def macos?(*ver)
+ CoreAssertions.macos?(*ver)
+ end
+ end
+ end
+end
diff --git a/tool/lib/dump.gdb b/tool/lib/dump.gdb
new file mode 100644
index 0000000000..56b420a546
--- /dev/null
+++ b/tool/lib/dump.gdb
@@ -0,0 +1,17 @@
+set height 0
+set width 0
+set confirm off
+
+echo \n>>> Threads\n\n
+info threads
+
+echo \n>>> Machine level backtrace\n\n
+thread apply all info stack full
+
+echo \n>>> Dump Ruby level backtrace (if possible)\n\n
+call rb_vmdebug_stack_dump_all_threads()
+call fflush(stderr)
+
+echo ">>> Finish\n"
+detach
+quit
diff --git a/tool/lib/dump.lldb b/tool/lib/dump.lldb
new file mode 100644
index 0000000000..ed9cb89010
--- /dev/null
+++ b/tool/lib/dump.lldb
@@ -0,0 +1,13 @@
+script print("\n>>> Threads\n\n")
+thread list
+
+script print("\n>>> Machine level backtrace\n\n")
+thread backtrace all
+
+script print("\n>>> Dump Ruby level backtrace (if possible)\n\n")
+call rb_vmdebug_stack_dump_all_threads()
+call fflush(stderr)
+
+script print(">>> Finish\n")
+detach
+quit
diff --git a/tool/lib/envutil.rb b/tool/lib/envutil.rb
index b4eb63da90..6089605056 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
@@ -47,12 +46,20 @@ module EnvUtil
class << self
attr_accessor :timeout_scale
attr_reader :original_internal_encoding, :original_external_encoding,
- :original_verbose
+ :original_verbose, :original_warning
def capture_global_values
@original_internal_encoding = Encoding.default_internal
@original_external_encoding = Encoding.default_external
@original_verbose = $VERBOSE
+ @original_warning =
+ 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
@@ -72,6 +79,72 @@ module EnvUtil
end
module_function :timeout
+ class Debugger
+ @list = []
+
+ attr_accessor :name
+
+ def self.register(name, &block)
+ @list << new(name, &block)
+ end
+
+ def initialize(name, &block)
+ @name = name
+ instance_eval(&block)
+ end
+
+ def usable?; false; end
+
+ def start(pid, *args) end
+
+ def dump(pid, timeout: 60, reprieve: timeout&.div(4))
+ dpid = start(pid, *command_file(File.join(__dir__, "dump.#{name}")), out: :err)
+ rescue Errno::ENOENT
+ return
+ else
+ return unless dpid
+ [[timeout, :TERM], [reprieve, :KILL]].find do |t, sig|
+ begin
+ return EnvUtil.timeout(t) {Process.wait(dpid)}
+ rescue Timeout::Error
+ Process.kill(sig, dpid)
+ end
+ end
+ true
+ end
+
+ # sudo -n: --non-interactive
+ PRECOMMAND = (%[sudo -n] if /darwin/ =~ RUBY_PLATFORM)
+
+ def spawn(*args, **opts)
+ super(*PRECOMMAND, *args, **opts)
+ end
+
+ register("gdb") do
+ class << self
+ def usable?; system(*%w[gdb --batch --quiet --nx -ex exit]); end
+ def start(pid, *args, **opts)
+ spawn(*%W[gdb --batch --quiet --pid #{pid}], *args, **opts)
+ end
+ def command_file(file) "--command=#{file}"; end
+ end
+ end
+
+ register("lldb") do
+ class << self
+ def usable?; system(*%w[lldb -Q --no-lldbinit -o exit]); end
+ def start(pid, *args, **opts)
+ spawn(*%W[lldb --batch -Q --attach-pid #{pid}], *args, **opts)
+ end
+ def command_file(file) ["--source", file]; end
+ end
+ end
+
+ def self.search
+ @debugger ||= @list.find(&:usable?)
+ end
+ end
+
def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1)
reprieve = apply_timeout_scale(reprieve) if reprieve
@@ -86,7 +159,15 @@ module EnvUtil
when nil, false
pgroup = pid
end
+
+ dumped = false
while signal = signals.shift
+
+ if !dumped and [:ABRT, :KILL].include?(signal)
+ Debugger.search&.dump(pid)
+ dumped = true
+ end
+
begin
Process.kill signal, pgroup
rescue Errno::EINVAL
@@ -100,6 +181,8 @@ module EnvUtil
begin
Timeout.timeout(reprieve) {Process.wait(pid)}
rescue Timeout::Error
+ else
+ break
end
end
end
@@ -109,7 +192,7 @@ module EnvUtil
def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false,
encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error,
- stdout_filter: nil, stderr_filter: nil,
+ stdout_filter: nil, stderr_filter: nil, ios: nil,
signal: :TERM,
rubybin: EnvUtil.rubybin, precommand: nil,
**opt)
@@ -125,6 +208,8 @@ module EnvUtil
out_p.set_encoding(encoding) if out_p
err_p.set_encoding(encoding) if err_p
end
+ ios.each {|i, o = i|opt[i] = o} if ios
+
c = "C"
child_env = {}
LANG_ENVS.each {|lc| child_env[lc] = c}
@@ -134,11 +219,24 @@ module EnvUtil
if RUBYLIB and lib = child_env["RUBYLIB"]
child_env["RUBYLIB"] = [lib, RUBYLIB].join(File::PATH_SEPARATOR)
end
+
+ # remain env
+ %w(ASAN_OPTIONS RUBY_ON_BUG).each{|name|
+ child_env[name] = ENV[name] if !child_env.key?(name) and ENV.key?(name)
+ }
+
args = [args] if args.kind_of?(String)
- pid = spawn(child_env, *precommand, rubybin, *args, **opt)
+ # use the same parser as current ruby
+ if (args.none? { |arg| arg.start_with?("--parser=") } and
+ /^ +--parser=/ =~ IO.popen([rubybin, "--help"], &:read))
+ args = ["--parser=#{current_parser}"] + args
+ end
+ 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
@@ -150,6 +248,7 @@ module EnvUtil
timeout_error = nil
else
status = terminate(pid, signal, opt[:pgroup], reprieve)
+ terminated = Time.now
end
stdout = th_stdout.value if capture_stdout
stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout
@@ -161,7 +260,7 @@ module EnvUtil
if timeout_error
bt = caller_locations
msg = "execution of #{bt.shift.label} expired timeout (#{timeout} sec)"
- msg = Test::Unit::Assertions::FailDesc[status, msg, [stdout, stderr].join("\n")].()
+ msg = failure_description(status, terminated, msg, [stdout, stderr].join("\n"))
raise timeout_error, msg, bt.map(&:to_s)
end
return stdout, stderr, status
@@ -179,10 +278,11 @@ module EnvUtil
end
module_function :invoke_ruby
- alias rubyexec invoke_ruby
- class << self
- alias rubyexec invoke_ruby
+ def current_parser
+ features = RUBY_DESCRIPTION[%r{\)\K [-+*/%._0-9a-zA-Z\[\] ]*(?=\[[-+*/%._0-9a-zA-Z]+\]\z)}]
+ features&.split&.include?("+PRISM") ? "prism" : "parse.y"
end
+ module_function :current_parser
def verbose_warning
class << (stderr = "".dup)
@@ -196,9 +296,25 @@ module EnvUtil
ensure
stderr, $stderr = $stderr, stderr
$VERBOSE = EnvUtil.original_verbose
+ EnvUtil.original_warning&.each {|i, v| Warning[i] = v}
end
module_function :verbose_warning
+ if defined?(Warning.[]=)
+ def deprecation_warning
+ previous_deprecated = Warning[:deprecated]
+ Warning[:deprecated] = true
+ yield
+ ensure
+ Warning[:deprecated] = previous_deprecated
+ end
+ else
+ def deprecation_warning
+ yield
+ end
+ end
+ module_function :deprecation_warning
+
def default_warning
$VERBOSE = false
yield
@@ -223,7 +339,30 @@ module EnvUtil
end
module_function :under_gc_stress
- def with_default_external(enc)
+ 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
+
+ if GC.respond_to?(:auto_compact)
+ auto_compact = GC.auto_compact
+ GC.auto_compact = val
+ end
+
+ under_gc_stress(&block)
+ ensure
+ GC.auto_compact = auto_compact if GC.respond_to?(: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 = nil, of: nil)
+ enc = of.encoding if defined?(of.encoding)
suppress_warning { Encoding.default_external = enc }
yield
ensure
@@ -231,7 +370,8 @@ module EnvUtil
end
module_function :with_default_external
- def with_default_internal(enc)
+ def with_default_internal(enc = nil, of: nil)
+ enc = of.encoding if defined?(of.encoding)
suppress_warning { Encoding.default_internal = enc }
yield
ensure
@@ -241,7 +381,11 @@ module EnvUtil
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
@@ -249,7 +393,11 @@ module EnvUtil
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
@@ -266,16 +414,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
@@ -286,6 +442,37 @@ module EnvUtil
end
end
+ def self.failure_description(status, now, message = "", out = "")
+ pid = status.pid
+ if signo = status.termsig
+ signame = Signal.signame(signo)
+ sigdesc = "signal #{signo}"
+ end
+ log = diagnostic_reports(signame, pid, now)
+ if signame
+ sigdesc = "SIG#{signame} (#{sigdesc})"
+ end
+ if status.coredump?
+ sigdesc = "#{sigdesc} (core dumped)"
+ end
+ full_message = ''.dup
+ message = message.call if Proc === message
+ if message and !message.empty?
+ full_message << message << "\n"
+ end
+ full_message << "pid #{pid}"
+ full_message << " exit #{status.exitstatus}" if status.exited?
+ full_message << " killed by #{sigdesc}" if sigdesc
+ if out and !out.empty?
+ full_message << "\n" << out.b.gsub(/^/, '| ')
+ full_message.sub!(/(?<!\n)\z/, "\n")
+ end
+ if log
+ full_message << "Diagnostic reports:\n" << log.b.gsub(/^/, '| ')
+ end
+ full_message
+ end
+
def self.gc_stress_to_class?
unless defined?(@gc_stress_to_class)
_, _, status = invoke_ruby(["-e""exit GC.respond_to?(:add_stress_to_class)"])
@@ -304,7 +491,6 @@ if defined?(RbConfig)
end
dir = File.dirname(ruby)
CONFIG['bindir'] = dir
- Gem::ConfigMap[:bindir] = dir if defined?(Gem::ConfigMap)
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/gem_env.rb b/tool/lib/gem_env.rb
new file mode 100644
index 0000000000..1893e07657
--- /dev/null
+++ b/tool/lib/gem_env.rb
@@ -0,0 +1 @@
+ENV['GEM_HOME'] = File.expand_path('../../.bundle', __dir__)
diff --git a/tool/lib/iseq_loader_checker.rb b/tool/lib/iseq_loader_checker.rb
index 1a1a694834..73784f8450 100644
--- a/tool/lib/iseq_loader_checker.rb
+++ b/tool/lib/iseq_loader_checker.rb
@@ -38,8 +38,14 @@ class RubyVM::InstructionSequence
i2
end
- CHECK_TO_A = ENV['RUBY_ISEQ_DUMP_DEBUG'] == 'to_a'
- CHECK_TO_BINARY = ENV['RUBY_ISEQ_DUMP_DEBUG'] == 'to_binary'
+ opt = ENV['RUBY_ISEQ_DUMP_DEBUG']
+
+ if opt && caller.any?{|e| /test\/runner\.rb/ =~ e}
+ puts "RUBY_ISEQ_DUMP_DEBUG = #{opt}" if opt
+ end
+
+ CHECK_TO_A = 'to_a' == opt
+ CHECK_TO_BINARY = 'to_binary' == opt
def self.translate i1
# check to_a/load_iseq
@@ -70,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/launchable.rb b/tool/lib/launchable.rb
new file mode 100644
index 0000000000..38f4fe92b3
--- /dev/null
+++ b/tool/lib/launchable.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+require 'json'
+require 'uri'
+
+module Launchable
+ ##
+ # 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
diff --git a/tool/lib/leakchecker.rb b/tool/lib/leakchecker.rb
index af9200bf77..69df9a64b8 100644
--- a/tool/lib/leakchecker.rb
+++ b/tool/lib/leakchecker.rb
@@ -1,31 +1,35 @@
# 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_safe(test_name),
check_verbose(test_name),
+ check_warning_flags(test_name),
]
GC.start if leaks.any?
end
- def check_safe test_name
- puts "#{test_name}: $SAFE == #{$SAFE}" unless $SAFE == 0
- end
-
def check_verbose test_name
puts "#{test_name}: $VERBOSE == #{$VERBOSE}" unless @old_verbose == $VERBOSE
end
@@ -34,19 +38,19 @@ class LeakChecker
if IO.respond_to?(:console) and (m = IO.method(:console)).arity.nonzero?
m[:close]
end
- fd_dir = "/proc/self/fd"
- 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
- }
- fds.sort
- else
- []
+ %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)
@@ -60,7 +64,7 @@ class LeakChecker
}
end
fd_leaked = live2 - live1
- if !fd_leaked.empty?
+ if !@@skip && !fd_leaked.empty?
leaked = true
h = {}
ObjectSpace.each_object(IO) {|io|
@@ -73,30 +77,54 @@ class LeakChecker
end
(h[fd] ||= []) << [io, autoclose, inspect]
}
- fd_leaked.each {|fd|
+ 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
}
- #system("lsof -p #$$") if !fd_leaked.empty?
+ unless fd_leaked.empty?
+ unless @@try_lsof == false
+ @@try_lsof |= system(*%W[lsof -w -a -d #{fd_leaked.minmax.uniq.join("-")} -p #$$], out: Test::Unit::Runner.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 object for a file descriptor:#{str}"
+ puts "Multiple autoclose IO objects for a file descriptor in: #{test_name}: #{str}"
end
}
end
@fd_info = live2
+ @@skip = false
return leaked
end
@@ -108,14 +136,14 @@ class LeakChecker
attr_accessor :count
end
- def new(data)
+ def new(...)
LeakChecker::TempfileCounter.count += 1
- super(data)
+ super
end
}
LeakChecker.const_set(:TempfileCounter, m)
- class << Tempfile::Remover
+ class << Tempfile
prepend LeakChecker::TempfileCounter
end
end
@@ -127,8 +155,8 @@ class LeakChecker
if prev_count == count
[prev_count, []]
else
- tempfiles = ObjectSpace.each_object(Tempfile).find_all {|t|
- t.instance_variable_defined?(:@tmpfile) and t.path
+ tempfiles = ObjectSpace.each_object(Tempfile).reject {|t|
+ t.instance_variables.empty? || t.closed?
}
[count, tempfiles]
end
@@ -154,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
@@ -181,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]
@@ -211,30 +261,61 @@ class LeakChecker
end
def find_encodings
- [Encoding.default_internal, Encoding.default_external]
+ {
+ '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_internal, old_external = @encoding_info
- new_internal, new_external = find_encodings
+ old_encoding_info = @encoding_info
+ @encoding_info = find_encodings
leaked = false
- if new_internal != old_internal
- leaked = true
- puts "Encoding.default_internal changed: #{test_name} : #{old_internal.inspect} to #{new_internal.inspect}"
+ @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
- if new_external != old_external
- leaked = true
- puts "Encoding.default_external changed: #{test_name} : #{old_external.inspect} to #{new_external.inspect}"
+ leaked
+ end
+
+ WARNING_CATEGORIES = (Warning.respond_to?(:[]) ? %i[deprecated experimental] : []).freeze
+
+ def find_warning_flags
+ WARNING_CATEGORIES.to_h do |category|
+ [category, Warning[category]]
+ end
+ end
+
+ def check_warning_flags(test_name)
+ new_warning_flags = find_warning_flags
+ 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
- @encoding_info = [new_internal, new_external]
return leaked
end
def puts(*a)
- output = MiniTest::Unit.output
+ output = Test::Unit::Runner.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/memory_status.rb b/tool/lib/memory_status.rb
index ad002b2dda..429e5f6a1d 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
@@ -20,47 +20,68 @@ module Memory
data.scan(pat) {|k, v| keys << k.downcase.intern}
when /mswin|mingw/ =~ RUBY_PLATFORM
- require 'fiddle/import'
- require 'fiddle/types'
-
- module Win32
- extend Fiddle::Importer
- dlload "kernel32.dll", "psapi.dll"
- include Fiddle::Win32Types
- typealias "SIZE_T", "size_t"
-
- PROCESS_MEMORY_COUNTERS = struct [
- "DWORD cb",
- "DWORD PageFaultCount",
- "SIZE_T PeakWorkingSetSize",
- "SIZE_T WorkingSetSize",
- "SIZE_T QuotaPeakPagedPoolUsage",
- "SIZE_T QuotaPagedPoolUsage",
- "SIZE_T QuotaPeakNonPagedPoolUsage",
- "SIZE_T QuotaNonPagedPoolUsage",
- "SIZE_T PagefileUsage",
- "SIZE_T PeakPagefileUsage",
- ]
-
- typealias "PPROCESS_MEMORY_COUNTERS", "PROCESS_MEMORY_COUNTERS*"
-
- extern "HANDLE GetCurrentProcess()", :stdcall
- extern "BOOL GetProcessMemoryInfo(HANDLE, PPROCESS_MEMORY_COUNTERS, DWORD)", :stdcall
-
- module_function
- def memory_info
- size = PROCESS_MEMORY_COUNTERS.size
- data = PROCESS_MEMORY_COUNTERS.malloc
- data.cb = size
- data if GetProcessMemoryInfo(GetCurrentProcess(), data, size)
+ keys.push(:size, :rss, :peak)
+
+ begin
+ require 'fiddle/import'
+ require 'fiddle/types'
+ rescue LoadError
+ # Fallback to PowerShell command to get memory information for current process
+ def self.read_status
+ cmd = [
+ "powershell.exe", "-NoProfile", "-Command",
+ "Get-Process -Id #{$$} | " \
+ "% { Write-Output $_.PagedMemorySize64 $_.WorkingSet64 $_.PeakWorkingSet64 }"
+ ]
+
+ IO.popen(cmd, "r", err: [:child, :out]) do |out|
+ if /^(\d+)\n(\d+)\n(\d+)$/ =~ out.read
+ yield :size, $1.to_i
+ yield :rss, $2.to_i
+ yield :peak, $3.to_i
+ end
+ end
+ end
+ else
+ module Win32
+ extend Fiddle::Importer
+ dlload "kernel32.dll", "psapi.dll"
+ include Fiddle::Win32Types
+ typealias "SIZE_T", "size_t"
+
+ PROCESS_MEMORY_COUNTERS = struct [
+ "DWORD cb",
+ "DWORD PageFaultCount",
+ "SIZE_T PeakWorkingSetSize",
+ "SIZE_T WorkingSetSize",
+ "SIZE_T QuotaPeakPagedPoolUsage",
+ "SIZE_T QuotaPagedPoolUsage",
+ "SIZE_T QuotaPeakNonPagedPoolUsage",
+ "SIZE_T QuotaNonPagedPoolUsage",
+ "SIZE_T PagefileUsage",
+ "SIZE_T PeakPagefileUsage",
+ ]
+
+ typealias "PPROCESS_MEMORY_COUNTERS", "PROCESS_MEMORY_COUNTERS*"
+
+ extern "HANDLE GetCurrentProcess()", :stdcall
+ extern "BOOL GetProcessMemoryInfo(HANDLE, PPROCESS_MEMORY_COUNTERS, DWORD)", :stdcall
+
+ module_function
+ def memory_info
+ size = PROCESS_MEMORY_COUNTERS.size
+ data = PROCESS_MEMORY_COUNTERS.malloc
+ data.cb = size
+ data if GetProcessMemoryInfo(GetCurrentProcess(), data, size)
+ end
end
- end
- keys << :peak << :size
- def self.read_status
- if info = Win32.memory_info
- yield :peak, info.PeakPagefileUsage
- yield :size, info.PagefileUsage
+ def self.read_status
+ if info = Win32.memory_info
+ yield :size, info.PagefileUsage
+ yield :rss, info.WorkingSetSize
+ yield :peak, info.PeakWorkingSetSize
+ end
end
end
when (require_relative 'find_executable'
@@ -94,6 +115,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/autorun.rb b/tool/lib/minitest/autorun.rb
deleted file mode 100644
index 844096623c..0000000000
--- a/tool/lib/minitest/autorun.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# encoding: utf-8
-# frozen_string_literal: true
-
-begin
- require 'rubygems'
- gem 'minitest'
-rescue Gem::LoadError
- # do nothing
-end
-
-require 'minitest/unit'
-require 'minitest/mock'
-
-MiniTest::Unit.autorun
diff --git a/tool/lib/minitest/benchmark.rb b/tool/lib/minitest/benchmark.rb
deleted file mode 100644
index b3f2bc28b3..0000000000
--- a/tool/lib/minitest/benchmark.rb
+++ /dev/null
@@ -1,418 +0,0 @@
-# encoding: utf-8
-# frozen_string_literal: true
-
-require 'minitest/unit'
-
-class MiniTest::Unit # :nodoc:
- def run_benchmarks # :nodoc:
- _run_anything :benchmark
- end
-
- def benchmark_suite_header suite # :nodoc:
- "\n#{suite}\t#{suite.bench_range.join("\t")}"
- end
-
- class TestCase
- ##
- # Returns a set of ranges stepped exponentially from +min+ to
- # +max+ by powers of +base+. Eg:
- #
- # bench_exp(2, 16, 2) # => [2, 4, 8, 16]
-
- def self.bench_exp min, max, base = 10
- min = (Math.log10(min) / Math.log10(base)).to_i
- max = (Math.log10(max) / Math.log10(base)).to_i
-
- (min..max).map { |m| base ** m }.to_a
- end
-
- ##
- # Returns a set of ranges stepped linearly from +min+ to +max+ by
- # +step+. Eg:
- #
- # bench_linear(20, 40, 10) # => [20, 30, 40]
-
- def self.bench_linear min, max, step = 10
- (min..max).step(step).to_a
- rescue LocalJumpError # 1.8.6
- r = []; (min..max).step(step) { |n| r << n }; r
- end
-
- ##
- # Returns the benchmark methods (methods that start with bench_)
- # for that class.
-
- def self.benchmark_methods # :nodoc:
- public_instance_methods(true).grep(/^bench_/).map { |m| m.to_s }.sort
- end
-
- ##
- # Returns all test suites that have benchmark methods.
-
- def self.benchmark_suites
- TestCase.test_suites.reject { |s| s.benchmark_methods.empty? }
- end
-
- ##
- # Specifies the ranges used for benchmarking for that class.
- # Defaults to exponential growth from 1 to 10k by powers of 10.
- # Override if you need different ranges for your benchmarks.
- #
- # See also: ::bench_exp and ::bench_linear.
-
- def self.bench_range
- bench_exp 1, 10_000
- end
-
- ##
- # Runs the given +work+, gathering the times of each run. Range
- # and times are then passed to a given +validation+ proc. Outputs
- # the benchmark name and times in tab-separated format, making it
- # easy to paste into a spreadsheet for graphing or further
- # analysis.
- #
- # Ranges are specified by ::bench_range.
- #
- # Eg:
- #
- # def bench_algorithm
- # validation = proc { |x, y| ... }
- # assert_performance validation do |n|
- # @obj.algorithm(n)
- # end
- # end
-
- def assert_performance validation, &work
- range = self.class.bench_range
-
- io.print "#{__name__}"
-
- times = []
-
- range.each do |x|
- GC.start
- t0 = Time.now
- instance_exec(x, &work)
- t = Time.now - t0
-
- io.print "\t%9.6f" % t
- times << t
- end
- io.puts
-
- validation[range, times]
- end
-
- ##
- # Runs the given +work+ and asserts that the times gathered fit to
- # match a constant rate (eg, linear slope == 0) within a given
- # +threshold+. Note: because we're testing for a slope of 0, R^2
- # is not a good determining factor for the fit, so the threshold
- # is applied against the slope itself. As such, you probably want
- # to tighten it from the default.
- #
- # See http://www.graphpad.com/curvefit/goodness_of_fit.htm for
- # more details.
- #
- # Fit is calculated by #fit_linear.
- #
- # Ranges are specified by ::bench_range.
- #
- # Eg:
- #
- # def bench_algorithm
- # assert_performance_constant 0.9999 do |n|
- # @obj.algorithm(n)
- # end
- # end
-
- def assert_performance_constant threshold = 0.99, &work
- validation = proc do |range, times|
- a, b, rr = fit_linear range, times
- assert_in_delta 0, b, 1 - threshold
- [a, b, rr]
- end
-
- assert_performance validation, &work
- end
-
- ##
- # Runs the given +work+ and asserts that the times gathered fit to
- # match a exponential curve within a given error +threshold+.
- #
- # Fit is calculated by #fit_exponential.
- #
- # Ranges are specified by ::bench_range.
- #
- # Eg:
- #
- # def bench_algorithm
- # assert_performance_exponential 0.9999 do |n|
- # @obj.algorithm(n)
- # end
- # end
-
- def assert_performance_exponential threshold = 0.99, &work
- assert_performance validation_for_fit(:exponential, threshold), &work
- end
-
- ##
- # Runs the given +work+ and asserts that the times gathered fit to
- # match a logarithmic curve within a given error +threshold+.
- #
- # Fit is calculated by #fit_logarithmic.
- #
- # Ranges are specified by ::bench_range.
- #
- # Eg:
- #
- # def bench_algorithm
- # assert_performance_logarithmic 0.9999 do |n|
- # @obj.algorithm(n)
- # end
- # end
-
- def assert_performance_logarithmic threshold = 0.99, &work
- assert_performance validation_for_fit(:logarithmic, threshold), &work
- end
-
- ##
- # Runs the given +work+ and asserts that the times gathered fit to
- # match a straight line within a given error +threshold+.
- #
- # Fit is calculated by #fit_linear.
- #
- # Ranges are specified by ::bench_range.
- #
- # Eg:
- #
- # def bench_algorithm
- # assert_performance_linear 0.9999 do |n|
- # @obj.algorithm(n)
- # end
- # end
-
- def assert_performance_linear threshold = 0.99, &work
- assert_performance validation_for_fit(:linear, threshold), &work
- end
-
- ##
- # Runs the given +work+ and asserts that the times gathered curve
- # fit to match a power curve within a given error +threshold+.
- #
- # Fit is calculated by #fit_power.
- #
- # Ranges are specified by ::bench_range.
- #
- # Eg:
- #
- # def bench_algorithm
- # assert_performance_power 0.9999 do |x|
- # @obj.algorithm
- # end
- # end
-
- def assert_performance_power threshold = 0.99, &work
- assert_performance validation_for_fit(:power, threshold), &work
- end
-
- ##
- # Takes an array of x/y pairs and calculates the general R^2 value.
- #
- # See: http://en.wikipedia.org/wiki/Coefficient_of_determination
-
- def fit_error xys
- y_bar = sigma(xys) { |x, y| y } / xys.size.to_f
- ss_tot = sigma(xys) { |x, y| (y - y_bar) ** 2 }
- ss_err = sigma(xys) { |x, y| (yield(x) - y) ** 2 }
-
- 1 - (ss_err / ss_tot)
- end
-
- ##
- # To fit a functional form: y = ae^(bx).
- #
- # Takes x and y values and returns [a, b, r^2].
- #
- # See: http://mathworld.wolfram.com/LeastSquaresFittingExponential.html
-
- def fit_exponential xs, ys
- n = xs.size
- xys = xs.zip(ys)
- sxlny = sigma(xys) { |x,y| x * Math.log(y) }
- slny = sigma(xys) { |x,y| Math.log(y) }
- sx2 = sigma(xys) { |x,y| x * x }
- sx = sigma xs
-
- c = n * sx2 - sx ** 2
- a = (slny * sx2 - sx * sxlny) / c
- b = ( n * sxlny - sx * slny ) / c
-
- return Math.exp(a), b, fit_error(xys) { |x| Math.exp(a + b * x) }
- end
-
- ##
- # To fit a functional form: y = a + b*ln(x).
- #
- # Takes x and y values and returns [a, b, r^2].
- #
- # See: http://mathworld.wolfram.com/LeastSquaresFittingLogarithmic.html
-
- def fit_logarithmic xs, ys
- n = xs.size
- xys = xs.zip(ys)
- slnx2 = sigma(xys) { |x,y| Math.log(x) ** 2 }
- slnx = sigma(xys) { |x,y| Math.log(x) }
- sylnx = sigma(xys) { |x,y| y * Math.log(x) }
- sy = sigma(xys) { |x,y| y }
-
- c = n * slnx2 - slnx ** 2
- b = ( n * sylnx - sy * slnx ) / c
- a = (sy - b * slnx) / n
-
- return a, b, fit_error(xys) { |x| a + b * Math.log(x) }
- end
-
-
- ##
- # Fits the functional form: a + bx.
- #
- # Takes x and y values and returns [a, b, r^2].
- #
- # See: http://mathworld.wolfram.com/LeastSquaresFitting.html
-
- def fit_linear xs, ys
- n = xs.size
- xys = xs.zip(ys)
- sx = sigma xs
- sy = sigma ys
- sx2 = sigma(xs) { |x| x ** 2 }
- sxy = sigma(xys) { |x,y| x * y }
-
- c = n * sx2 - sx**2
- a = (sy * sx2 - sx * sxy) / c
- b = ( n * sxy - sx * sy ) / c
-
- return a, b, fit_error(xys) { |x| a + b * x }
- end
-
- ##
- # To fit a functional form: y = ax^b.
- #
- # Takes x and y values and returns [a, b, r^2].
- #
- # See: http://mathworld.wolfram.com/LeastSquaresFittingPowerLaw.html
-
- def fit_power xs, ys
- n = xs.size
- xys = xs.zip(ys)
- slnxlny = sigma(xys) { |x, y| Math.log(x) * Math.log(y) }
- slnx = sigma(xs) { |x | Math.log(x) }
- slny = sigma(ys) { | y| Math.log(y) }
- slnx2 = sigma(xs) { |x | Math.log(x) ** 2 }
-
- b = (n * slnxlny - slnx * slny) / (n * slnx2 - slnx ** 2);
- a = (slny - b * slnx) / n
-
- return Math.exp(a), b, fit_error(xys) { |x| (Math.exp(a) * (x ** b)) }
- end
-
- ##
- # Enumerates over +enum+ mapping +block+ if given, returning the
- # sum of the result. Eg:
- #
- # sigma([1, 2, 3]) # => 1 + 2 + 3 => 7
- # sigma([1, 2, 3]) { |n| n ** 2 } # => 1 + 4 + 9 => 14
-
- def sigma enum, &block
- enum = enum.map(&block) if block
- enum.inject { |sum, n| sum + n }
- end
-
- ##
- # Returns a proc that calls the specified fit method and asserts
- # that the error is within a tolerable threshold.
-
- def validation_for_fit msg, threshold
- proc do |range, times|
- a, b, rr = send "fit_#{msg}", range, times
- assert_operator rr, :>=, threshold
- [a, b, rr]
- end
- end
- end
-end
-
-class MiniTest::Spec
- ##
- # This is used to define a new benchmark method. You usually don't
- # use this directly and is intended for those needing to write new
- # performance curve fits (eg: you need a specific polynomial fit).
- #
- # See ::bench_performance_linear for an example of how to use this.
-
- def self.bench name, &block
- define_method "bench_#{name.gsub(/\W+/, '_')}", &block
- end
-
- ##
- # Specifies the ranges used for benchmarking for that class.
- #
- # bench_range do
- # bench_exp(2, 16, 2)
- # end
- #
- # See Unit::TestCase.bench_range for more details.
-
- def self.bench_range &block
- return super unless block
-
- meta = (class << self; self; end)
- meta.send :define_method, "bench_range", &block
- end
-
- ##
- # Create a benchmark that verifies that the performance is linear.
- #
- # describe "my class" do
- # bench_performance_linear "fast_algorithm", 0.9999 do |n|
- # @obj.fast_algorithm(n)
- # end
- # end
-
- def self.bench_performance_linear name, threshold = 0.99, &work
- bench name do
- assert_performance_linear threshold, &work
- end
- end
-
- ##
- # Create a benchmark that verifies that the performance is constant.
- #
- # describe "my class" do
- # bench_performance_constant "zoom_algorithm!" do |n|
- # @obj.zoom_algorithm!(n)
- # end
- # end
-
- def self.bench_performance_constant name, threshold = 0.99, &work
- bench name do
- assert_performance_constant threshold, &work
- end
- end
-
- ##
- # Create a benchmark that verifies that the performance is exponential.
- #
- # describe "my class" do
- # bench_performance_exponential "algorithm" do |n|
- # @obj.algorithm(n)
- # end
- # end
-
- def self.bench_performance_exponential name, threshold = 0.99, &work
- bench name do
- assert_performance_exponential threshold, &work
- end
- end
-end
diff --git a/tool/lib/minitest/mock.rb b/tool/lib/minitest/mock.rb
deleted file mode 100644
index 224b06cb89..0000000000
--- a/tool/lib/minitest/mock.rb
+++ /dev/null
@@ -1,196 +0,0 @@
-# encoding: utf-8
-# frozen_string_literal: true
-
-class MockExpectationError < StandardError; end # :nodoc:
-
-##
-# A simple and clean mock object framework.
-
-module MiniTest # :nodoc:
-
- ##
- # All mock objects are an instance of Mock
-
- class Mock
- alias :__respond_to? :respond_to?
-
- skip_methods = %w(object_id respond_to_missing? inspect === to_s)
-
- instance_methods.each do |m|
- undef_method m unless skip_methods.include?(m.to_s) || m =~ /^__/
- end
-
- def initialize # :nodoc:
- @expected_calls = Hash.new { |calls, name| calls[name] = [] }
- @actual_calls = Hash.new { |calls, name| calls[name] = [] }
- end
-
- ##
- # Expect that method +name+ is called, optionally with +args+ or a
- # +blk+, and returns +retval+.
- #
- # @mock.expect(:meaning_of_life, 42)
- # @mock.meaning_of_life # => 42
- #
- # @mock.expect(:do_something_with, true, [some_obj, true])
- # @mock.do_something_with(some_obj, true) # => true
- #
- # @mock.expect(:do_something_else, true) do |a1, a2|
- # a1 == "buggs" && a2 == :bunny
- # end
- #
- # +args+ is compared to the expected args using case equality (ie, the
- # '===' operator), allowing for less specific expectations.
- #
- # @mock.expect(:uses_any_string, true, [String])
- # @mock.uses_any_string("foo") # => true
- # @mock.verify # => true
- #
- # @mock.expect(:uses_one_string, true, ["foo"]
- # @mock.uses_one_string("bar") # => true
- # @mock.verify # => raises MockExpectationError
-
- def expect(name, retval, args=[], &blk)
- if block_given?
- raise ArgumentError, "args ignored when block given" unless args.empty?
- @expected_calls[name] << { :retval => retval, :block => blk }
- else
- raise ArgumentError, "args must be an array" unless Array === args
- @expected_calls[name] << { :retval => retval, :args => args }
- end
- self
- end
-
- def __call name, data # :nodoc:
- case data
- when Hash then
- "#{name}(#{data[:args].inspect[1..-2]}) => #{data[:retval].inspect}"
- else
- data.map { |d| __call name, d }.join ", "
- end
- end
-
- ##
- # Verify that all methods were called as expected. Raises
- # +MockExpectationError+ if the mock object was not called as
- # expected.
-
- def verify
- @expected_calls.each do |name, calls|
- calls.each do |expected|
- msg1 = "expected #{__call name, expected}"
- msg2 = "#{msg1}, got [#{__call name, @actual_calls[name]}]"
-
- raise MockExpectationError, msg2 if
- @actual_calls.has_key?(name) and
- not @actual_calls[name].include?(expected)
-
- raise MockExpectationError, msg1 unless
- @actual_calls.has_key?(name) and
- @actual_calls[name].include?(expected)
- end
- end
- true
- end
-
- def method_missing(sym, *args) # :nodoc:
- unless @expected_calls.has_key?(sym) then
- raise NoMethodError, "unmocked method %p, expected one of %p" %
- [sym, @expected_calls.keys.sort_by(&:to_s)]
- end
-
- index = @actual_calls[sym].length
- expected_call = @expected_calls[sym][index]
-
- unless expected_call then
- raise MockExpectationError, "No more expects available for %p: %p" %
- [sym, args]
- end
-
- expected_args, retval, val_block =
- expected_call.values_at(:args, :retval, :block)
-
- if val_block then
- raise MockExpectationError, "mocked method %p failed block w/ %p" %
- [sym, args] unless val_block.call(args)
-
- # keep "verify" happy
- @actual_calls[sym] << expected_call
- return retval
- end
-
- if expected_args.size != args.size then
- raise ArgumentError, "mocked method %p expects %d arguments, got %d" %
- [sym, expected_args.size, args.size]
- end
-
- fully_matched = expected_args.zip(args).all? { |mod, a|
- mod === a or mod == a
- }
-
- unless fully_matched then
- raise MockExpectationError, "mocked method %p called with unexpected arguments %p" %
- [sym, args]
- end
-
- @actual_calls[sym] << {
- :retval => retval,
- :args => expected_args.zip(args).map { |mod, a| mod === a ? mod : a }
- }
-
- retval
- end
-
- def respond_to?(sym, include_private = false) # :nodoc:
- return true if @expected_calls.has_key?(sym.to_sym)
- return __respond_to?(sym, include_private)
- end
- end
-end
-
-class Object # :nodoc:
-
- ##
- # Add a temporary stubbed method replacing +name+ for the duration
- # of the +block+. If +val_or_callable+ responds to #call, then it
- # returns the result of calling it, otherwise returns the value
- # as-is. Cleans up the stub at the end of the +block+. The method
- # +name+ must exist before stubbing.
- #
- # def test_stale_eh
- # obj_under_test = Something.new
- # refute obj_under_test.stale?
- #
- # Time.stub :now, Time.at(0) do
- # assert obj_under_test.stale?
- # end
- # end
-
- def stub name, val_or_callable, &block
- new_name = "__minitest_stub__#{name}"
-
- metaclass = class << self; self; end
-
- if respond_to? name and not methods.map(&:to_s).include? name.to_s then
- metaclass.send :define_method, name do |*args|
- super(*args)
- end
- end
-
- metaclass.send :alias_method, new_name, name
-
- metaclass.send :define_method, name do |*args|
- if val_or_callable.respond_to? :call then
- val_or_callable.call(*args)
- else
- val_or_callable
- end
- end
-
- yield self
- ensure
- metaclass.send :undef_method, name
- metaclass.send :alias_method, name, new_name
- metaclass.send :undef_method, new_name
- end
-end
diff --git a/tool/lib/minitest/unit.rb b/tool/lib/minitest/unit.rb
deleted file mode 100644
index d922a5fc41..0000000000
--- a/tool/lib/minitest/unit.rb
+++ /dev/null
@@ -1,1420 +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
-
- def _assertions # :nodoc:
- @_assertions ||= 0
- 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 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
-
- ##
- # 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
-
- ##
- # 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
-
- ##
- # 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 ||= {}
- end
-
- @@installed_at_exit ||= false
- @@out = $stdout
- @@after_tests = []
-
- ##
- # 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 _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
-
- 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
- count += 1
- unless @repeat_count
- puts
- puts
- end
- puts "Finished%s %ss in %.6fs, %.4f tests/s, %.4f assertions/s.\n" %
- [(@repeat_count ? "(#{count}/#{@repeat_count}) " : ""), type,
- t, @test_count.fdiv(t), @assertion_count.fdiv(t)]
- end while @repeat_count && 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
-
- 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
-
- ##
- # 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 = ""
- 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 = ""
- 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:
- :random
- end
-
- def self.test_suites # :nodoc:
- @@test_suites.keys.sort_by { |ts| ts.name.to_s }
- 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
- 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..f16a164338
--- /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, real_src, *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, real_src)
+ ln_safe(src, dest, "/d")
+ end
+ end
+
+ module HardlinkExcutable
+ def ln_exe(relative_src, dest, src)
+ ln(src, dest, force: true)
+ end
+ end
+
+ def ln_safe(src, dest, real_src)
+ 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?(real_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(relative(src, parent), dest, src) if File.exist?(src))
+ end
+ clean_link(relative(src, parent), dest) {|s, d| ln_safe(s, d, src)}
+ 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, src)}
+ 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/jobserver.rb b/tool/lib/test/jobserver.rb
new file mode 100644
index 0000000000..7b889163b0
--- /dev/null
+++ b/tool/lib/test/jobserver.rb
@@ -0,0 +1,47 @@
+module Test
+ module JobServer
+ end
+end
+
+class << Test::JobServer
+ def connect(makeflags = ENV["MAKEFLAGS"])
+ return unless /(?:\A|\s)--jobserver-(?:auth|fds)=(?:(\d+),(\d+)|fifo:((?:\\.|\S)+))/ =~ makeflags
+ begin
+ 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
+ nil
+ else
+ return r, w
+ end
+ end
+
+ def acquire_possible(r, w, max)
+ return unless tokens = r.read_nonblock(max - 1, exception: false)
+ if (jobs = tokens.size) > 0
+ jobserver, w = w, nil
+ at_exit do
+ jobserver.print(tokens)
+ jobserver.close
+ end
+ end
+ return jobs + 1
+ rescue Errno::EBADF
+ ensure
+ r&.close
+ w&.close
+ end
+
+ def max_jobs(max = 2, makeflags = ENV["MAKEFLAGS"])
+ if max > 1 and (r, w = connect(makeflags))
+ acquire_possible(r, w, max)
+ end
+ end
+end
diff --git a/tool/lib/test/unit.rb b/tool/lib/test/unit.rb
index d237a9a0e9..2663b7b76a 100644
--- a/tool/lib/test/unit.rb
+++ b/tool/lib/test/unit.rb
@@ -1,26 +1,138 @@
# frozen_string_literal: true
-begin
- gem 'minitest', '< 5.0.0' if defined? Gem
-rescue Gem::LoadError
+
+# 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 'minitest/unit'
-require 'test/unit/assertions'
+
require_relative '../envutil'
require_relative '../colorize'
-require 'test/unit/testcase'
+require_relative '../leakchecker'
+require_relative '../test/unit/testcase'
+require_relative '../test/jobserver'
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
+
+ class << self
+ ##
+ # Extract the location where the last assertion method was
+ # called. Returns "<empty>" if _e_ does not have backtrace, or
+ # an empty string if no assertion method location was found.
+
+ def location e
+ last_before_assertion = nil
+
+ return '<empty>' unless e&.backtrace # SystemStackError can return nil.
+
+ e.backtrace.reverse_each do |s|
+ break if s =~ /:in \W(?:.*\#)?(?:assert|refute|flunk|pass|fail|raise|must|wont)/
+ last_before_assertion = s
+ end
+ return "" unless last_before_assertion
+ /:in / =~ last_before_assertion ? $` : last_before_assertion
+ end
+ 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
@@ -64,14 +176,28 @@ module Test
args = @init_hook.call(args, options) if @init_hook
non_options(args, options)
@run_options = orig_args
- @help = orig_args.map { |s| s =~ /[\s|&<>$()]/ ? s.inspect : s }.join " "
+
+ order = options[:test_order]
+ if seed = options[:seed]
+ order ||= :random
+ elsif (order ||= :random) == :random
+ seed = options[:seed] = rand(0x10000)
+ orig_args.unshift "--seed=#{seed}"
+ end
+ 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
@@ -79,7 +205,7 @@ module Test
end
opts.on '-s', '--seed SEED', Integer, "Sets random seed" do |m|
- options[:seed] = m
+ options[:seed] = m.to_i
end
opts.on '-v', '--verbose', "Verbose. Show progress processing files." do
@@ -91,8 +217,9 @@ module Test
(options[:filter] ||= []) << a
end
- opts.on '--test-order=random|alpha|sorted', [:random, :alpha, :sorted] do |a|
- MiniTest::Unit::TestCase.test_order = a
+ orders = Test::Unit::Order::Types.keys
+ opts.on "--test-order=#{orders.join('|')}", orders do |a|
+ options[:test_order] = a
end
end
@@ -106,6 +233,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
@@ -113,12 +243,6 @@ module Test
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
@@ -126,6 +250,8 @@ module Test
end
module Parallel # :nodoc: all
+ attr_accessor :prefix
+
def process_args(args = [])
return @options if @options
options = super
@@ -137,22 +263,10 @@ module Test
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
+ if !options[:parallel] and @jobserver = Test::JobServer.connect(ENV.delete("MAKEFLAGS"))
+ options[:parallel] ||= 256 # number of tokens to acquire first
end
+ @worker_timeout = EnvUtil.apply_timeout_scale(options[:worker_timeout] || 1200)
super
end
@@ -168,13 +282,17 @@ module Test
opts.separator "parallel test options:"
- options[:retry] = true
+ options[:retry] = false
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 '--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
@@ -188,24 +306,35 @@ 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
+
+ opts.on '--timetable-data=FILE', "Path to timetable data" do |a|
+ options[:timetable_data] = a
+ end
end
class Worker
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)
end
attr_reader :quit_called
+ attr_accessor :start_time
+ attr_accessor :response_at
+ attr_accessor :current
+
+ @@worker_number = 0
def initialize(io, pid, status)
+ @num = (@@worker_number += 1)
@io = io
@pid = pid
@status = status
@@ -214,20 +343,31 @@ module Test
@loadpath = []
@hooks = {}
@quit_called = false
+ @response_at = nil
+ end
+
+ def name
+ "Worker #{@num}"
end
def puts(*args)
@io.puts(*args)
end
- def run(task,type)
- @file = File.basename(task, ".rb")
+ def run(task, type, base = nil)
+ if base
+ @file = task.delete_prefix(base).chomp(".rb")
+ else
+ @file = File.basename(task, ".rb")
+ end
@real_file = task
begin
puts "loadpath #{[Marshal.dump($:-@loadpath)].pack("m0")}"
@loadpath = $:.dup
puts "run #{task} #{type}"
@status = :prepare
+ @start_time = Time.now
+ @response_at = @start_time
rescue Errno::EPIPE
died
rescue IOError
@@ -244,6 +384,7 @@ module Test
def read
res = (@status == :quit) ? @io.read : @io.gets
+ @response_at = Time.now
res && res.chomp
end
@@ -253,16 +394,19 @@ module Test
rescue IOError
end
- def quit
+ def quit(reason = :normal)
return if @io.closed?
@quit_called = true
- @io.puts "quit"
+ @io.puts "quit #{reason}"
rescue Errno::EPIPE => e
warn "#{@pid}:#{@status.to_s.ljust(7)}:#{@file}: #{e.message}"
end
def kill
- Process.kill(:KILL, @pid)
+ EnvUtil::Debugger.search&.dump(@pid)
+ signal = RUBY_PLATFORM =~ /mswin|mingw/ ? :KILL : :SEGV
+ Process.kill(signal, @pid)
+ warn "worker #{to_s} does not respond; #{signal} is sent"
rescue Errno::ESRCH
end
@@ -316,10 +460,26 @@ 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')
+ require 'fileutils'
+ require 'time'
+ Dir.glob('/tmp/test-unit-core.*').each do |f|
+ if Time.now - File.mtime(f) > 7 * 24 * 60 * 60 # 7 days
+ warn "Deleting an old core file: #{f}"
+ FileUtils.rm(f)
+ end
+ end
+ core_path = "/tmp/test-unit-core.#{Time.now.utc.iso8601}"
+ warn "A core file is found. Saving it at: #{core_path.dump}"
+ FileUtils.mv('core', core_path)
+ cmd = ['gdb', RbConfig.ruby, '-c', core_path, '-ex', 'bt', '-batch']
+ p cmd # debugging why it's not working
+ system(*cmd)
+ end
STDERR.flush
exit c
end
@@ -358,30 +518,45 @@ 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
+ Timeout.timeout(5) do
+ worker.quit(cond ? :timeout : :normal)
end
rescue Errno::EPIPE
rescue Timeout::Error
end
- worker.close
+ closed&.push worker
+ begin
+ Timeout.timeout(1) 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(1 * 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)
@@ -393,6 +568,7 @@ module Test
worker = @workers_hash[io]
cmd = worker.read
cmd.sub!(/\A\.+/, '') if cmd # read may return nil
+
case cmd
when ''
# just only dots, ignore
@@ -410,38 +586,49 @@ module Test
worker.quit
worker = launch_worker
end
- worker.run(task, type)
+ worker.run(task, type, (@prefix unless @options[:job_status] == :replace))
@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]
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])
+ r = Marshal.load($1.unpack1("m"))
+
+ suite = r.first
+ key = [worker.name, suite]
+ if @records[key]
+ @records[key][1] = worker.start_time = Time.now
+ else
+ @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
@@ -455,6 +642,8 @@ module Test
end
def _run_parallel suites, type, result
+ @records = {}
+
if @options[:parallel] < 1
warn "Error: parameter of -j option should be greater than 0."
return
@@ -462,7 +651,8 @@ module Test
# Require needed thing for parallel running
require 'timeout'
- @tasks = @files.dup # Array of filenames.
+ @tasks = @order.group(@order.sort_by_string(@files)) # Array of filenames.
+
@need_quit = false
@dead_workers = [] # Array of dead workers.
@warnings = []
@@ -474,26 +664,48 @@ 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
+
+ timeout = [(@workers.filter_map {|w| w.response_at}.min&.-(Time.now) || 0), 0].max + @worker_timeout
- while _io = IO.select(@ios)[0]
- break if _io.any? do |io|
+ 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
@interrupt = ex
return result
ensure
+ if file = @options[:timetable_data]
+ 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(", ") + '],'
+ }
+ }
+ end
+
if @interrupt
@ios.select!{|x| @workers_hash[x].status == :running }
while !@ios.empty? && (__io = IO.select(@ios,[],[],10))
@@ -507,14 +719,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.map {|r| r[:file]}.uniq.each {|file| require file}
- suites.map! {|r| eval("::"+r[:testcase])}
+ 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}
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]
@@ -522,20 +770,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 ""
@@ -605,7 +861,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]
@@ -684,7 +940,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
@@ -703,7 +959,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
@@ -719,11 +975,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
@@ -761,7 +1020,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
@@ -817,7 +1076,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)
@@ -856,7 +1115,8 @@ module Test
def setup_options(parser, options)
super
parser.separator "globbing options:"
- parser.on '-b', '--basedir=DIR', 'Base directory of test suites.' do |dir|
+ parser.on '-B', '--base-directory DIR', 'Base directory to glob.' do |dir|
+ raise OptionParser::InvalidArgument, "not a directory: #{dir}" unless File.directory?(dir)
options[:base_directory] = dir
end
parser.on '-x', '--exclude REGEXP', 'Exclude test files on pattern.' do |pattern|
@@ -864,6 +1124,18 @@ module Test
end
end
+ def complement_test_name f, orig_f
+ basename = File.basename(f)
+
+ if /\.rb\z/ !~ basename
+ return File.join(File.dirname(f), basename+'.rb')
+ elsif /\Atest_/ !~ basename
+ return File.join(File.dirname(f), 'test_'+basename)
+ end if f.end_with?(basename) # otherwise basename is dirname/
+
+ raise ArgumentError, "file not found: #{orig_f}"
+ end
+
def non_options(files, options)
paths = [options.delete(:base_directory), nil].uniq
if reject = options.delete(:reject)
@@ -871,6 +1143,7 @@ module Test
end
files.map! {|f|
f = f.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR
+ orig_f = f
while true
ret = ((paths if /\A\.\.?(?:\z|\/)/ !~ f) || [nil]).any? do |prefix|
if prefix
@@ -897,11 +1170,7 @@ module Test
end
end
if !ret
- if /\.rb\z/ =~ f
- raise ArgumentError, "file not found: #{f}"
- else
- f = "#{f}.rb"
- end
+ f = complement_test_name(f, orig_f)
else
break ret
end
@@ -912,29 +1181,66 @@ module Test
end
end
- module GCStressOption # :nodoc: all
+ 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
parser.separator "GC options:"
parser.on '--[no-]gc-stress', 'Set GC.stress as true' do |flag|
options[:gc_stress] = flag
end
+ parser.on '--[no-]gc-compact', 'GC.compact every time' do |flag|
+ options[:gc_compact] = flag
+ end
end
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
gc_stress, GC.stress = GC.stress, true
- oldrun.bind(self).call(runner)
+ oldrun.bind_call(self, runner)
ensure
GC.stress = gc_stress
end
end
end
end
+ if options.delete(:gc_compact)
+ Test::Unit::TestCase.class_eval do
+ oldrun = instance_method(:run)
+ define_method(:run) do |runner|
+ begin
+ oldrun.bind_call(self, runner)
+ ensure
+ GC.compact
+ end
+ end
+ end
+ end
super
end
end
@@ -958,8 +1264,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
@@ -970,10 +1281,15 @@ module Test
parser.on '--repeat-count=NUM', "Number of times to repeat", Integer do |n|
options[:repeat_count] = n
end
+ options[:keep_repeating] = false
+ parser.on '--[no-]keep-repeating', "Keep repeating even failed" do |n|
+ options[:keep_repeating] = true
+ end
end
def _run_anything(type)
@repeat_count = @options[:repeat_count]
+ @keep_repeating = @options[:keep_repeating]
super
end
end
@@ -1061,21 +1377,425 @@ 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::GCStressOption
- include Test::Unit::ExcludesOption
- include Test::Unit::TimeoutOption
- include Test::Unit::RunCount
-
- class << self; undef autorun; end
+ 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_relative '../launchable'
+ options[:launchable_test_reports] = writer = Launchable::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
+ 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 &&
+ (@keep_repeating || 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
+
+ leakchecker.check("#{inst.class}\##{inst.__name__}")
+
+ _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:
+ Test::Unit.location e
+ 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
@@stop_auto_run = false
def self.autorun
@@ -1087,16 +1807,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
@@ -1107,6 +1841,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
@@ -1114,10 +1849,12 @@ module Test
def initialize(force_standalone = false, default_dir = nil, argv = ARGV)
@force_standalone = force_standalone
@runner = Runner.new do |files, options|
- options[:base_directory] ||= default_dir
+ base = options[:base_directory] ||= default_dir
+ @runner.prefix = base ? (base + "/") : nil
files << default_dir if files.empty? and default_dir
@to_run = files
yield self if block_given?
+ $LOAD_PATH.unshift base if base
files
end
Runner.runner = @runner
@@ -1156,30 +1893,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 0c68a93b02..19581fc3ab 100644
--- a/tool/lib/test/unit/assertions.rb
+++ b/tool/lib/test/unit/assertions.rb
@@ -1,196 +1,566 @@
# frozen_string_literal: true
-require 'minitest/unit'
-require 'test/unit/core_assertions'
require 'pp'
module Test
module Unit
module Assertions
- include Test::Unit::CoreAssertions
- MINI_DIR = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), "minitest") #:nodoc:
+ ##
+ # Returns the diff command to use in #diff. Tries to intelligently
+ # figure out what diff to use.
- # :call-seq:
- # assert(test, [failure_message])
+ 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.
#
- #Tests if +test+ is true.
+ # 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.
#
- #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used
- #as the failure message. Otherwise, the result of calling +msg+ will be
- #used as the message if the assertion fails.
+ # 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.
#
- #If no +msg+ is given, a default message will be used.
+ # NOTE: this uses #capture_io, not #capture_subprocess_io.
#
- # assert(false, "This was expected to be true")
- def assert(test, *msgs)
- case msg = msgs.first
- when String, Proc
- when nil
- msgs.shift
- else
- bt = caller.reject { |s| s.start_with?(MINI_DIR) }
- raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt
- end unless msgs.empty?
- super
+ # 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
- # :call-seq:
- # assert_block( failure_message = nil )
+ ##
+ # For testing with predicates.
#
- #Tests the result of the given block. If the block does not return true,
- #the assertion will fail. The optional +failure_message+ argument is the same as in
- #Assertions#assert.
+ # assert_predicate str, :empty?
#
- # assert_block do
- # [1, 2, 3].any? { |num| num < 1 }
- # end
- def assert_block(*msgs)
- assert yield, *msgs
+ # 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
- # :call-seq:
- # assert_raise( *args, &block )
+ ##
+ # 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.
#
- #Tests if the given block raises an exception. Acceptable exception
- #types may be given as optional arguments. If the last argument is a
- #String, it will be used as the error message.
+ # 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:
#
- # assert_raise do #Fails, no Exceptions are raised
- # end
+ # out, err = capture_output do
+ # puts "Some info"
+ # warn "You did a bad thing"
+ # end
#
- # assert_raise NameError do
- # puts x #Raises NameError, so assertion succeeds
- # end
- def assert_raise(*exp, &b)
- case exp.last
- when String, Proc
- msg = exp.pop
- end
+ # assert_match %r%info%, out
+ # assert_match %r%bad%, err
- 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
- }
+ def capture_output
+ require 'stringio'
- assert expected, proc {
- exception_details(e, message(msg) {"#{mu_pp(exp)} exception expected, not"}.call)
- }
+ captured_stdout, captured_stderr = StringIO.new, StringIO.new
- return e
- ensure
- unless e
- exp = exp.first if exp.size == 1
+ synchronize do
+ orig_stdout, orig_stderr = $stdout, $stderr
+ $stdout, $stderr = captured_stdout, captured_stderr
- flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"})
+ begin
+ yield
+ ensure
+ $stdout = orig_stdout
+ $stderr = orig_stderr
end
end
+
+ return captured_stdout.string, captured_stderr.string
end
- def assert_raises(*exp, &b)
- raise NoMethodError, "use assert_raise", caller
+ def capture_io
+ raise NoMethodError, "use capture_output"
end
- # :call-seq:
- # assert_raise_with_message(exception, expected, msg = nil, &block)
+ ##
+ # 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>.
#
- #Tests if the given block raises an exception with the expected
- #message.
+ # 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+.
#
- # assert_raise_with_message(RuntimeError, "foo") do
- # nil #Fails, no Exceptions are raised
- # end
+ # 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:
#
- # assert_raise_with_message(RuntimeError, "foo") do
- # raise ArgumentError, "foo" #Fails, different Exception is raised
- # end
+ # 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.
#
- # assert_raise_with_message(RuntimeError, "foo") do
- # raise "bar" #Fails, RuntimeError is raised but the message differs
- # end
+ # refute_predicate str, :empty?
#
- # assert_raise_with_message(RuntimeError, "foo") do
- # raise "foo" #Raises RuntimeError with the message, so assertion succeeds
- # end
- def assert_raise_with_message(exception, expected, msg = nil, &block)
- case expected
- when String
- assert = :assert_equal
- when Regexp
- assert = :assert_match
- else
- raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}"
- end
+ # This is really meant for specs and is front-ended by refute_operator:
+ #
+ # str.wont_be :empty?
- ex = m = nil
- EnvUtil.with_default_internal(expected.encoding) do
- ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do
- yield
- end
- m = ex.message
- end
- msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"}
+ def refute_predicate o1, op, msg = nil
+ msg = message(msg) { "Expected #{mu_pp(o1)} to not be #{op}" }
+ refute o1.__send__(op), msg
+ end
- if assert == :assert_equal
- assert_equal(expected, m, msg)
- else
- msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" }
- assert expected =~ m, msg
- block.binding.eval("proc{|_|$~=_}").call($~)
+ ##
+ # 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
- ex
end
# :call-seq:
- # assert_nothing_raised( *args, &block )
- #
- #If any exceptions are given as arguments, the assertion will
- #fail if one of those exceptions are raised. Otherwise, the test fails
- #if any exceptions are raised.
- #
- #The final argument may be a failure message.
+ # assert_block( failure_message = nil )
#
- # assert_nothing_raised RuntimeError do
- # raise Exception #Assertion passes, Exception is not a RuntimeError
- # end
+ #Tests the result of the given block. If the block does not return true,
+ #the assertion will fail. The optional +failure_message+ argument is the same as in
+ #Assertions#assert.
#
- # assert_nothing_raised do
- # raise Exception #Assertion fails
+ # assert_block do
+ # [1, 2, 3].any? { |num| num < 1 }
# end
- def assert_nothing_raised(*args)
- self._assertions += 1
- if Module === args.last
- msg = nil
- else
- msg = args.pop
- end
- begin
- line = __LINE__; yield
- rescue MiniTest::Skip
- 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)}>" }
- raise MiniTest::Assertion, msg.call, bt
- else
- raise
- end
- end
+ def assert_block(*msgs)
+ assert yield, *msgs
end
# :call-seq:
@@ -217,35 +587,6 @@ module Test
end
# :call-seq:
- # assert_throw( tag, failure_message = nil, &block )
- #
- #Fails unless the given block throws +tag+, returns the caught
- #value otherwise.
- #
- #An optional failure message may be provided as the final argument.
- #
- # tag = Object.new
- # assert_throw(tag, "#{tag} was not thrown!") do
- # throw tag
- # end
- def assert_throw(tag, msg = nil)
- ret = catch(tag) do
- begin
- yield(tag)
- rescue UncaughtThrowError => e
- thrown = e.tag
- end
- msg = message(msg) {
- "Expected #{mu_pp(tag)} to have been thrown"\
- "#{%Q[, not #{thrown}] if thrown}"
- }
- assert(false, msg)
- end
- assert(true)
- ret
- end
-
- # :call-seq:
# assert_equal( expected, actual, failure_message = nil )
#
#Tests if +expected+ is equal to +actual+.
@@ -352,52 +693,6 @@ EOT
end
# :call-seq:
- # assert_respond_to( object, method, failure_message = nil )
- #
- #Tests if the given Object responds to +method+.
- #
- #An optional failure message may be provided as the final argument.
- #
- # assert_respond_to("hello", :reverse) #Succeeds
- # assert_respond_to("hello", :does_not_exist) #Fails
- def assert_respond_to(obj, (meth, *priv), msg = nil)
- unless priv.empty?
- msg = message(msg) {
- "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}"
- }
- return assert obj.respond_to?(meth, *priv), msg
- end
- #get rid of overcounting
- if caller_locations(1, 1)[0].path.start_with?(MINI_DIR)
- return if obj.respond_to?(meth)
- end
- super(obj, meth, msg)
- end
-
- # :call-seq:
- # assert_not_respond_to( object, method, failure_message = nil )
- #
- #Tests if the given Object does not respond to +method+.
- #
- #An optional failure message may be provided as the final argument.
- #
- # assert_not_respond_to("hello", :reverse) #Fails
- # assert_not_respond_to("hello", :does_not_exist) #Succeeds
- def assert_not_respond_to(obj, (meth, *priv), msg = nil)
- unless priv.empty?
- msg = message(msg) {
- "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}"
- }
- return assert !obj.respond_to?(meth, *priv), msg
- end
- #get rid of overcounting
- if caller_locations(1, 1)[0].path.start_with?(MINI_DIR)
- return unless obj.respond_to?(meth)
- end
- refute_respond_to(obj, meth, msg)
- end
-
- # :call-seq:
# assert_send( +send_array+, failure_message = nil )
#
# Passes if the method send returns a true value.
@@ -457,16 +752,6 @@ EOT
alias assert_include assert_includes
alias assert_not_include assert_not_includes
- def assert_all?(obj, m = nil, &blk)
- failed = []
- obj.each do |*a, &b|
- unless blk.call(*a, &b)
- failed << (a.size > 1 ? a : a[0])
- end
- end
- assert(failed.empty?, message(m) {failed.pretty_inspect})
- end
-
def assert_not_all?(obj, m = nil, &blk)
failed = []
obj.each do |*a, &b|
@@ -477,102 +762,22 @@ EOT
assert(failed.empty?, message(m) {failed.pretty_inspect})
end
- # compatibility with test-unit
- alias pend skip
-
- if defined?(RubyVM::InstructionSequence)
- def syntax_check(code, fname, line)
- code = code.dup.force_encoding(Encoding::UTF_8)
- RubyVM::InstructionSequence.compile(code, fname, fname, line)
- :ok
- end
- else
- def syntax_check(code, fname, line)
- code = code.b
- code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) {
- "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n"
- }
- code = code.force_encoding(Encoding::UTF_8)
- catch {|tag| eval(code, binding, fname, line - 1)}
- end
- end
-
- def prepare_syntax_check(code, fname = nil, mesg = nil, verbose: nil)
- fname ||= caller_locations(2, 1)[0]
- mesg ||= fname.to_s
- verbose, $VERBOSE = $VERBOSE, verbose
- case
- when Array === fname
- fname, line = *fname
- when defined?(fname.path) && defined?(fname.lineno)
- fname, line = fname.path, fname.lineno
- else
- line = 1
- end
- yield(code, fname, line, message(mesg) {
- if code.end_with?("\n")
- "```\n#{code}```\n"
- else
- "```\n#{code}\n```\n""no-newline"
- end
- })
- ensure
- $VERBOSE = verbose
- end
-
- def assert_valid_syntax(code, *args, **opt)
+ def assert_syntax_error(code, error, *args, **opt)
prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg|
yield if defined?(yield)
- assert_nothing_raised(SyntaxError, mesg) do
- assert_equal(:ok, syntax_check(src, fname, line), mesg)
- end
- end
- end
-
- def assert_syntax_error(code, error, *args)
- prepare_syntax_check(code, *args) do |src, fname, line, mesg|
- yield if defined?(yield)
e = assert_raise(SyntaxError, mesg) do
syntax_check(src, fname, line)
end
- assert_match(error, e.message, mesg)
- e
- end
- end
-
- def assert_normal_exit(testsrc, message = '', child_env: nil, **opt)
- assert_valid_syntax(testsrc, caller_locations(1, 1)[0])
- if child_env
- child_env = [child_env]
- else
- child_env = []
- end
- out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt)
- assert !status.signaled?, FailDesc[status, message, out]
- end
-
- def assert_ruby_status(args, test_stdin="", message=nil, **opt)
- out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt)
- desc = FailDesc[status, message, out]
- assert(!status.signaled?, desc)
- message ||= "ruby exit status is not success:"
- assert(status.success?, desc)
- end
- def assert_warning(pat, msg = nil)
- result = nil
- stderr = EnvUtil.with_default_internal(pat.encoding) {
- EnvUtil.verbose_warning {
- result = yield
- }
- }
- msg = message(msg) {diff pat, stderr}
- assert(pat === stderr, msg)
- result
- end
+ # Prism adds ANSI escape sequences to syntax error messages to
+ # colorize and format them. We strip them out here to make them easier
+ # to match against in tests.
+ message = e.message
+ message.gsub!(/\e\[.*?m/, "")
- def assert_warn(*args)
- assert_warning(*args) {$VERBOSE = false; yield}
+ assert_match(error, message, mesg)
+ e
+ end
end
def assert_no_warning(pat, msg = nil)
@@ -587,195 +792,53 @@ EOT
result
end
- def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt)
- # TODO: consider choosing some appropriate limit for MJIT and stop skipping this once it does not randomly fail
- skip 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled?
-
- require_relative '../../memory_status'
- raise MiniTest::Skip, "unsupported platform" unless defined?(Memory::Status)
-
- token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m"
- token_dump = token.dump
- token_re = Regexp.quote(token)
- envs = args.shift if Array === args and Hash === args.first
- args = [
- "--disable=gems",
- "-r", File.expand_path("../../../memory_status", __FILE__),
- *args,
- "-v", "-",
- ]
- if defined? Memory::NO_MEMORY_LEAK_ENVS then
- envs ||= {}
- newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break }
- envs = newenvs if newenvs
- end
- args.unshift(envs) if envs
- cmd = [
- 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}',
- prepare,
- 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")',
- '$initial_size = $initial_status.size',
- code,
- 'GC.start',
- ].join("\n")
- _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt)
- before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1)
- after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1)
- assert(status.success?, FailDesc[status, message, err])
- ([:size, (rss && :rss)] & after.members).each do |n|
- b = before[n]
- a = after[n]
- next unless a > 0 and b > 0
- assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"})
- end
- rescue LoadError
- skip
- end
-
# 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)
- require 'benchmark'
-
wait = EnvUtil.apply_timeout_scale(wait)
if wait < 0.1 # TIME_QUANTUM_USEC in thread_pthread.c
warn "test #{msg || 'assert_cpu_usage_low'} too short to be accurate"
end
- tms = Benchmark.measure(msg || '') do
- if stop
- th = Thread.start {sleep wait; stop.call}
- yield
- th.join
- else
- begin
- Timeout.timeout(wait) {yield}
- rescue Timeout::Error
- end
+
+ t0, r0 = Process.times, Process.clock_gettime(Process::CLOCK_MONOTONIC)
+
+ if stop
+ th = Thread.start {sleep wait; stop.call}
+ yield
+ th.join
+ else
+ begin
+ Timeout.timeout(wait) {yield}
+ rescue Timeout::Error
end
end
- max = pct * tms.real
+ t1, r1 = Process.times, Process.clock_gettime(Process::CLOCK_MONOTONIC)
+
+ total = t1.utime - t0.utime + t1.stime - t0.stime + t1.cutime - t0.cutime + t1.cstime - t0.cstime
+ real = r1 - r0
+
+ max = pct * real
min_measurable = MIN_MEASURABLE
min_measurable *= 1.30 # add a little (30%) to account for misc. overheads
if max < min_measurable
max = min_measurable
end
- assert_operator tms.total, :<=, max, msg
+ assert_operator total, :<=, max, msg
end
def assert_is_minus_zero(f)
assert(1.0/f == -Float::INFINITY, "#{f} is not -0.0")
end
- # pattern_list is an array which contains regexp and :*.
- # :* means any sequence.
- #
- # pattern_list is anchored.
- # Use [:*, regexp, :*] for non-anchored match.
- def assert_pattern_list(pattern_list, actual, message=nil)
- rest = actual
- anchored = true
- pattern_list.each_with_index {|pattern, i|
- if pattern == :*
- anchored = false
- else
- if anchored
- match = /\A#{pattern}/.match(rest)
- else
- match = pattern.match(rest)
- end
- unless match
- msg = message(msg) {
- expect_msg = "Expected #{mu_pp pattern}\n"
- if /\n[^\n]/ =~ rest
- actual_mesg = +"to match\n"
- rest.scan(/.*\n+/) {
- actual_mesg << ' ' << $&.inspect << "+\n"
- }
- actual_mesg.sub!(/\+\n\z/, '')
- else
- actual_mesg = "to match " + mu_pp(rest)
- end
- actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters"
- expect_msg + actual_mesg
- }
- assert false, msg
- end
- rest = match.post_match
- anchored = true
- end
- }
- if anchored
- assert_equal("", rest)
- end
- end
-
- # threads should respond to shift method.
- # Array can be used.
- def assert_join_threads(threads, message = nil)
- errs = []
- values = []
- while th = threads.shift
- begin
- values << th.value
- rescue Exception
- errs << [th, $!]
- th = nil
- end
- end
- values
- ensure
- if th&.alive?
- th.raise(Timeout::Error.new)
- th.join rescue errs << [th, $!]
- end
- if !errs.empty?
- msg = "exceptions on #{errs.length} threads:\n" +
- errs.map {|t, err|
- "#{t.inspect}:\n" +
- err.full_message(highlight: false, order: :top)
- }.join("\n---\n")
- if message
- msg = "#{message}\n#{msg}"
- end
- raise MiniTest::Assertion, msg
- end
- end
-
- def assert_all_assertions_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)) }
end
-
- def message(msg = nil, *args, &default) # :nodoc:
- if Proc === msg
- super(nil, *args) do
- ary = [msg.call, (default.call if default)].compact.reject(&:empty?)
- if 1 < ary.length
- ary[0...-1] = ary[0...-1].map {|str| str.sub(/(?<!\.)\z/, '.') }
- end
- begin
- ary.join("\n")
- rescue Encoding::CompatibilityError
- ary.map(&:b).join("\n")
- end
- end
- else
- super
- end
- end
end
end
end
diff --git a/tool/lib/test/unit/core_assertions.rb b/tool/lib/test/unit/core_assertions.rb
deleted file mode 100644
index 21be860176..0000000000
--- a/tool/lib/test/unit/core_assertions.rb
+++ /dev/null
@@ -1,216 +0,0 @@
-# frozen_string_literal: true
-
-require_relative '../../envutil'
-
-module Test
- module Unit
- module CoreAssertions
- include MiniTest::Assertions
-
- def mu_pp(obj) #:nodoc:
- obj.pretty_inspect.chomp
- end
-
- def assert_file
- AssertFile
- end
-
- FailDesc = proc do |status, message = "", out = ""|
- pid = status.pid
- now = Time.now
- faildesc = proc do
- if signo = status.termsig
- signame = Signal.signame(signo)
- sigdesc = "signal #{signo}"
- end
- log = EnvUtil.diagnostic_reports(signame, pid, now)
- if signame
- sigdesc = "SIG#{signame} (#{sigdesc})"
- end
- if status.coredump?
- sigdesc = "#{sigdesc} (core dumped)"
- end
- full_message = ''.dup
- message = message.call if Proc === message
- if message and !message.empty?
- full_message << message << "\n"
- end
- full_message << "pid #{pid}"
- full_message << " exit #{status.exitstatus}" if status.exited?
- full_message << " killed by #{sigdesc}" if sigdesc
- if out and !out.empty?
- full_message << "\n" << out.b.gsub(/^/, '| ')
- full_message.sub!(/(?<!\n)\z/, "\n")
- end
- if log
- full_message << "Diagnostic reports:\n" << log.b.gsub(/^/, '| ')
- end
- full_message
- end
- faildesc
- end
-
- def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil,
- success: nil, **opt)
- args = Array(args).dup
- args.insert((Hash === args[0] ? 1 : 0), '--disable=gems')
- stdout, stderr, status = EnvUtil.invoke_ruby(args, test_stdin, true, true, **opt)
- if signo = status.termsig
- EnvUtil.diagnostic_reports(Signal.signame(signo), status.pid, Time.now)
- end
- if block_given?
- raise "test_stdout ignored, use block only or without block" if test_stdout != []
- raise "test_stderr ignored, use block only or without block" if test_stderr != []
- yield(stdout.lines.map {|l| l.chomp }, stderr.lines.map {|l| l.chomp }, status)
- else
- all_assertions(message) do |a|
- [["stdout", test_stdout, stdout], ["stderr", test_stderr, stderr]].each do |key, exp, act|
- a.for(key) do
- if exp.is_a?(Regexp)
- assert_match(exp, act)
- elsif exp.all? {|e| String === e}
- assert_equal(exp, act.lines.map {|l| l.chomp })
- else
- assert_pattern_list(exp, act)
- end
- end
- end
- unless success.nil?
- a.for("success?") do
- if success
- assert_predicate(status, :success?)
- else
- assert_not_predicate(status, :success?)
- end
- end
- end
- end
- status
- end
- end
-
- ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM")
-
- def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt)
- unless file and line
- loc, = caller_locations(1,1)
- file ||= loc.path
- line ||= loc.lineno
- end
- src = <<eom
-# -*- coding: #{line += __LINE__; src.encoding}; -*-
- require #{__dir__.dump};include Test::Unit::Assertions
- END {
- puts [Marshal.dump($!)].pack('m'), "assertions=\#{self._assertions}"
- }
-#{line -= __LINE__; src}
- class Test::Unit::Runner
- @@stop_auto_run = true
- end
-eom
- args = args.dup
- args.insert((Hash === args.first ? 1 : 0), "-w", "--disable=gems", *$:.map {|l| "-I#{l}"})
- stdout, stderr, status = EnvUtil.invoke_ruby(args, src, true, true, **opt)
- abort = status.coredump? || (status.signaled? && ABORT_SIGNALS.include?(status.termsig))
- assert(!abort, FailDesc[status, nil, stderr])
- self._assertions += stdout[/^assertions=(\d+)/, 1].to_i
- begin
- res = Marshal.load(stdout.unpack("m")[0])
- rescue => marshal_error
- ignore_stderr = nil
- end
- if res
- if bt = res.backtrace
- bt.each do |l|
- l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"}
- end
- bt.concat(caller)
- else
- res.set_backtrace(caller)
- end
- raise res unless SystemExit === res
- end
-
- # really is it succeed?
- unless ignore_stderr
- # the body of assert_separately must not output anything to detect error
- assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr])
- end
- assert(status.success?, FailDesc[status, "assert_separately failed", stderr])
- raise marshal_error if marshal_error
- end
-
- class << (AssertFile = Struct.new(:failure_message).new)
- include CoreAssertions
- def assert_file_predicate(predicate, *args)
- if /\Anot_/ =~ predicate
- predicate = $'
- neg = " not"
- end
- result = File.__send__(predicate, *args)
- result = !result if neg
- mesg = "Expected file ".dup << args.shift.inspect
- mesg << "#{neg} to be #{predicate}"
- mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty?
- mesg << " #{failure_message}" if failure_message
- assert(result, mesg)
- end
- alias method_missing assert_file_predicate
-
- def for(message)
- clone.tap {|a| a.failure_message = message}
- end
- end
-
- class AllFailures
- attr_reader :failures
-
- def initialize
- @count = 0
- @failures = {}
- end
-
- def for(key)
- @count += 1
- yield
- rescue Exception => e
- @failures[key] = [@count, e]
- end
-
- def foreach(*keys)
- keys.each do |key|
- @count += 1
- begin
- yield key
- rescue Exception => e
- @failures[key] = [@count, e]
- end
- end
- end
-
- def message
- i = 0
- total = @count.to_s
- fmt = "%#{total.size}d"
- @failures.map {|k, (n, v)|
- v = v.message
- "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.b.gsub(/^/, ' | ').force_encoding(v.encoding)}"
- }.join("\n")
- end
-
- def pass?
- @failures.empty?
- end
- end
-
- def assert_all_assertions(msg = nil)
- all = AllFailures.new
- yield all
- ensure
- assert(all.pass?, message(msg) {all.message.chomp(".")})
- end
- alias all_assertions assert_all_assertions
-
- end
- end
-end
diff --git a/tool/lib/test/unit/parallel.rb b/tool/lib/test/unit/parallel.rb
index d851326aca..188a0d1a19 100644
--- a/tool/lib/test/unit/parallel.rb
+++ b/tool/lib/test/unit/parallel.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../.."
-require 'test/unit'
+
+require_relative "../../../test/init"
module Test
module Unit
@@ -9,7 +9,6 @@ module Test
undef autorun
end
- alias orig_run_suite mini_run_suite
undef _run_suite
undef _run_suites
undef run
@@ -26,21 +25,24 @@ 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
begin
while buf = (self.verbose ? i.gets : i.readpartial(1024))
- _report "p", buf
+ _report "p", buf or break
end
rescue IOError
- rescue Errno::EPIPE
end
end
@@ -53,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
@@ -71,12 +73,10 @@ module Test
result << ($: - @old_loadpath)
result << suite.name
- begin
- _report "done", Marshal.dump(result)
- rescue Errno::EPIPE; end
+ _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?
@@ -105,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)
@@ -119,31 +119,39 @@ 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
- begin
- _report "bye"
- rescue Errno::EPIPE; end
+ _report "bye"
exit
else
_report "ready"
end
- when /^quit$/
- begin
- _report "bye"
- rescue Errno::EPIPE; end
+ when /^quit (.+?)$/, "quit"
+ if $1 == "timeout"
+ err = ["", "!!! worker #{$$} killed due to timeout:"]
+ Thread.list.each do |th|
+ err << "#{ th.inspect }:"
+ th.backtrace.each do |s|
+ err << " #{ s }"
+ end
+ end
+ err << ""
+ STDERR.puts err.join("\n")
+ end
+ _report "bye"
exit
end
end
- rescue Errno::EPIPE
rescue Exception => e
- begin
- trace = e.backtrace || ['unknown method']
- err = ["#{trace.shift}: #{e.message} (#{e.class})"] + trace.map{|t| t.prepend("\t") }
+ trace = e.backtrace || ['unknown method']
+ err = ["#{trace.shift}: #{e.message} (#{e.class})"] + trace.map{|t| "\t" + t }
+ if @stdout
_report "bye", Marshal.dump(err.join("\n"))
- rescue Errno::EPIPE;end
+ else
+ raise "failed to report a failure due to lack of @stdout"
+ end
exit
ensure
@stdin.close if @stdin
@@ -153,24 +161,28 @@ module Test
def _report(res, *args) # :nodoc:
@stdout.write(args.empty? ? "#{res}\n" : "#{res} #{args.pack("m0")}\n")
+ true
+ rescue Errno::EPIPE
+ rescue TypeError => e
+ abort("#{e.inspect} in _report(#{res.inspect}, #{args.inspect})\n#{e.backtrace.join("\n")}")
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)
@@ -179,7 +191,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
@@ -189,20 +201,21 @@ 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
end
+ def self.on_parallel_worker?
+ true
+ end
end
end
end
require 'rubygems'
- module Gem # :nodoc:
- end
- class Gem::TestCase < MiniTest::Unit::TestCase # :nodoc:
- @@project_dir = File.expand_path('../../../../..', __FILE__)
+ 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 58cfbcab99..51ffff37eb 100644
--- a/tool/lib/test/unit/testcase.rb
+++ b/tool/lib/test/unit/testcase.rb
@@ -1,25 +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
- :sorted
+ 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)
@@ -27,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/tracepointchecker.rb b/tool/lib/tracepointchecker.rb
index 47822ecef5..3254e59357 100644
--- a/tool/lib/tracepointchecker.rb
+++ b/tool/lib/tracepointchecker.rb
@@ -80,7 +80,7 @@ module TracePointChecker
call_stack.push method
STATE[:count] += 1
- verbose_out :psuh, method if verbose
+ verbose_out :push, method if verbose
}
TRACES << TracePoint.new(*return_events){|tp|
diff --git a/tool/lib/vcs.rb b/tool/lib/vcs.rb
index dd6162c8c2..26c9763c13 100644
--- a/tool/lib/vcs.rb
+++ b/tool/lib/vcs.rb
@@ -1,12 +1,34 @@
# 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.
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
require 'pathname'
def File.realpath(arg)
@@ -15,106 +37,23 @@ unless File.respond_to? :realpath
end
def IO.pread(*args)
- STDERR.puts(args.inspect) if $DEBUG
+ VCS.dump(args, "args: ") if $DEBUG
popen(*args) {|f|f.read}
end
-if RUBY_VERSION < "2.0"
- class IO
- @orig_popen = method(:popen)
-
- if defined?(fork)
- def self.popen(command, *rest, &block)
- if command.kind_of?(Hash)
- env = command
- command = rest.shift
- end
- opts = rest.last
- if opts.kind_of?(Hash)
- dir = opts.delete(:chdir)
- rest.pop if opts.empty?
- opts.delete(:external_encoding)
- end
-
- if block
- @orig_popen.call("-", *rest) do |f|
- if f
- yield(f)
- else
- Dir.chdir(dir) if dir
- ENV.replace(env) if env
- exec(*command)
- end
- end
- else
- f = @orig_popen.call("-", *rest)
- unless f
- Dir.chdir(dir) if dir
- ENV.replace(env) if env
- exec(*command)
- end
- f
- end
- end
- else
- require 'shellwords'
- def self.popen(command, *rest, &block)
- if command.kind_of?(Hash)
- env = command
- oldenv = ENV.to_hash
- command = rest.shift
- end
- opts = rest.last
- if opts.kind_of?(Hash)
- dir = opts.delete(:chdir)
- rest.pop if opts.empty?
- opts.delete(:external_encoding)
- end
-
- command = command.shelljoin if Array === command
- Dir.chdir(dir || ".") do
- ENV.replace(env) if env
- @orig_popen.call(command, *rest, &block)
- ENV.replace(oldenv) if oldenv
- end
- end
- end
- end
-else
- module DebugPOpen
- verbose, $VERBOSE = $VERBOSE, nil if RUBY_VERSION < "2.1"
- refine IO.singleton_class do
- def popen(*args)
- STDERR.puts args.inspect if $DEBUG
- super
- end
- end
- ensure
- $VERBOSE = verbose unless verbose.nil?
- end
- using DebugPOpen
- module DebugSystem
- def system(*args)
- STDERR.puts args.inspect if $DEBUG
- exception = false
- opts = Hash.try_convert(args[-1])
- if RUBY_VERSION >= "2.6"
- unless opts
- opts = {}
- args << opts
- end
- exception = opts.fetch(:exception) {opts[:exception] = true}
- elsif opts
- exception = opts.delete(:exception) {true}
- args.pop if opts.empty?
- end
- ret = super(*args)
- raise "Command failed with status (#$?): #{args[0]}" if exception and !ret
- ret
+module DebugPOpen
+ refine IO.singleton_class do
+ def popen(*args)
+ VCS.dump(args, "args: ") if $DEBUG
+ super
end
end
- module Kernel
- prepend(DebugSystem)
+end
+using DebugPOpen
+module DebugSystem
+ def system(*args, exception: true, **opts)
+ VCS.dump(args, "args: ") if $DEBUG
+ super(*args, exception: exception, **opts)
end
end
@@ -127,14 +66,19 @@ class VCS
@@dirs << [dir, self, pred]
end
- def self.detect(path, options = {}, argv = ::ARGV)
+ def self.detect(path = '.', options = {}, parser = nil, **opts)
+ options.update(opts)
uplevel_limit = options.fetch(:uplevel_limit, 0)
curr = path
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.parse_options(argv)
+ vcs.define_options(parser) if parser
+ vcs.set_options(options)
return vcs
end
end
@@ -151,6 +95,27 @@ class VCS
String === path or path.respond_to?(:to_path)
end
+ def self.define_options(parser, opts = {})
+ 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)
@@ -158,39 +123,37 @@ class VCS
super()
end
- def parse_options(opts, parser = OptionParser.new)
- case opts
- when Array
- parser.on("--[no-]dryrun") {|v| @dryrun = v}
- parser.on("--[no-]debug") {|v| @debug = v}
- parser.parse(opts)
- @debug = $DEBUG unless defined?(@debug)
- @dryrun = @debug unless defined?(@dryrun)
- when Hash
- unless (keys = opts.keys - [:debug, :dryrun]).empty?
- raise "Unknown options: #{keys.join(', ')}"
- end
- @debug = opts.fetch(:debug) {$DEBUG}
- @dryrun = opts.fetch(:dryrun) {@debug}
- end
+ def chdir(path)
+ @srcdir = path
+ end
+
+ def define_options(parser)
+ end
+
+ 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
- # return a pair of strings, the last revision and the last revision in which
- # +path+ was modified.
+ # returns
+ # * the last revision of the current branch
+ # * the last revision in which +path+ was modified
+ # * the last modified time of +path+
+ # * the last commit title since the latest upstream
def get_revisions(path)
if self.class.local_path?(path)
path = relative_to(path)
end
last, changed, modified, *rest = (
begin
- if NullDevice
+ if NullDevice and !debug?
save_stderr = STDERR.dup
STDERR.reopen NullDevice, 'w'
end
@@ -217,6 +180,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
@@ -247,183 +211,76 @@ class VCS
end
def after_export(dir)
+ FileUtils.rm_rf(Dir.glob("#{dir}/.git*"))
+ FileUtils.rm_rf(Dir.glob("#{dir}/.mailmap"))
+ end
+
+ def revision_handler(rev)
+ self.class
end
def revision_name(rev)
- self.class.revision_name(rev)
+ revision_handler(rev).revision_name(rev)
end
def short_revision(rev)
- self.class.short_revision(rev)
+ revision_handler(rev).short_revision(rev)
end
- class SVN < self
- register(".svn")
- COMMAND = ENV['SVN'] || 'svn'
-
- def self.revision_name(rev)
- "r#{rev}"
- end
-
- def self.short_revision(rev)
- rev
- end
-
- def _get_revisions(path, srcdir = nil)
- if srcdir and local_path?(path)
- path = File.join(srcdir, path)
- end
- if srcdir
- info_xml = IO.pread(%W"#{COMMAND} info --xml #{srcdir}")
- info_xml = nil unless info_xml[/<url>(.*)<\/url>/, 1] == path.to_s
- end
- info_xml ||= IO.pread(%W"#{COMMAND} info --xml #{path}")
- _, last, _, changed, _ = info_xml.split(/revision="(\d+)"/)
- modified = info_xml[/<date>([^<>]*)/, 1]
- branch = info_xml[%r'<relative-url>\^/(?:branches/|tags/)?([^<>]+)', 1]
- [last, changed, modified, branch]
- end
-
- def self.search_root(path)
- return unless local_path?(path)
- parent = File.realpath(path)
- begin
- parent = File.dirname(wkdir = parent)
- return wkdir if File.directory?(wkdir + "/.svn")
- end until parent == wkdir
- end
-
- def get_info
- @info ||= IO.pread(%W"#{COMMAND} info --xml #{@srcdir}")
- end
-
- def url
- unless @url
- url = get_info[/<root>(.*)<\/root>/, 1]
- @url = URI.parse(url+"/") if url
- end
- @url
- end
-
- def wcroot
- unless @wcroot
- info = get_info
- @wcroot = info[/<wcroot-abspath>(.*)<\/wcroot-abspath>/, 1]
- @wcroot ||= self.class.search_root(@srcdir)
- end
- @wcroot
- end
-
- def branch(name)
- return trunk if name == "trunk"
- url + "branches/#{name}"
- end
-
- def tag(name)
- url + "tags/#{name}"
- end
-
- def trunk
- url + "trunk"
- end
- alias master trunk
-
- def branch_list(pat)
- IO.popen(%W"#{COMMAND} ls #{branch('')}") do |f|
- f.each do |line|
- line.chomp!
- line.chomp!('/')
- yield(line) if File.fnmatch?(pat, line)
- end
- end
- end
-
- def grep(pat, tag, *files, &block)
- cmd = %W"#{COMMAND} cat"
- files.map! {|n| File.join(tag, n)} if tag
- set = block.binding.eval("proc {|match| $~ = match}")
- IO.popen([cmd, *files]) do |f|
- f.grep(pat) do |s|
- set[$~]
- yield s
- end
- end
- end
-
- def export(revision, url, dir, keep_temp = false)
- if @srcdir and (rootdir = wcroot)
- srcdir = File.realpath(@srcdir)
- rootdir << "/"
- if srcdir.start_with?(rootdir)
- subdir = srcdir[rootdir.size..-1]
- subdir = nil if subdir.empty?
- FileUtils.mkdir_p(svndir = dir+"/.svn")
- FileUtils.ln_s(Dir.glob(rootdir+"/.svn/*"), svndir)
- system(COMMAND, "-q", "revert", "-R", subdir || ".", :chdir => dir) or return false
- FileUtils.rm_rf(svndir) unless keep_temp
- if subdir
- tmpdir = Dir.mktmpdir("tmp-co.", "#{dir}/#{subdir}")
- File.rename(tmpdir, tmpdir = "#{dir}/#{File.basename(tmpdir)}")
- FileUtils.mv(Dir.glob("#{dir}/#{subdir}/{.[^.]*,..?*,*}"), tmpdir)
- begin
- Dir.rmdir("#{dir}/#{subdir}")
- end until (subdir = File.dirname(subdir)) == '.'
- FileUtils.mv(Dir.glob("#{tmpdir}/#{subdir}/{.[^.]*,..?*,*}"), dir)
- Dir.rmdir(tmpdir)
- end
- return true
- end
- end
- IO.popen(%W"#{COMMAND} export -r #{revision} #{url} #{dir}") do |pipe|
- pipe.each {|line| /^A/ =~ line or yield line}
- end
- $?.success?
- end
-
- def after_export(dir)
- FileUtils.rm_rf(dir+"/.svn")
- 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
- def branch_beginning(url)
- # `--limit` of svn-log is useless in this case, because it is
- # applied before `--search`.
- rev = IO.pread(%W[ #{COMMAND} log --xml
- --search=matz --search-and=has\ started
- -- #{url}/version.h])[/<logentry\s+revision="(\d+)"/m, 1]
- rev.to_i if rev
- end
-
- def export_changelog(url, from, to, path)
- range = [to || 'HEAD', (from ? from+1 : branch_beginning(url))].compact.join(':')
- IO.popen({'TZ' => 'JST-9', 'LANG' => 'C', 'LC_ALL' => 'C'},
- %W"#{COMMAND} log -r#{range} #{url}") do |r|
- open(path, 'w') do |w|
- IO.copy_stream(r, w)
+ class GIT < self
+ 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
- end
- end
- def commit
- args = %W"#{COMMAND} commit"
- if dryrun?
- STDERR.puts(args.inspect)
- return true
- end
- system(*args)
+ COMMAND and File.exist?(File.join(path, dir))
end
- end
-
- class GIT < self
- register(".git") {|path, dir| File.exist?(File.join(path, dir))}
- COMMAND = ENV["GIT"] || 'git'
def cmd_args(cmds, srcdir = nil)
(opts = cmds.last).kind_of?(Hash) or cmds << (opts = {})
opts[:external_encoding] ||= "UTF-8"
- if srcdir and self.class.local_path?(srcdir)
+ if srcdir
opts[:chdir] ||= srcdir
end
- STDERR.puts cmds.inspect if debug?
+ VCS.dump(cmds, "cmds: ") if debug? and !$DEBUG
cmds
end
@@ -432,7 +289,9 @@ class VCS
end
def cmd_read_at(srcdir, cmds)
- without_gitconfig { IO.pread(*cmd_args(cmds, srcdir)) }
+ result = without_gitconfig { IO.pread(*cmd_args(cmds, srcdir)) }
+ VCS.dump(result, "result: ") if debug?
+ result
end
def cmd_pipe(*cmds, &block)
@@ -443,15 +302,41 @@ class VCS
cmd_read_at(@srcdir, cmds)
end
+ def svn_revision(log)
+ if /^ *git-svn-id: .*@(\d+) .*\n+\z/ =~ log
+ $1.to_i
+ end
+ end
+
def _get_revisions(path, srcdir = nil)
+ ref = Branch === path ? path.to_str : 'HEAD'
gitcmd = [COMMAND]
- last = cmd_read_at(srcdir, [[*gitcmd, 'rev-parse', 'HEAD']]).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 VCS::NotFoundError, "#{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]
- branch = cmd_read_at(srcdir, [gitcmd + %W[symbolic-ref --short HEAD]])
+ if rev = svn_revision(log)
+ if changed == last
+ last = rev
+ else
+ svn_rev = svn_revision(cmd_read_at(srcdir, [[*gitcmd, 'log', '-n1', '--format=%B', last]]))
+ last = svn_rev if svn_rev
+ end
+ changed = rev
+ end
+ branch = cmd_read_at(srcdir, [gitcmd + %W[symbolic-ref --short #{ref}]])
if branch.empty?
- branch_list = cmd_read_at(srcdir, [gitcmd + %W[branch --list --contains HEAD]]).lines.to_a
+ branch = cmd_read_at(srcdir, [gitcmd + %W[describe --contains #{ref}]]).strip
+ end
+ if branch.empty?
+ branch_list = cmd_read_at(srcdir, [gitcmd + %W[branch --list --contains #{ref}]]).lines.to_a
branch, = branch_list.grep(/\A\*/)
case branch
when /\A\* *\(\S+ detached at (.*)\)\Z/
@@ -472,7 +357,7 @@ class VCS
branch = ":detached:" if branch.empty?
upstream = cmd_read_at(srcdir, [gitcmd + %W[branch --list --format=%(upstream:short) #{branch}]])
upstream.chomp!
- title = cmd_read_at(srcdir, [gitcmd + %W[log --format=%s -n1 #{upstream}..HEAD]])
+ title = cmd_read_at(srcdir, [gitcmd + %W[log --format=%s -n1 #{upstream}..#{ref}]])
title = nil if title.empty?
[last, changed, modified, branch, title]
end
@@ -486,17 +371,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
- if srcdir = @srcdir and self.class.local_path?(srcdir)
- @srcdir = File.realpath(srcdir)
- end
+ @srcdir = File.realpath(@srcdir)
+ @gitconfig = nil
+ VCS.dump(@srcdir, "srcdir: ") if debug?
self
end
@@ -540,47 +443,176 @@ class VCS
end
def export(revision, url, dir, keep_temp = false)
- ret = system(COMMAND, "clone", "-s", (@srcdir || '.').to_s, "-b", url, dir)
- ret
- end
-
- def after_export(dir)
- FileUtils.rm_rf(Dir.glob("#{dir}/.git*"))
+ system(COMMAND, "clone", "-c", "advice.detachedHead=false", "-s", (@srcdir || '.').to_s, "-b", url, dir) or return
+ GIT.new(File.expand_path(dir))
end
def branch_beginning(url)
- cmd_read(%W[ #{COMMAND} log -n1 --format=format:%H
- --author=matz --committer=matz --grep=has\ started
- -- version.h include/ruby/version.h])
+ year = cmd_read(%W[ #{COMMAND} log -n1 --format=%cd --date=format:%Y #{url} --]).to_i
+ cmd_read(%W[ #{COMMAND} log --format=format:%H --reverse --since=#{year-1}-12-25
+ --author=matz --committer=matz --grep=started\\.$
+ #{url} -- version.h include/ruby/version.h])[/.*/]
end
- def export_changelog(url, from, to, path)
+ def export_changelog(url = '@', from = nil, to = nil, _path = nil, path: _path, base_url: true)
from, to = [from, to].map do |rev|
rev or next
- if Integer === rev
- rev = cmd_read({'LANG' => 'C', 'LC_ALL' => 'C'},
- %W"#{COMMAND} log -n1 --format=format:%H" <<
- "--grep=^ *git-svn-id: .*@#{rev} ")
- end
rev unless rev.empty?
end
- unless /./.match(from ||= branch_beginning(url))
- raise "cannot find the beginning revision of the branch"
+ unless from&.match?(/./) or (from = branch_beginning(url))&.match?(/./)
+ warn "no starting commit found", uplevel: 1
+ from = nil
end
- range = [from, (to || 'HEAD')].join('^..')
- cmd_pipe({'TZ' => 'JST-9', 'LANG' => 'C', 'LC_ALL' => 'C'},
- %W"#{COMMAND} log --format=medium --no-notes --date=iso-local --topo-order #{range}", "rb") do |r|
- format_changelog(r, path)
+ if system(*%W"#{COMMAND} fetch origin refs/notes/commits:refs/notes/commits",
+ chdir: @srcdir, exception: false)
+ system(*%W"#{COMMAND} fetch origin refs/notes/log-fix:refs/notes/log-fix",
+ chdir: @srcdir, exception: false)
+ else
+ warn "Could not fetch notes/commits tree", uplevel: 1
end
- end
+ to ||= url.to_str
+ if from
+ arg = ["#{from}^..#{to}"]
+ else
+ arg = ["--since=25 Dec 00:00:00", to]
+ end
+ if base_url == true
+ env = CHANGELOG_ENV
+ remote, = upstream
+ if remote &&= cmd_read(env, %W[#{COMMAND} remote get-url --no-push #{remote}])
+ remote.chomp!
+ # hack to redirect git.r-l.o to github
+ remote.sub!(/\Agit@git\.ruby-lang\.org:/, 'git@github.com:ruby/')
+ remote.sub!(/\Agit@(.*?):(.*?)(?:\.git)?\z/, 'https://\1/\2/commit/')
+ end
+ base_url = remote
+ end
+ writer = changelog_formatter(path, arg, base_url)
+ if !path or path == '-'
+ writer[$stdout]
+ else
+ File.open(path, 'wb', &writer)
+ end
+ end
+
+ LOG_FIX_REGEXP_SEPARATORS = '/!:;|,#%&'
+ CHANGELOG_ENV = {'TZ' => 'JST-9', 'LANG' => 'C', 'LC_ALL' => 'C'}
+
+ def changelog_formatter(path, arg, base_url = nil)
+ env = CHANGELOG_ENV
+ 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, "-1", chdir: @srcdir, out: NullDevice, exception: false)
+ date = "--date=iso"
+ end
+ cmd << date
+ cmd.concat(arg)
+ proc do |w|
+ w.print "-*- coding: utf-8 -*-\n"
+ w.print "\n""base-url = #{base_url}\n" if base_url
+
+ begin
+ ignore_revs = File.readlines(File.join(@srcdir, ".git-blame-ignore-revs"), chomp: true)
+ .grep_v(/^ *(?:#|$)/)
+ .to_h {|v| [v, true]}
+ ignore_revs = nil if ignore_revs.empty?
+ rescue Errno::ENOENT
+ end
+
+ cmd_pipe(env, cmd, chdir: @srcdir) do |r|
+ r.gets(sep = "commit ")
+ sep = "\n" + sep
+ while s = r.gets(sep, chomp: true)
+ h, s = s.split(/^$/, 2)
+ if ignore_revs&.key?(h[/\A\h{40}/])
+ next
+ end
+
+ next if /^Author: *dependabot\[bot\]/ =~ h
+
+ h.gsub!(/^(?:(?:Author|Commit)(?:Date)?|Date): /, ' \&')
+ if s.sub!(/\nNotes \(log-fix\):\n((?: +.*\n)+)/, '')
+ fix = $1
+ next if /\A *skip\Z/ =~ fix
+ 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[^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 = ["changelog_formatter 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
+ message << "#{i}:#{e}"
+ end
+ raise message.join('')
+ end
+ 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
+
+ s.gsub!(%r[(?!<\w)([-\w]+/[-\w]+)(?:@(\h{8,40})|#(\d{5,}))\b]) do
+ path = defined?($2) ? "commit/#{$2}" : "pull/#{$3}"
+ "[#$&](https://github.com/#{$1}/#{path})"
+ 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
- def format_changelog(r, path)
- IO.copy_stream(r, path)
+ s.gsub!(/ +\n/, "\n")
+ s.sub!(/^Notes:/, ' \&')
+ w.print sep, h, s
+ end
+ end
+ end
end
- def commit(opts = {})
- args = [COMMAND, "push"]
- args << "-n" if dryrun
+ def upstream
(branch = cmd_read(%W"#{COMMAND} symbolic-ref --short HEAD")).chomp!
(upstream = cmd_read(%W"#{COMMAND} branch --list --format=%(upstream) #{branch}")).chomp!
while ref = upstream[%r"\Arefs/heads/(.*)", 1]
@@ -589,81 +621,36 @@ class VCS
unless %r"\Arefs/remotes/([^/]+)/(.*)" =~ upstream
raise "Upstream not found"
end
- args << $1 << "HEAD:#$2"
+ [$1, $2]
+ end
+
+ def commit(opts = {})
+ args = [COMMAND, "push"]
+ args << "-n" if dryrun?
+ remote, branch = upstream
+ args << remote
+ branches = %W[refs/notes/commits:refs/notes/commits HEAD:#{branch}]
if dryrun?
- STDERR.puts(args.inspect)
+ branches.each do |b|
+ VCS.dump(args + [b], "commit: ")
+ end
return true
end
- system(*args) or return false
+ branches.each do |b|
+ system(*(args + [b])) or return false
+ end
true
end
end
- class GITSVN < GIT
- def self.revision_name(rev)
- SVN.revision_name(rev)
+ class Null < self
+ def get_revisions(path, srcdir = nil)
+ @modified ||= Time.now - 10
+ return nil, nil, @modified
end
- def self.short_revision(rev)
- SVN.short_revision(rev)
- end
-
- def format_changelog(r, path)
- open(path, 'w') do |w|
- sep = "-"*72
- w.puts sep
- while s = r.gets('')
- author = s[/^Author:\s*(\S+)/, 1]
- time = s[/^Date:\s*(.+)/, 1]
- s = r.gets('')
- s.gsub!(/^ {4}/, '')
- s.sub!(/^git-svn-id: .*@(\d+) .*\n+\z/, '')
- rev = $1
- s.gsub!(/^ {8}/, '') if /^(?! {8}|$)/ !~ s
- s.sub!(/\n\n\z/, "\n")
- if /\A(\d+)-(\d+)-(\d+)/ =~ time
- date = Time.new($1.to_i, $2.to_i, $3.to_i).strftime("%a, %d %b %Y")
- end
- lines = s.count("\n")
- lines = "#{lines} line#{lines == 1 ? '' : 's'}"
- w.puts "r#{rev} | #{author} | #{time} (#{date}) | #{lines}\n\n"
- w.puts s, sep
- end
- end
- end
-
- def last_changed_revision
- rev = cmd_read(%W"#{COMMAND} svn info"+[STDERR=>[:child, :out]])[/^Last Changed Rev: (\d+)/, 1]
- com = cmd_read(%W"#{COMMAND} svn find-rev r#{rev}").chomp
- return rev, com
- end
-
- def commit(opts = {})
- rev, com = last_changed_revision
- head = cmd_read(%W"#{COMMAND} symbolic-ref --short HEAD").chomp
-
- commits = cmd_read([COMMAND, "log", "--reverse", "--format=%H %ae %ce", "#{com}..@"], "rb").split("\n")
- commits.each_with_index do |l, i|
- r, a, c = l.split
- dcommit = [COMMAND, "svn", "dcommit"]
- dcommit.insert(-2, "-n") if dryrun
- dcommit << "--add-author-from" unless a == c
- dcommit << r
- system(*dcommit) or return false
- system(COMMAND, "checkout", head) or return false
- system(COMMAND, "rebase") or return false
- end
-
- if rev
- old = [cmd_read(%W"#{COMMAND} log -1 --format=%H").chomp]
- old << cmd_read(%W"#{COMMAND} svn reset -r#{rev}")[/^r#{rev} = (\h+)/, 1]
- 3.times do
- sleep 2
- system(*%W"#{COMMAND} pull --no-edit --rebase")
- break unless old.include?(cmd_read(%W"#{COMMAND} log -1 --format=%H").chomp)
- end
- end
- true
+ 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)/, '')