summaryrefslogtreecommitdiff
path: root/tool/lib
diff options
context:
space:
mode:
Diffstat (limited to 'tool/lib')
-rw-r--r--tool/lib/_tmpdir.rb100
-rw-r--r--tool/lib/bundled_gem.rb118
-rw-r--r--tool/lib/colorize.rb35
-rw-r--r--tool/lib/core_assertions.rb129
-rw-r--r--tool/lib/envutil.rb76
-rw-r--r--tool/lib/iseq_loader_checker.rb9
-rw-r--r--tool/lib/leakchecker.rb6
-rw-r--r--tool/lib/memory_status.rb2
-rw-r--r--tool/lib/output.rb70
-rw-r--r--tool/lib/path.rb101
-rw-r--r--tool/lib/test/unit.rb370
-rw-r--r--tool/lib/test/unit/parallel.rb14
-rw-r--r--tool/lib/test/unit/testcase.rb22
-rw-r--r--tool/lib/vcs.rb217
-rw-r--r--tool/lib/vpath.rb7
-rw-r--r--tool/lib/webrick/httprequest.rb2
-rw-r--r--tool/lib/webrick/httpserver.rb1
-rw-r--r--tool/lib/webrick/httputils.rb2
18 files changed, 1098 insertions, 183 deletions
diff --git a/tool/lib/_tmpdir.rb b/tool/lib/_tmpdir.rb
new file mode 100644
index 0000000000..fd429dab37
--- /dev/null
+++ b/tool/lib/_tmpdir.rb
@@ -0,0 +1,100 @@
+template = "rubytest."
+
+# This path is only for tests.
+# Assume the directory by these environment variables are safe.
+base = [ENV["TMPDIR"], ENV["TMP"], "/tmp"].find do |tmp|
+ next unless tmp and tmp.size <= 50 and File.directory?(tmp)
+ # On macOS, the default TMPDIR is very long, inspite of UNIX socket
+ # path length is limited.
+ #
+ # Also Rubygems creates its own temporary directory per tests, and
+ # some tests copy the full path of gemhome there. In that caes, the
+ # path contains both temporary names twice, and can exceed path name
+ # limit very easily.
+ tmp
+end
+begin
+ tmpdir = File.join(base, template + Random.new_seed.to_s(36)[-6..-1])
+ Dir.mkdir(tmpdir, 0o700)
+rescue Errno::EEXIST
+ retry
+end
+# warn "tmpdir(#{tmpdir.size}) = #{tmpdir}"
+
+pid = $$
+END {
+ if pid == $$
+ begin
+ Dir.rmdir(tmpdir)
+ rescue Errno::ENOENT
+ rescue Errno::ENOTEMPTY
+ require_relative "colorize"
+ colorize = Colorize.new
+ ls = Struct.new(:colorize) do
+ def mode_inspect(m, s)
+ [
+ (m & 0o4 == 0 ? ?- : ?r),
+ (m & 0o2 == 0 ? ?- : ?w),
+ (m & 0o1 == 0 ? (s ? s.upcase : ?-) : (s || ?x)),
+ ]
+ end
+ def decorate_path(path, st)
+ case
+ when st.directory?
+ color = "bold;blue"
+ type = "/"
+ when st.symlink?
+ color = "bold;cyan"
+ # type = "@"
+ when st.executable?
+ color = "bold;green"
+ type = "*"
+ when path.end_with?(".gem")
+ color = "green"
+ end
+ colorize.decorate(path, color) + (type || "")
+ end
+ def list_tree(parent, indent = "", &block)
+ children = Dir.children(parent).map do |child|
+ [child, path = File.join(parent, child), File.lstat(path)]
+ end
+ nlink_width = children.map {|child, path, st| st.nlink}.max.to_s.size
+ size_width = children.map {|child, path, st| st.size}.max.to_s.size
+
+ children.each do |child, path, st|
+ m = st.mode
+ m = [
+ (st.file? ? ?- : st.ftype[0]),
+ mode_inspect(m >> 6, (?s unless m & 04000 == 0)),
+ mode_inspect(m >> 3, (?s unless m & 02000 == 0)),
+ mode_inspect(m, (?t unless m & 01000 == 0)),
+ ].join("")
+ warn sprintf("%s* %s %*d %*d %s % s%s",
+ indent, m, nlink_width, st.nlink, size_width, st.size,
+ st.mtime.to_s, decorate_path(child, st),
+ (" -> " + decorate_path(File.readlink(path), File.stat(path)) if
+ st.symlink?))
+ if st.directory?
+ list_tree(File.join(parent, child), indent + " ", &block)
+ end
+ yield path, st if block
+ end
+ end
+ end.new(colorize)
+ warn colorize.notice("Children under ")+colorize.fail(tmpdir)+":"
+ Dir.chdir(tmpdir) do
+ ls.list_tree(".") do |path, st|
+ if st.directory?
+ Dir.rmdir(path)
+ else
+ File.unlink(path)
+ end
+ end
+ end
+ require "fileutils"
+ FileUtils.rm_rf(tmpdir)
+ end
+ end
+}
+
+ENV["TMPDIR"] = ENV["SPEC_TEMP_DIR"] = ENV["GEM_TEST_TMPDIR"] = tmpdir
diff --git a/tool/lib/bundled_gem.rb b/tool/lib/bundled_gem.rb
new file mode 100644
index 0000000000..3ba27f6d64
--- /dev/null
+++ b/tool/lib/bundled_gem.rb
@@ -0,0 +1,118 @@
+require 'fileutils'
+require 'rubygems'
+require 'rubygems/package'
+
+# This library is used by "make extract-gems" to
+# unpack bundled gem files.
+
+module BundledGem
+ DEFAULT_GEMS_DEPENDENCIES = [
+ "net-protocol", # net-ftp
+ "time", # net-ftp
+ "singleton", # prime
+ "ipaddr", # rinda
+ "forwardable" # prime, rinda
+ ]
+
+ module_function
+
+ def unpack(file, *rest)
+ pkg = Gem::Package.new(file)
+ prepare_test(pkg.spec, *rest) {|dir| pkg.extract_files(dir)}
+ puts "Unpacked #{file}"
+ rescue Gem::Package::FormatError, Errno::ENOENT
+ puts "Try with hash version of bundled gems instead of #{file}. We don't use this gem with release version of Ruby."
+ if file =~ /^gems\/(\w+)-/
+ file = Dir.glob("gems/#{$1}-*.gem").first
+ end
+ retry
+ end
+
+ def build(gemspec, version, outdir = ".", validation: true)
+ outdir = File.expand_path(outdir)
+ gemdir, gemfile = File.split(gemspec)
+ Dir.chdir(gemdir) do
+ spec = Gem::Specification.load(gemfile)
+ abort "Failed to load #{gemspec}" unless spec
+ output = File.join(outdir, spec.file_name)
+ FileUtils.rm_rf(output)
+ package = Gem::Package.new(output)
+ package.spec = spec
+ package.build(validation == false)
+ end
+ end
+
+ def copy(path, *rest)
+ path, n = File.split(path)
+ spec = Dir.chdir(path) {Gem::Specification.load(n)} or raise "Cannot load #{path}"
+ prepare_test(spec, *rest) do |dir|
+ FileUtils.rm_rf(dir)
+ files = spec.files.reject {|f| f.start_with?(".git")}
+ dirs = files.map {|f| File.dirname(f) if f.include?("/")}.uniq
+ FileUtils.mkdir_p(dirs.map {|d| d ? "#{dir}/#{d}" : dir}.sort_by {|d| d.count("/")})
+ files.each do |f|
+ File.copy_stream(File.join(path, f), File.join(dir, f))
+ end
+ end
+ puts "Copied #{path}"
+ end
+
+ def prepare_test(spec, dir = ".")
+ target = spec.full_name
+ Gem.ensure_gem_subdirectories(dir)
+ gem_dir = File.join(dir, "gems", target)
+ yield gem_dir
+ spec_dir = spec.extensions.empty? ? "specifications" : File.join("gems", target)
+ if spec.extensions.empty?
+ spec.dependencies.reject! {|dep| DEFAULT_GEMS_DEPENDENCIES.include?(dep.name)}
+ end
+ File.binwrite(File.join(dir, spec_dir, "#{target}.gemspec"), spec.to_ruby)
+ unless spec.extensions.empty?
+ spec.dependencies.clear
+ File.binwrite(File.join(dir, spec_dir, ".bundled.#{target}.gemspec"), spec.to_ruby)
+ end
+ if spec.bindir and spec.executables
+ bindir = File.join(dir, "bin")
+ Dir.mkdir(bindir) rescue nil
+ spec.executables.each do |exe|
+ File.open(File.join(bindir, exe), "wb", 0o777) {|f|
+ f.print "#!ruby\n",
+ %[load File.realpath("../gems/#{target}/#{spec.bindir}/#{exe}", __dir__)\n]
+ }
+ end
+ end
+ FileUtils.rm_rf(Dir.glob("#{gem_dir}/.git*"))
+ end
+
+ def dummy_gemspec(gemspec)
+ return if File.exist?(gemspec)
+ gemdir, gemfile = File.split(gemspec)
+ Dir.chdir(gemdir) do
+ spec = Gem::Specification.new do |s|
+ s.name = gemfile.chomp(".gemspec")
+ s.version = File.read("lib/#{s.name}.rb")[/VERSION = "(.+?)"/, 1]
+ s.authors = ["DUMMY"]
+ s.email = ["dummy@ruby-lang.org"]
+ s.files = Dir.glob("{lib,ext}/**/*").select {|f| File.file?(f)}
+ s.licenses = ["Ruby"]
+ s.description = "DO NOT USE; dummy gemspec only for test"
+ s.summary = "(dummy gemspec)"
+ end
+ File.write(gemfile, spec.to_ruby)
+ end
+ end
+
+ def checkout(gemdir, repo, rev, git: $git)
+ return unless rev or !git or git.empty?
+ unless File.exist?("#{gemdir}/.git")
+ puts "Cloning #{repo}"
+ command = "#{git} clone #{repo} #{gemdir}"
+ system(command) or raise "failed: #{command}"
+ end
+ puts "Update #{File.basename(gemdir)} to #{rev}"
+ command = "#{git} fetch origin #{rev}"
+ system(command, chdir: gemdir) or raise "failed: #{command}"
+ command = "#{git} checkout --detach #{rev}"
+ system(command, chdir: gemdir) or raise "failed: #{command}"
+ end
+end
diff --git a/tool/lib/colorize.rb b/tool/lib/colorize.rb
index 11b878d318..0904312119 100644
--- a/tool/lib/colorize.rb
+++ b/tool/lib/colorize.rb
@@ -6,8 +6,8 @@ class Colorize
# Colorize.new(color: color, colors_file: colors_file)
def initialize(color = nil, opts = ((_, color = color, nil)[0] if Hash === color))
@colors = @reset = nil
- @color = (opts[:color] if opts)
- if color or (color == nil && STDOUT.tty?)
+ @color = opts && opts[:color] || color
+ if color or (color == nil && coloring?)
if (%w[smso so].any? {|attr| /\A\e\[.*m\z/ =~ IO.popen("tput #{attr}", "r", :err => IO::NULL, &:read)} rescue nil)
@beg = "\e["
colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {}
@@ -27,21 +27,48 @@ class Colorize
end
DEFAULTS = {
- "pass"=>"32", "fail"=>"31;1", "skip"=>"33;1",
+ # color names
"black"=>"30", "red"=>"31", "green"=>"32", "yellow"=>"33",
"blue"=>"34", "magenta"=>"35", "cyan"=>"36", "white"=>"37",
"bold"=>"1", "underline"=>"4", "reverse"=>"7",
+ "bright_black"=>"90", "bright_red"=>"91", "bright_green"=>"92", "bright_yellow"=>"93",
+ "bright_blue"=>"94", "bright_magenta"=>"95", "bright_cyan"=>"96", "bright_white"=>"97",
+
+ # abstract decorations
+ "pass"=>"green", "fail"=>"red;bold", "skip"=>"yellow;bold",
+ "note"=>"bright_yellow", "notice"=>"bright_yellow", "info"=>"bright_magenta",
}
+ def coloring?
+ STDOUT.tty? && (!(nc = ENV['NO_COLOR']) || nc.empty?)
+ end
+
# colorize.decorate(str, name = color_name)
def decorate(str, name = @color)
- if @colors and color = (@colors[name] || DEFAULTS[name])
+ if coloring? and color = resolve_color(name)
"#{@beg}#{color}m#{str}#{@reset}"
else
str
end
end
+ def resolve_color(color = @color, seen = {}, colors = nil)
+ return unless @colors
+ color.to_s.gsub(/\b[a-z][\w ]+/) do |n|
+ n.gsub!(/\W+/, "_")
+ n.downcase!
+ c = seen[n] and next c
+ if colors
+ c = colors[n]
+ elsif (c = (tbl = @colors)[n] || (tbl = DEFAULTS)[n])
+ colors = tbl
+ else
+ next n
+ end
+ seen[n] = resolve_color(c, seen, colors)
+ end
+ end
+
DEFAULTS.each_key do |name|
define_method(name) {|str|
decorate(str, name)
diff --git a/tool/lib/core_assertions.rb b/tool/lib/core_assertions.rb
index 67373139ca..b456a55b34 100644
--- a/tool/lib/core_assertions.rb
+++ b/tool/lib/core_assertions.rb
@@ -1,6 +1,43 @@
# 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)
@@ -37,6 +74,11 @@ module Test
module CoreAssertions
require_relative 'envutil'
require 'pp'
+ begin
+ require '-test-/asan'
+ rescue LoadError
+ end
+
nil.pretty_inspect
def mu_pp(obj) #:nodoc:
@@ -111,8 +153,13 @@ module Test
end
def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt)
- # TODO: consider choosing some appropriate limit for MJIT and stop skipping this once it does not randomly fail
+ # TODO: consider choosing some appropriate limit for RJIT and stop skipping this once it does not randomly fail
+ pend 'assert_no_memory_leak may consider RJIT memory usage as leak' if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled?
+ # For previous versions which implemented MJIT
pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled?
+ # ASAN has the same problem - its shadow memory greatly increases memory usage
+ # (plus asan has better ways to detect memory leaks than this assertion)
+ pend 'assert_no_memory_leak may consider ASAN memory usage as leak' if defined?(Test::ASAN) && Test::ASAN.enabled?
require_relative 'memory_status'
raise Test::Unit::PendedError, "unsupported platform" unless defined?(Memory::Status)
@@ -248,7 +295,11 @@ module Test
at_exit {
out.puts "#{token}<error>", [Marshal.dump($!)].pack('m'), "#{token}</error>", "#{token}assertions=#{self._assertions}"
}
- Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) if defined?(Test::Unit::Runner)
+ if defined?(Test::Unit::Runner)
+ Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true)
+ elsif defined?(Test::Unit::AutoRunner)
+ Test::Unit::AutoRunner.need_auto_run = false
+ end
end
def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt)
@@ -268,7 +319,7 @@ module Test
src = <<eom
# -*- coding: #{line += __LINE__; src.encoding}; -*-
BEGIN {
- require "test/unit";include Test::Unit::Assertions;include Test::Unit::CoreAssertions;require #{__FILE__.dump}
+ 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}
@@ -535,11 +586,11 @@ eom
refute_respond_to(obj, meth, msg)
end
- # pattern_list is an array which contains regexp and :*.
+ # pattern_list is an array which contains regexp, string and :*.
# :* means any sequence.
#
# pattern_list is anchored.
- # Use [:*, regexp, :*] for non-anchored match.
+ # Use [:*, regexp/string, :*] for non-anchored match.
def assert_pattern_list(pattern_list, actual, message=nil)
rest = actual
anchored = true
@@ -548,11 +599,13 @@ eom
anchored = false
else
if anchored
- match = /\A#{pattern}/.match(rest)
+ match = rest.rindex(pattern, 0)
else
- match = pattern.match(rest)
+ match = rest.index(pattern)
end
- unless match
+ if match
+ post_match = $~ ? $~.post_match : rest[match+pattern.size..-1]
+ else
msg = message(msg) {
expect_msg = "Expected #{mu_pp pattern}\n"
if /\n[^\n]/ =~ rest
@@ -569,7 +622,7 @@ eom
}
assert false, msg
end
- rest = match.post_match
+ rest = post_match
anchored = true
end
}
@@ -695,7 +748,7 @@ eom
msg = "exceptions on #{errs.length} threads:\n" +
errs.map {|t, err|
"#{t.inspect}:\n" +
- err.full_message(highlight: false, order: :top)
+ (err.respond_to?(:full_message) ? err.full_message(highlight: false, order: :top) : err.message)
}.join("\n---\n")
if message
msg = "#{message}\n#{msg}"
@@ -730,6 +783,62 @@ eom
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
+ 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
+ tbase = 10 ** Math.log10(tmax * ([(tmax / tmin), 2].max ** 2)).ceil
+ info = "(tmin: #{tmin}, tmax: #{tmax}, tbase: #{tbase})"
+
+ seq.each do |i|
+ next if i == first
+ t = tbase * i.fdiv(first)
+ *arg = pre.call(i)
+ message = "[#{i}]: in #{t}s #{info}"
+ Timeout.timeout(t, Timeout::Error, message) do
+ measure[arg, message]
+ end
+ end
+ end
+
def diff(exp, act)
require 'pp'
q = PP.new(+"")
diff --git a/tool/lib/envutil.rb b/tool/lib/envutil.rb
index e21305c9ef..642965047f 100644
--- a/tool/lib/envutil.rb
+++ b/tool/lib/envutil.rb
@@ -15,23 +15,22 @@ end
module EnvUtil
def rubybin
if ruby = ENV["RUBY"]
- return ruby
- end
- ruby = "ruby"
- exeext = RbConfig::CONFIG["EXEEXT"]
- rubyexe = (ruby + exeext if exeext and !exeext.empty?)
- 3.times do
- if File.exist? ruby and File.executable? ruby and !File.directory? ruby
- return File.expand_path(ruby)
- end
- if rubyexe and File.exist? rubyexe and File.executable? rubyexe
- return File.expand_path(rubyexe)
- end
- ruby = File.join("..", ruby)
- end
- if defined?(RbConfig.ruby)
+ ruby
+ elsif defined?(RbConfig.ruby)
RbConfig.ruby
else
+ ruby = "ruby"
+ exeext = RbConfig::CONFIG["EXEEXT"]
+ rubyexe = (ruby + exeext if exeext and !exeext.empty?)
+ 3.times do
+ if File.exist? ruby and File.executable? ruby and !File.directory? ruby
+ return File.expand_path(ruby)
+ end
+ if rubyexe and File.exist? rubyexe and File.executable? rubyexe
+ return File.expand_path(rubyexe)
+ end
+ ruby = File.join("..", ruby)
+ end
"ruby"
end
end
@@ -53,7 +52,14 @@ module EnvUtil
@original_internal_encoding = Encoding.default_internal
@original_external_encoding = Encoding.default_external
@original_verbose = $VERBOSE
- @original_warning = defined?(Warning.[]) ? %i[deprecated experimental].to_h {|i| [i, Warning[i]]} : nil
+ @original_warning =
+ if defined?(Warning.categories)
+ Warning.categories.to_h {|i| [i, Warning[i]]}
+ elsif defined?(Warning.[]) # 2.7+
+ %i[deprecated experimental performance].to_h do |i|
+ [i, begin Warning[i]; rescue ArgumentError; end]
+ end.compact
+ end
end
end
@@ -155,7 +161,7 @@ module EnvUtil
# remain env
%w(ASAN_OPTIONS RUBY_ON_BUG).each{|name|
- child_env[name] = ENV[name] if ENV[name]
+ child_env[name] = ENV[name] if !child_env.key?(name) and ENV.key?(name)
}
args = [args] if args.kind_of?(String)
@@ -246,6 +252,24 @@ module EnvUtil
end
module_function :under_gc_stress
+ def under_gc_compact_stress(val = :empty, &block)
+ raise "compaction doesn't work well on s390x. Omit the test in the caller." if RUBY_PLATFORM =~ /s390x/ # https://github.com/ruby/ruby/pull/5077
+ auto_compact = GC.auto_compact
+ GC.auto_compact = val
+ under_gc_stress(&block)
+ ensure
+ GC.auto_compact = auto_compact
+ end
+ module_function :under_gc_compact_stress
+
+ def without_gc
+ prev_disabled = GC.disable
+ yield
+ ensure
+ GC.enable unless prev_disabled
+ end
+ module_function :without_gc
+
def with_default_external(enc)
suppress_warning { Encoding.default_external = enc }
yield
@@ -297,16 +321,24 @@ module EnvUtil
cmd = @ruby_install_name if "ruby-runner#{RbConfig::CONFIG["EXEEXT"]}" == cmd
path = DIAGNOSTIC_REPORTS_PATH
timeformat = DIAGNOSTIC_REPORTS_TIMEFORMAT
- pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.crash"
+ pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.{crash,ips}"
first = true
30.times do
first ? (first = false) : sleep(0.1)
Dir.glob(pat) do |name|
log = File.read(name) rescue next
- if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log
- File.unlink(name)
- File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil
- return log
+ case name
+ when /\.crash\z/
+ if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log
+ File.unlink(name)
+ File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil
+ return log
+ end
+ when /\.ips\z/
+ if /^ *"pid" *: *#{pid},/ =~ log
+ File.unlink(name)
+ return log
+ end
end
end
end
diff --git a/tool/lib/iseq_loader_checker.rb b/tool/lib/iseq_loader_checker.rb
index 3f07b3a999..73784f8450 100644
--- a/tool/lib/iseq_loader_checker.rb
+++ b/tool/lib/iseq_loader_checker.rb
@@ -76,6 +76,15 @@ class RubyVM::InstructionSequence
# return value
i2_bin if CHECK_TO_BINARY
end if CHECK_TO_A || CHECK_TO_BINARY
+
+ if opt == "prism"
+ # If RUBY_ISEQ_DUMP_DEBUG is "prism", we'll set up
+ # InstructionSequence.load_iseq to intercept loading filepaths to compile
+ # using prism.
+ def self.load_iseq(filepath)
+ RubyVM::InstructionSequence.compile_file_prism(filepath)
+ end
+ end
end
#require_relative 'x'; exit(1)
diff --git a/tool/lib/leakchecker.rb b/tool/lib/leakchecker.rb
index 26d75b92fa..4cd28b9dd5 100644
--- a/tool/lib/leakchecker.rb
+++ b/tool/lib/leakchecker.rb
@@ -233,7 +233,13 @@ class LeakChecker
old_env = @env_info
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]
diff --git a/tool/lib/memory_status.rb b/tool/lib/memory_status.rb
index 5e9e80a68a..60632523a8 100644
--- a/tool/lib/memory_status.rb
+++ b/tool/lib/memory_status.rb
@@ -12,7 +12,7 @@ module Memory
PROC_FILE = procfile
VM_PAT = pat
def self.read_status
- IO.foreach(PROC_FILE, encoding: Encoding::ASCII_8BIT) do |l|
+ File.foreach(PROC_FILE, encoding: Encoding::ASCII_8BIT) do |l|
yield($1.downcase.intern, $2.to_i * 1024) if VM_PAT =~ l
end
end
diff --git a/tool/lib/output.rb b/tool/lib/output.rb
new file mode 100644
index 0000000000..8cb426ae4a
--- /dev/null
+++ b/tool/lib/output.rb
@@ -0,0 +1,70 @@
+require_relative 'vpath'
+require_relative 'colorize'
+
+class Output
+ attr_reader :path, :vpath
+
+ def initialize(path: nil, timestamp: nil, ifchange: nil, color: nil,
+ overwrite: false, create_only: false, vpath: VPath.new)
+ @path = path
+ @timestamp = timestamp
+ @ifchange = ifchange
+ @color = color
+ @overwrite = overwrite
+ @create_only = create_only
+ @vpath = vpath
+ end
+
+ COLOR_WHEN = {
+ 'always' => true, 'auto' => nil, 'never' => false,
+ nil => true, false => false,
+ }
+
+ def def_options(opt)
+ opt.separator(" Output common options:")
+ opt.on('-o', '--output=PATH') {|v| @path = v}
+ opt.on('-t', '--timestamp[=PATH]') {|v| @timestamp = v || true}
+ opt.on('-c', '--[no-]if-change') {|v| @ifchange = v}
+ opt.on('--[no-]color=[WHEN]', COLOR_WHEN.keys) {|v| @color = COLOR_WHEN[v]}
+ opt.on('--[no-]create-only') {|v| @create_only = v}
+ opt.on('--[no-]overwrite') {|v| @overwrite = v}
+ @vpath.def_options(opt)
+ end
+
+ def write(data, overwrite: @overwrite, create_only: @create_only)
+ unless @path
+ $stdout.print data
+ return true
+ end
+ color = Colorize.new(@color)
+ unchanged = color.pass("unchanged")
+ updated = color.fail("updated")
+ outpath = nil
+
+ if (@ifchange or overwrite or create_only) and (@vpath.open(@path, "rb") {|f|
+ outpath = f.path
+ if @ifchange or create_only
+ original = f.read
+ (@ifchange and original == data) or (create_only and !original.empty?)
+ end
+ } rescue false)
+ puts "#{outpath} #{unchanged}"
+ written = false
+ else
+ unless overwrite and outpath and (File.binwrite(outpath, data) rescue nil)
+ File.binwrite(outpath = @path, data)
+ end
+ puts "#{outpath} #{updated}"
+ written = true
+ end
+ if timestamp = @timestamp
+ if timestamp == true
+ dir, base = File.split(@path)
+ timestamp = File.join(dir, ".time." + base)
+ end
+ File.binwrite(timestamp, '')
+ File.utime(nil, nil, timestamp)
+ end
+ written
+ end
+end
diff --git a/tool/lib/path.rb b/tool/lib/path.rb
new file mode 100644
index 0000000000..5582b2851e
--- /dev/null
+++ b/tool/lib/path.rb
@@ -0,0 +1,101 @@
+module Path
+ module_function
+
+ def clean(path)
+ path = "#{path}/".gsub(/(\A|\/)(?:\.\/)+/, '\1').tr_s('/', '/')
+ nil while path.sub!(/[^\/]+\/\.\.\//, '')
+ path
+ end
+
+ def relative(path, base)
+ path = clean(path)
+ base = clean(base)
+ path, base = [path, base].map{|s|s.split("/")}
+ until path.empty? or base.empty? or path[0] != base[0]
+ path.shift
+ base.shift
+ end
+ path, base = [path, base].map{|s|s.join("/")}
+ if base.empty?
+ path
+ elsif base.start_with?("../") or File.absolute_path?(base)
+ File.expand_path(path)
+ else
+ base.gsub!(/[^\/]+/, '..')
+ File.join(base, path)
+ end
+ end
+
+ def clean_link(src, dest)
+ begin
+ link = File.readlink(dest)
+ rescue
+ else
+ return if link == src
+ File.unlink(dest)
+ end
+ yield src, dest
+ end
+
+ # Extensions to FileUtils
+
+ module Mswin
+ def ln_safe(src, dest, *opt)
+ cmd = ["mklink", dest.tr("/", "\\"), src.tr("/", "\\")]
+ cmd[1, 0] = opt
+ return if system("cmd", "/c", *cmd)
+ # TODO: use RUNAS or something
+ puts cmd.join(" ")
+ end
+
+ def ln_dir_safe(src, dest)
+ ln_safe(src, dest, "/d")
+ end
+ end
+
+ module HardlinkExcutable
+ def ln_exe(src, dest)
+ ln(src, dest, force: true)
+ end
+ end
+
+ def ln_safe(src, dest)
+ ln_sf(src, dest)
+ rescue Errno::ENOENT
+ # Windows disallows to create broken symboic links, probably because
+ # it is a kind of reparse points.
+ raise if File.exist?(src)
+ end
+
+ alias ln_dir_safe ln_safe
+ alias ln_exe ln_safe
+
+ def ln_relative(src, dest, executable = false)
+ return if File.identical?(src, dest)
+ parent = File.dirname(dest)
+ File.directory?(parent) or mkdir_p(parent)
+ if executable
+ return (ln_exe(src, dest) if File.exist?(src))
+ end
+ clean_link(relative(src, parent), dest) {|s, d| ln_safe(s, d)}
+ end
+
+ def ln_dir_relative(src, dest)
+ return if File.identical?(src, dest)
+ parent = File.dirname(dest)
+ File.directory?(parent) or mkdir_p(parent)
+ clean_link(relative(src, parent), dest) {|s, d| ln_dir_safe(s, d)}
+ end
+
+ case (CROSS_COMPILING || RUBY_PLATFORM)
+ when /linux|darwin|solaris/
+ prepend HardlinkExcutable
+ extend HardlinkExcutable
+ when /mingw|mswin/
+ unless File.respond_to?(:symlink)
+ prepend Mswin
+ extend Mswin
+ end
+ else
+ end
+end
diff --git a/tool/lib/test/unit.rb b/tool/lib/test/unit.rb
index ba3685d1ef..d758b5fb02 100644
--- a/tool/lib/test/unit.rb
+++ b/tool/lib/test/unit.rb
@@ -1,5 +1,20 @@
# frozen_string_literal: true
+# Enable deprecation warnings for test-all, so deprecated methods/constants/functions are dealt with early.
+Warning[:deprecated] = true
+
+if ENV['BACKTRACE_FOR_DEPRECATION_WARNINGS']
+ Warning.extend Module.new {
+ def warn(message, category: nil, **kwargs)
+ if category == :deprecated and $stderr.respond_to?(:puts)
+ $stderr.puts nil, message, caller, nil
+ else
+ super
+ end
+ end
+ }
+end
+
require_relative '../envutil'
require_relative '../colorize'
require_relative '../leakchecker'
@@ -9,42 +24,6 @@ require 'optparse'
# See Test::Unit
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
-
##
# Test::Unit is an implementation of the xUnit testing framework for Ruby.
module Unit
@@ -74,17 +53,7 @@ module Test
end
end
- module MJITFirst
- def group(list)
- # MJIT first
- mjit, others = list.partition {|e| /test_mjit/ =~ e}
- mjit + others
- end
- end
-
class Alpha < NoSort
- include MJITFirst
-
def sort_by_name(list)
list.sort_by(&:name)
end
@@ -97,8 +66,6 @@ module Test
# shuffle test suites based on CRC32 of their names
Shuffle = Struct.new(:seed, :salt) do
- include MJITFirst
-
def initialize(seed)
self.class::CRC_TBL ||= (0..255).map {|i|
(0..7).inject(i) {|c,| (c & 1 == 1) ? (0xEDB88320 ^ (c >> 1)) : (c >> 1) }
@@ -116,6 +83,10 @@ module Test
list.sort_by {|e| randomize_key(e)}
end
+ def group(list)
+ list
+ end
+
private
def crc32(str, crc32 = 0xffffffff)
@@ -200,8 +171,6 @@ module Test
" " + (s =~ /[\s|&<>$()]/ ? s.inspect : s)
}.join("\n")
- @failed_output = options[:stderr_on_failure] ? $stderr : $stdout
-
@options = options
end
@@ -273,10 +242,16 @@ module Test
@jobserver = nil
makeflags = ENV.delete("MAKEFLAGS")
if !options[:parallel] and
- /(?:\A|\s)--jobserver-(?:auth|fds)=(\d+),(\d+)/ =~ makeflags
+ /(?:\A|\s)--jobserver-(?:auth|fds)=(?:(\d+),(\d+)|fifo:((?:\\.|\S)+))/ =~ makeflags
begin
- r = IO.for_fd($1.to_i(10), "rb", autoclose: false)
- w = IO.for_fd($2.to_i(10), "wb", autoclose: false)
+ if fifo = $3
+ fifo.gsub!(/\\(?=.)/, '')
+ r = File.open(fifo, IO::RDONLY|IO::NONBLOCK|IO::BINARY)
+ w = File.open(fifo, IO::WRONLY|IO::NONBLOCK|IO::BINARY)
+ else
+ r = IO.for_fd($1.to_i(10), "rb", autoclose: false)
+ w = IO.for_fd($2.to_i(10), "wb", autoclose: false)
+ end
rescue
r.close if r
nil
@@ -284,7 +259,7 @@ module Test
r.close_on_exec = true
w.close_on_exec = true
@jobserver = [r, w]
- options[:parallel] ||= 1
+ options[:parallel] ||= 256 # number of tokens to acquire first
end
end
@worker_timeout = EnvUtil.apply_timeout_scale(options[:worker_timeout] || 180)
@@ -327,7 +302,8 @@ module Test
options[:retry] = false
end
- opts.on '--ruby VAL', "Path to ruby which is used at -j option" do |a|
+ opts.on '--ruby VAL', "Path to ruby which is used at -j option",
+ "Also used as EnvUtil.rubybin by some assertion methods" do |a|
options[:ruby] = a.split(/ /).reject(&:empty?)
end
@@ -677,10 +653,18 @@ 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
- timeout = [(@workers.filter_map {|w| w.response_at}.min&.-(Time.now) || 0) + @worker_timeout, 1].max
+ 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
if !(_io = IO.select(@ios, nil, nil, timeout))
timeout = Time.now - @worker_timeout
@@ -694,15 +678,9 @@ module Test
}
break
end
- break if @tasks.empty? and @workers.empty?
- if @jobserver and @job_tokens and !@tasks.empty? and
- ((newjobs = [@tasks.size, @options[:parallel]].min) > @workers.size or
- !@workers.any? {|x| x.status == :ready})
- t = @jobserver[0].read_nonblock(newjobs, 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
@@ -710,7 +688,7 @@ module Test
return result
ensure
if file = @options[:timetable_data]
- open(file, 'w'){|f|
+ File.open(file, 'w'){|f|
@records.each{|(worker, suite), (st, ed)|
f.puts '[' + [worker.dump, suite.dump, st.to_f * 1_000, ed.to_f * 1_000].join(", ") + '],'
}
@@ -738,7 +716,15 @@ module Test
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)
@@ -774,7 +760,7 @@ module Test
unless rep.empty?
rep.each do |r|
if r[:error]
- puke(*r[:error], Timeout::Error)
+ puke(*r[:error], Timeout::Error.new)
next
end
r[:report]&.each do |f|
@@ -794,6 +780,7 @@ module Test
warn ""
@warnings.uniq! {|w| w[1].message}
@warnings.each do |w|
+ @errors += 1
warn "#{w[0]}: #{w[1].message} (#{w[1].class})"
end
warn ""
@@ -863,7 +850,7 @@ module Test
end
end
- def record(suite, method, assertions, time, error)
+ def record(suite, method, assertions, time, error, source_location = nil)
if @options.values_at(:longest, :most_asserted).any?
@tops ||= {}
rec = [suite.name, method, assertions, time, error]
@@ -961,7 +948,7 @@ module Test
end
def _prepare_run(suites, type)
- options[:job_status] ||= :replace if @tty && !@verbose
+ options[:job_status] ||= @tty ? :replace : :normal unless @verbose
case options[:color]
when :always
color = true
@@ -977,11 +964,14 @@ module Test
@output = Output.new(self) unless @options[:testing]
filter = options[:filter]
type = "#{type}_methods"
- total = if filter
- suites.inject(0) {|n, suite| n + suite.send(type).grep(filter).size}
- else
- suites.inject(0) {|n, suite| n + suite.send(type).size}
- end
+ total = suites.sum {|suite|
+ methods = suite.send(type)
+ if filter
+ methods.count {|method| filter === "#{suite}##{method}"}
+ else
+ methods.size
+ end
+ }
@test_count = 0
@total_tests = total.to_s(10)
end
@@ -1075,7 +1065,7 @@ module Test
runner.add_status(" = #$1")
when /\A\.+\z/
runner.succeed
- when /\A\.*[EFS][EFS.]*\z/
+ when /\A\.*[EFST][EFST.]*\z/
runner.failed(s)
else
$stdout.print(s)
@@ -1121,9 +1111,6 @@ module Test
parser.on '-x', '--exclude REGEXP', 'Exclude test files on pattern.' do |pattern|
(options[:reject] ||= []) << pattern
end
- parser.on '--stderr-on-failure', 'Use stderr to print failure messages' do
- options[:stderr_on_failure] = true
- end
end
def complement_test_name f, orig_f
@@ -1183,6 +1170,28 @@ module Test
end
end
+ module OutputOption # :nodoc: all
+ def setup_options(parser, options)
+ super
+ parser.separator "output options:"
+
+ options[:failed_output] = $stdout
+ parser.on '--stderr-on-failure', 'Use stderr to print failure messages' do
+ options[:failed_output] = $stderr
+ end
+ parser.on '--stdout-on-failure', 'Use stdout to print failure messages', '(default)' do
+ options[:failed_output] = $stdout
+ end
+ end
+
+ def process_args(args = [])
+ return @options if @options
+ options = super
+ @failed_output = options[:failed_output]
+ options
+ end
+ end
+
module GCOption # :nodoc: all
def setup_options(parser, options)
super
@@ -1244,8 +1253,13 @@ module Test
puts "#{f}: #{$!}"
end
}
+ @load_failed = errors.size.nonzero?
result
end
+
+ def run(*)
+ super or @load_failed
+ end
end
module RepeatOption # :nodoc: all
@@ -1347,6 +1361,182 @@ module Test
end
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 'json'
+ require 'uri'
+ options[:launchable_test_reports] = writer = JsonStreamWriter.new(path)
+ writer.write_array('testCases')
+ main_pid = Process.pid
+ at_exit {
+ # This block is executed when the fork block in a test is completed.
+ # Therefore, we need to verify whether all tests have been completed.
+ stack = caller
+ if stack.size == 0 && main_pid == Process.pid && $!.is_a?(SystemExit)
+ writer.close
+ end
+ }
+ end
+
+ def encode_test_path_component component
+ component.to_s.gsub('%', '%25').gsub('=', '%3D').gsub('#', '%23').gsub('&', '%26')
+ end
+ end
+
+ ##
+ # JsonStreamWriter writes a JSON file using a stream.
+ # By utilizing a stream, we can minimize memory usage, especially for large files.
+ class JsonStreamWriter
+ def initialize(path)
+ @file = File.open(path, "w")
+ @file.write("{")
+ @indent_level = 0
+ @is_first_key_val = true
+ @is_first_obj = true
+ write_new_line
+ end
+
+ def write_object obj
+ if @is_first_obj
+ @is_first_obj = false
+ else
+ write_comma
+ write_new_line
+ end
+ @indent_level += 1
+ @file.write(to_json_str(obj))
+ @indent_level -= 1
+ @is_first_key_val = true
+ # Occasionally, invalid JSON will be created as shown below, especially when `--repeat-count` is specified.
+ # {
+ # "testPath": "file=test%2Ftest_timeout.rb&class=TestTimeout&testcase=test_allows_zero_seconds",
+ # "status": "TEST_PASSED",
+ # "duration": 2.7e-05,
+ # "createdAt": "2024-02-09 12:21:07 +0000",
+ # "stderr": null,
+ # "stdout": null
+ # }: null <- here
+ # },
+ # To prevent this, IO#flush is called here.
+ @file.flush
+ end
+
+ def write_array(key)
+ @indent_level += 1
+ @file.write(to_json_str(key))
+ write_colon
+ @file.write(" ", "[")
+ write_new_line
+ end
+
+ def close
+ return if @file.closed?
+ close_array
+ @indent_level -= 1
+ write_new_line
+ @file.write("}", "\n")
+ @file.flush
+ @file.close
+ end
+
+ private
+ def to_json_str(obj)
+ json = JSON.pretty_generate(obj)
+ json.gsub(/^/, ' ' * (2 * @indent_level))
+ end
+
+ def write_indent
+ @file.write(" " * 2 * @indent_level)
+ end
+
+ def write_new_line
+ @file.write("\n")
+ end
+
+ def write_comma
+ @file.write(',')
+ end
+
+ def write_colon
+ @file.write(":")
+ end
+
+ def close_array
+ write_new_line
+ write_indent
+ @file.write("]")
+ @indent_level -= 1
+ end
+ end
+ end
+
class Runner # :nodoc: all
attr_accessor :report, :failures, :errors, :skips # :nodoc:
@@ -1536,7 +1726,7 @@ module Test
_start_method(inst)
inst._assertions = 0
- print "#{suite}##{method} = " if @verbose
+ print "#{suite}##{method.inspect.sub(/\A:/, '')} = " if @verbose
start_time = Time.now if @verbose
result =
@@ -1551,7 +1741,7 @@ module Test
puts if @verbose
$stdout.flush
- unless defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? # compiler process is wrongly considered as leak
+ unless defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? # compiler process is wrongly considered as leak
leakchecker.check("#{inst.class}\##{inst.__name__}")
end
@@ -1584,16 +1774,16 @@ module Test
# failure or error in teardown, it will be sent again with the
# error or failure.
- def record suite, method, assertions, time, error
+ def record suite, method, assertions, time, error, source_location = nil
end
def location e # :nodoc:
last_before_assertion = ""
- return '<empty>' unless e.backtrace # SystemStackError can return nil.
+ return '<empty>' unless e&.backtrace # SystemStackError can return nil.
e.backtrace.reverse_each do |s|
- break if s =~ /in .(assert|refute|flunk|pass|fail|raise|must|wont)/
+ break if s =~ /in .(?:Test::Unit::(?:Core)?Assertions#)?(assert|refute|flunk|pass|fail|raise|must|wont)/
last_before_assertion = s
end
last_before_assertion.sub(/:in .*$/, '')
@@ -1642,7 +1832,7 @@ module Test
break unless report.empty?
end
- return failures + errors if self.test_count > 0 # or return nil...
+ return (failures + errors).nonzero? # or return nil...
rescue Interrupt
abort 'Interrupted'
end
@@ -1668,12 +1858,14 @@ module Test
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.
@@ -1709,6 +1901,9 @@ module Test
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 "
@@ -1727,6 +1922,7 @@ module Test
class AutoRunner # :nodoc: all
class Runner < Test::Unit::Runner
include Test::Unit::RequireFiles
+ include Test::Unit::LaunchableOption
end
attr_accessor :to_run, :options
diff --git a/tool/lib/test/unit/parallel.rb b/tool/lib/test/unit/parallel.rb
index b3a8957f26..ac297d4a0e 100644
--- a/tool/lib/test/unit/parallel.rb
+++ b/tool/lib/test/unit/parallel.rb
@@ -1,12 +1,6 @@
# frozen_string_literal: true
-$LOAD_PATH.unshift "#{__dir__}/../.."
-require_relative '../../test/unit'
-require_relative '../../profile_test_all' if ENV.key?('RUBY_TEST_ALL_PROFILE')
-require_relative '../../tracepointchecker'
-require_relative '../../zombie_hunter'
-require_relative '../../iseq_loader_checker'
-require_relative '../../gc_checker'
+require_relative "../../../test/init"
module Test
module Unit
@@ -186,7 +180,7 @@ module Test
else
error = ProxyError.new(error)
end
- _report "record", Marshal.dump([suite.name, method, assertions, time, error])
+ _report "record", Marshal.dump([suite.name, method, assertions, time, error, suite.instance_method(method).source_location])
super
end
end
@@ -208,5 +202,9 @@ if $0 == __FILE__
end
end
require 'rubygems'
+ begin
+ require 'rake'
+ rescue LoadError
+ end
Test::Unit::Worker.new.run(ARGV)
end
diff --git a/tool/lib/test/unit/testcase.rb b/tool/lib/test/unit/testcase.rb
index 44d9ba7fdb..51ffff37eb 100644
--- a/tool/lib/test/unit/testcase.rb
+++ b/tool/lib/test/unit/testcase.rb
@@ -137,6 +137,9 @@ module Test
attr_reader :__name__ # :nodoc:
+ # Method name of this test.
+ alias method_name __name__
+
PASSTHROUGH_EXCEPTIONS = [NoMemoryError, SignalException,
Interrupt, SystemExit] # :nodoc:
@@ -144,8 +147,7 @@ module Test
# Runs the tests reporting the status to +runner+
def run runner
- @options = runner.options
-
+ @__runner_options__ = runner.options
trap "INFO" do
runner.report.each_with_index do |msg, i|
warn "\n%3d) %s" % [i + 1, msg]
@@ -161,7 +163,7 @@ module Test
result = ""
begin
- @passed = nil
+ @__passed__ = nil
self.before_setup
self.setup
self.after_setup
@@ -169,11 +171,11 @@ module Test
result = "." unless io?
time = Time.now - start_time
runner.record self.class, self.__name__, self._assertions, time, nil
- @passed = true
+ @__passed__ = true
rescue *PASSTHROUGH_EXCEPTIONS
raise
rescue Exception => e
- @passed = Test::Unit::PendedError === 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
@@ -184,7 +186,7 @@ module Test
rescue *PASSTHROUGH_EXCEPTIONS
raise
rescue Exception => e
- @passed = false
+ @__passed__ = false
runner.record self.class, self.__name__, self._assertions, time, e
result = runner.puke self.class, self.__name__, e
end
@@ -206,12 +208,12 @@ module Test
def initialize name # :nodoc:
@__name__ = name
@__io__ = nil
- @passed = nil
- @@current = self # FIX: make thread local
+ @__passed__ = nil
+ @@__current__ = self # FIX: make thread local
end
def self.current # :nodoc:
- @@current # FIX: make thread local
+ @@__current__ # FIX: make thread local
end
##
@@ -263,7 +265,7 @@ module Test
# Returns true if the test passed.
def passed?
- @passed
+ @__passed__
end
##
diff --git a/tool/lib/vcs.rb b/tool/lib/vcs.rb
index 29be8db3b4..3894f9c8e8 100644
--- a/tool/lib/vcs.rb
+++ b/tool/lib/vcs.rb
@@ -1,6 +1,8 @@
# vcs
require 'fileutils'
require 'optparse'
+require 'pp'
+require 'tempfile'
# This library is used by several other tools/ scripts to detect the current
# VCS in use (e.g. SVN, Git) or to interact with that VCS.
@@ -9,6 +11,22 @@ ENV.delete('PWD')
class VCS
DEBUG_OUT = STDERR.dup
+
+ def self.dump(obj, pre = nil)
+ out = DEBUG_OUT
+ @pp ||= PP.new(out)
+ @pp.guard_inspect_key do
+ if pre
+ @pp.group(pre.size, pre) {
+ obj.pretty_print(@pp)
+ }
+ else
+ obj.pretty_print(@pp)
+ end
+ @pp.flush
+ out << "\n"
+ end
+ end
end
unless File.respond_to? :realpath
@@ -19,14 +37,14 @@ unless File.respond_to? :realpath
end
def IO.pread(*args)
- VCS::DEBUG_OUT.puts(args.inspect) if $DEBUG
+ VCS.dump(args, "args: ") if $DEBUG
popen(*args) {|f|f.read}
end
module DebugPOpen
refine IO.singleton_class do
def popen(*args)
- VCS::DEBUG_OUT.puts args.inspect if $DEBUG
+ VCS.dump(args, "args: ") if $DEBUG
super
end
end
@@ -34,7 +52,7 @@ end
using DebugPOpen
module DebugSystem
def system(*args)
- VCS::DEBUG_OUT.puts args.inspect if $DEBUG
+ VCS.dump(args, "args: ") if $DEBUG
exception = false
opts = Hash.try_convert(args[-1])
if RUBY_VERSION >= "2.6"
@@ -69,6 +87,9 @@ class VCS
begin
@@dirs.each do |dir, klass, pred|
if pred ? pred[curr, dir] : File.directory?(File.join(curr, dir))
+ if klass.const_defined?(:COMMAND)
+ IO.pread([{'LANG' => 'C', 'LC_ALL' => 'C'}, klass::COMMAND, "--version"]) rescue next
+ end
vcs = klass.new(curr)
vcs.define_options(parser) if parser
vcs.set_options(options)
@@ -92,9 +113,23 @@ class VCS
parser.separator(" VCS common options:")
parser.define("--[no-]dryrun") {|v| opts[:dryrun] = v}
parser.define("--[no-]debug") {|v| opts[:debug] = v}
+ parser.define("-z", "--zone=OFFSET", /\A[-+]\d\d:\d\d\z/) {|v| opts[:zone] = v}
opts
end
+ def release_date(time)
+ t = time.getlocal(@zone)
+ [
+ t.strftime('#define RUBY_RELEASE_YEAR %Y'),
+ t.strftime('#define RUBY_RELEASE_MONTH %-m'),
+ t.strftime('#define RUBY_RELEASE_DAY %-d'),
+ ]
+ end
+
+ def self.short_revision(rev)
+ rev
+ end
+
attr_reader :srcdir
def initialize(path)
@@ -112,14 +147,14 @@ class VCS
def set_options(opts)
@debug = opts.fetch(:debug) {$DEBUG}
@dryrun = opts.fetch(:dryrun) {@debug}
+ @zone = opts.fetch(:zone) {'+09:00'}
end
attr_reader :dryrun, :debug
alias dryrun? dryrun
alias debug? debug
- NullDevice = defined?(IO::NULL) ? IO::NULL :
- %w[/dev/null NUL NIL: NL:].find {|dev| File.exist?(dev)}
+ NullDevice = IO::NULL
# returns
# * the last revision of the current branch
@@ -159,6 +194,7 @@ class VCS
rescue ArgumentError
modified = Time.utc(*$~[1..6]) + $7.to_i * 3600 + $8.to_i * 60
end
+ modified = modified.getlocal(@zone)
end
return last, changed, modified, *rest
end
@@ -190,6 +226,7 @@ class VCS
def after_export(dir)
FileUtils.rm_rf(Dir.glob("#{dir}/.git*"))
+ FileUtils.rm_rf(Dir.glob("#{dir}/.mailmap"))
end
def revision_handler(rev)
@@ -204,6 +241,36 @@ class VCS
revision_handler(rev).short_revision(rev)
end
+ # make-snapshot generates only release_date whereas file2lastrev generates both release_date and release_datetime
+ def revision_header(last, release_date, release_datetime = nil, branch = nil, title = nil, limit: 20)
+ short = short_revision(last)
+ if /[^\x00-\x7f]/ =~ title and title.respond_to?(:force_encoding)
+ title = title.dup.force_encoding("US-ASCII")
+ end
+ code = [
+ "#define RUBY_REVISION #{short.inspect}",
+ ]
+ unless short == last
+ code << "#define RUBY_FULL_REVISION #{last.inspect}"
+ end
+ if branch
+ e = '..'
+ name = branch.sub(/\A(.{#{limit-e.size}}).{#{e.size+1},}/o) {$1+e}
+ name = name.dump.sub(/\\#/, '#')
+ code << "#define RUBY_BRANCH_NAME #{name}"
+ end
+ if title
+ title = title.dump.sub(/\\#/, '#')
+ code << "#define RUBY_LAST_COMMIT_TITLE #{title}"
+ end
+ if release_datetime
+ t = release_datetime.utc
+ code << t.strftime('#define RUBY_RELEASE_DATETIME "%FT%TZ"')
+ end
+ code += self.release_date(release_date)
+ code
+ end
+
class SVN < self
register(".svn")
COMMAND = ENV['SVN'] || 'svn'
@@ -212,10 +279,6 @@ class VCS
"r#{rev}"
end
- def self.short_revision(rev)
- rev
- end
-
def _get_revisions(path, srcdir = nil)
if srcdir and self.class.local_path?(path)
path = File.join(srcdir, path)
@@ -350,7 +413,7 @@ class VCS
def commit
args = %W"#{COMMAND} commit"
if dryrun?
- VCS::DEBUG_OUT.puts(args.inspect)
+ VCS.dump(args, "commit: ")
return true
end
system(*args)
@@ -358,8 +421,21 @@ class VCS
end
class GIT < self
- register(".git") {|path, dir| File.exist?(File.join(path, dir))}
- COMMAND = ENV["GIT"] || 'git'
+ register(".git") do |path, dir|
+ SAFE_DIRECTORIES ||=
+ begin
+ command = ENV["GIT"] || 'git'
+ dirs = IO.popen(%W"#{command} config --global --get-all safe.directory", &:read).split("\n")
+ rescue
+ command = nil
+ dirs = []
+ ensure
+ VCS.dump(dirs, "safe.directory: ") if $DEBUG
+ COMMAND = command
+ end
+
+ COMMAND and File.exist?(File.join(path, dir))
+ end
def cmd_args(cmds, srcdir = nil)
(opts = cmds.last).kind_of?(Hash) or cmds << (opts = {})
@@ -367,7 +443,7 @@ class VCS
if srcdir
opts[:chdir] ||= srcdir
end
- VCS::DEBUG_OUT.puts cmds.inspect if debug?
+ VCS.dump(cmds, "cmds: ") if debug? and !$DEBUG
cmds
end
@@ -377,7 +453,7 @@ class VCS
def cmd_read_at(srcdir, cmds)
result = without_gitconfig { IO.pread(*cmd_args(cmds, srcdir)) }
- VCS::DEBUG_OUT.puts result.inspect if debug?
+ VCS.dump(result, "result: ") if debug?
result
end
@@ -398,7 +474,14 @@ class VCS
def _get_revisions(path, srcdir = nil)
ref = Branch === path ? path.to_str : 'HEAD'
gitcmd = [COMMAND]
- last = cmd_read_at(srcdir, [[*gitcmd, 'rev-parse', ref]]).rstrip
+ last = nil
+ IO.pipe do |r, w|
+ last = cmd_read_at(srcdir, [[*gitcmd, 'rev-parse', ref, err: w]]).rstrip
+ w.close
+ unless r.eof?
+ raise "#{COMMAND} rev-parse failed\n#{r.read.gsub(/^(?=\s*\S)/, ' ')}"
+ end
+ end
log = cmd_read_at(srcdir, [[*gitcmd, 'log', '-n1', '--date=iso', '--pretty=fuller', *path]])
changed = log[/\Acommit (\h+)/, 1]
modified = log[/^CommitDate:\s+(.*)/, 1]
@@ -460,19 +543,35 @@ class VCS
end
def without_gitconfig
- envs = %w'HOME XDG_CONFIG_HOME GIT_SYSTEM_CONFIG GIT_CONFIG_SYSTEM'.each_with_object({}) do |v, h|
+ envs = (%w'HOME XDG_CONFIG_HOME' + ENV.keys.grep(/\AGIT_/)).each_with_object({}) do |v, h|
h[v] = ENV.delete(v)
- ENV[v] = NullDevice if v.start_with?('GIT_')
end
+ ENV['GIT_CONFIG_SYSTEM'] = NullDevice
+ ENV['GIT_CONFIG_GLOBAL'] = global_config
yield
ensure
ENV.update(envs)
end
+ def global_config
+ return NullDevice if SAFE_DIRECTORIES.empty?
+ unless @gitconfig
+ @gitconfig = Tempfile.new(%w"vcs_ .gitconfig")
+ @gitconfig.close
+ ENV['GIT_CONFIG_GLOBAL'] = @gitconfig.path
+ SAFE_DIRECTORIES.each do |dir|
+ system(*%W[#{COMMAND} config --global --add safe.directory #{dir}])
+ end
+ VCS.dump(`#{COMMAND} config --global --get-all safe.directory`, "safe.directory: ") if debug?
+ end
+ @gitconfig.path
+ end
+
def initialize(*)
super
@srcdir = File.realpath(@srcdir)
- VCS::DEBUG_OUT.puts @srcdir.inspect if debug?
+ @gitconfig = nil
+ VCS.dump(@srcdir, "srcdir: ") if debug?
self
end
@@ -582,7 +681,10 @@ class VCS
def format_changelog(path, arg, base_url = nil)
env = {'TZ' => 'JST-9', 'LANG' => 'C', 'LC_ALL' => 'C'}
- cmd = %W"#{COMMAND} log --format=fuller --notes=commits --notes=log-fix --topo-order --no-merges"
+ cmd = %W[#{COMMAND} log
+ --format=fuller --notes=commits --notes=log-fix --topo-order --no-merges
+ --fixed-strings --invert-grep --grep=[ci\ skip] --grep=[skip\ ci]
+ ]
date = "--date=iso-local"
unless system(env, *cmd, date, "-1", chdir: @srcdir, out: NullDevice, exception: false)
date = "--date=iso"
@@ -595,20 +697,49 @@ class VCS
cmd_pipe(env, cmd, chdir: @srcdir) do |r|
while s = r.gets("\ncommit ")
h, s = s.split(/^$/, 2)
+
+ next if /^Author: *dependabot\[bot\]/ =~ h
+
h.gsub!(/^(?:(?:Author|Commit)(?:Date)?|Date): /, ' \&')
if s.sub!(/\nNotes \(log-fix\):\n((?: +.*\n)+)/, '')
fix = $1
s = s.lines
fix.each_line do |x|
+ next unless x.sub!(/^(\s+)(?:(\d+)|\$(?:-\d+)?)/, '')
+ b = ($2&.to_i || (s.size - 1 + $3.to_i))
+ sp = $1
+ if x.sub!(/^,(?:(\d+)|\$(?:-\d+)?)/, '')
+ range = b..($1&.to_i || (s.size - 1 + $2.to_i))
+ else
+ range = b..b
+ end
case x
- when %r[^ +(\d+)s([#{LOG_FIX_REGEXP_SEPARATORS}])(.+)\2(.*)\2]o
- n = $1.to_i
- wrong = $3
- correct = $4
- begin
- s[n][wrong] = correct
- rescue IndexError
- message = ["format_changelog failed to replace #{wrong.dump} with #{correct.dump} at #$1\n"]
+ when %r[^s([#{LOG_FIX_REGEXP_SEPARATORS}])(.+)\1(.*)\1([gr]+)?]o
+ wrong = $2
+ correct = $3
+ if opt = $4 and opt.include?("r") # regexp
+ wrong = Regexp.new(wrong)
+ correct.gsub!(/(?<!\\)(?:\\\\)*\K(?:\\n)+/) {"\n" * ($&.size / 2)}
+ sub = opt.include?("g") ? :gsub! : :sub!
+ else
+ sub = false
+ end
+ range.each do |n|
+ if sub
+ ss = s[n].sub(/^#{sp}/, "") # un-indent for /^/
+ if ss.__send__(sub, wrong, correct)
+ s[n, 1] = ss.lines.map {|l| "#{sp}#{l}"}
+ next
+ end
+ else
+ begin
+ s[n][wrong] = correct
+ rescue IndexError
+ else
+ next
+ end
+ end
+ message = ["format_changelog failed to replace #{wrong.dump} with #{correct.dump} at #{n}\n"]
from = [1, n-2].max
to = [s.size-1, n+2].min
s.each_with_index do |e, i|
@@ -618,12 +749,13 @@ class VCS
end
raise message.join('')
end
- when %r[^( +)(\d+)i([#{LOG_FIX_REGEXP_SEPARATORS}])(.*)\3]o
- s[$2.to_i, 0] = "#{$1}#{$4}\n"
- when %r[^ +(\d+)(?:,(\d+))?d]
- n = $1.to_i
- e = $2
- s[n..(e ? e.to_i : n)] = []
+ when %r[^i([#{LOG_FIX_REGEXP_SEPARATORS}])(.*)\1]o
+ insert = "#{sp}#{$2}\n"
+ range.reverse_each do |n|
+ s[n, 0] = insert
+ end
+ when %r[^d]
+ s[range] = []
end
end
s = s.join('')
@@ -631,7 +763,7 @@ class VCS
if %r[^ +(https://github\.com/[^/]+/[^/]+/)commit/\h+\n(?=(?: +\n(?i: +Co-authored-by: .*\n)+)?(?:\n|\Z))] =~ s
issue = "#{$1}pull/"
- s.gsub!(/\b[Ff]ix(?:e[sd])? \K#(?=\d+)/) {issue}
+ s.gsub!(/\b(?:(?i:fix(?:e[sd])?) +|GH-)\K#(?=\d+\b)|\(\K#(?=\d+\))/) {issue}
end
s.gsub!(/ +\n/, "\n")
@@ -677,13 +809,13 @@ class VCS
def commit(opts = {})
args = [COMMAND, "push"]
- args << "-n" if dryrun
+ args << "-n" if dryrun?
remote, branch = upstream
args << remote
branches = %W[refs/notes/commits:refs/notes/commits HEAD:#{branch}]
if dryrun?
branches.each do |b|
- VCS::DEBUG_OUT.puts((args + [b]).inspect)
+ VCS.dump(args + [b], "commit: ")
end
return true
end
@@ -713,7 +845,7 @@ class VCS
commits.each_with_index do |l, i|
r, a, c = l.split(' ')
dcommit = [COMMAND, "svn", "dcommit"]
- dcommit.insert(-2, "-n") if dryrun
+ dcommit.insert(-2, "-n") if dryrun?
dcommit << "--add-author-from" unless a == c
dcommit << r
system(*dcommit) or return false
@@ -733,4 +865,15 @@ class VCS
true
end
end
+
+ class Null < self
+ def get_revisions(path, srcdir = nil)
+ @modified ||= Time.now - 10
+ return nil, nil, @modified
+ end
+
+ def revision_header(last, release_date, release_datetime = nil, branch = nil, title = nil, limit: 20)
+ self.release_date(release_date)
+ end
+ end
end
diff --git a/tool/lib/vpath.rb b/tool/lib/vpath.rb
index 48ab148405..fa819f3242 100644
--- a/tool/lib/vpath.rb
+++ b/tool/lib/vpath.rb
@@ -53,10 +53,11 @@ class VPath
end
def def_options(opt)
+ opt.separator(" VPath common options:")
opt.on("-I", "--srcdir=DIR", "add a directory to search path") {|dir|
@additional << dir
}
- opt.on("-L", "--vpath=PATH LIST", "add directories to search path") {|dirs|
+ opt.on("-L", "--vpath=PATH-LIST", "add directories to search path") {|dirs|
@additional << [dirs]
}
opt.on("--path-separator=SEP", /\A(?:\W\z|\.(\W).+)/, "separator for vpath") {|sep, vsep|
@@ -80,6 +81,10 @@ class VPath
@list
end
+ def add(path)
+ @additional << path
+ end
+
def strip(path)
prefix = list.map {|dir| Regexp.quote(dir)}
path.sub(/\A#{prefix.join('|')}(?:\/|\z)/, '')
diff --git a/tool/lib/webrick/httprequest.rb b/tool/lib/webrick/httprequest.rb
index d34eac7ecf..258ee37a38 100644
--- a/tool/lib/webrick/httprequest.rb
+++ b/tool/lib/webrick/httprequest.rb
@@ -402,7 +402,7 @@ module WEBrick
# This method provides the metavariables defined by the revision 3
# of "The WWW Common Gateway Interface Version 1.1"
# To browse the current document of CGI Version 1.1, see below:
- # http://tools.ietf.org/html/rfc3875
+ # https://www.rfc-editor.org/rfc/rfc3875
def meta_vars
meta = Hash.new
diff --git a/tool/lib/webrick/httpserver.rb b/tool/lib/webrick/httpserver.rb
index e85d059319..f3f948da3b 100644
--- a/tool/lib/webrick/httpserver.rb
+++ b/tool/lib/webrick/httpserver.rb
@@ -9,7 +9,6 @@
#
# $IPR: httpserver.rb,v 1.63 2002/10/01 17:16:32 gotoyuzo Exp $
-require 'io/wait'
require_relative 'server'
require_relative 'httputils'
require_relative 'httpstatus'
diff --git a/tool/lib/webrick/httputils.rb b/tool/lib/webrick/httputils.rb
index f1b9ddf9f0..e21284ee7f 100644
--- a/tool/lib/webrick/httputils.rb
+++ b/tool/lib/webrick/httputils.rb
@@ -112,7 +112,7 @@ module WEBrick
def load_mime_types(file)
# note: +file+ may be a "| command" for now; some people may
# rely on this, but currently we do not use this method by default.
- open(file){ |io|
+ File.open(file){ |io|
hash = Hash.new
io.each{ |line|
next if /^#/ =~ line