summaryrefslogtreecommitdiff
path: root/tool/lib
diff options
context:
space:
mode:
Diffstat (limited to 'tool/lib')
-rw-r--r--tool/lib/_tmpdir.rb115
-rw-r--r--tool/lib/bundled_gem.rb49
-rw-r--r--tool/lib/colorize.rb72
-rw-r--r--tool/lib/core_assertions.rb84
-rw-r--r--tool/lib/envutil.rb21
-rw-r--r--tool/lib/leakchecker.rb36
-rw-r--r--tool/lib/memory_status.rb100
-rw-r--r--tool/lib/output.rb13
-rw-r--r--tool/lib/test/jobserver.rb47
-rw-r--r--tool/lib/test/unit.rb24
-rw-r--r--tool/lib/test/unit/assertions.rb10
-rw-r--r--tool/lib/vcs.rb75
12 files changed, 438 insertions, 208 deletions
diff --git a/tool/lib/_tmpdir.rb b/tool/lib/_tmpdir.rb
index daa1a1f235..ac5b9be792 100644
--- a/tool/lib/_tmpdir.rb
+++ b/tool/lib/_tmpdir.rb
@@ -28,66 +28,71 @@ END {
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"
+ unless $no_report_tmpdir ||= nil
+ 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
- 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)]
+ 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
- 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
+ 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)
+ 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
- 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.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
+ rescue Errno::EACCES
+ # On Windows, a killed process may still hold file locks briefly.
+ # Ignore and let FileUtils.rm_rf handle it below.
end
end
end
diff --git a/tool/lib/bundled_gem.rb b/tool/lib/bundled_gem.rb
index d2ed61a508..ad103825bc 100644
--- a/tool/lib/bundled_gem.rb
+++ b/tool/lib/bundled_gem.rb
@@ -16,6 +16,13 @@ module BundledGem
"psych" # rdoc
]
+ def self.command(gem, cmd)
+ if stub = Gem::Specification.latest_spec_for(gem)
+ spec = stub.spec
+ File.join(spec.gem_dir, spec.bindir, cmd)
+ end
+ end
+
module_function
def unpack(file, *rest)
@@ -123,4 +130,46 @@ module BundledGem
command = "#{git} checkout --detach #{rev}"
system(command, chdir: gemdir) or raise "failed: #{command}"
end
+
+ class GemspecLoader
+ module NoPipe
+ refine IO.singleton_class do
+ def popen(...) ""; end
+ end
+ end
+ using NoPipe
+
+ def `(command) ""; end
+
+ def load_gemspec(file)
+ code = File.read(file, encoding: "utf-8:-")
+ eval(code, binding, file)
+ rescue
+ nil
+ end
+ end
+
+ def load_gemspec(g)
+ spec = GemspecLoader.new.load_gemspec(g)
+ spec.files.clear
+ spec.extensions.clear
+ src = spec.to_ruby
+ src.sub!(/^$$/) {
+ %[# default: #{g} #{File.mtime(g).strftime(%[%s.%N])}\n]
+ }
+ return spec.full_name+'.gemspec', src
+ end
+
+ def update_default_gemspecs(basedirs, out, quiet: true)
+ basedirs.each do |basedir|
+ Dir.glob(basedir+'/**/*.gemspec') do |g|
+ name, src = BundledGem.load_gemspec(g)
+ unless src
+ puts "Ignoring #{g}" unless quiet
+ next
+ end
+ out.write(src, name: name, newer: File.mtime(g), quiet: quiet)
+ end
+ end
+ end
end
diff --git a/tool/lib/colorize.rb b/tool/lib/colorize.rb
index 0904312119..89da90e075 100644
--- a/tool/lib/colorize.rb
+++ b/tool/lib/colorize.rb
@@ -1,57 +1,78 @@
# frozen-string-literal: true
+# Decorate TTY output using ANSI Select Graphic Rendition control
+# sequences.
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
- @color = opts && opts[:color] || color
+ #
+ # Creates and load color settings.
+ def initialize(_color = nil, color: _color, colors_file: nil)
+ @colors = nil
+ @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)
+ 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]
+ colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(COLORS_PATTERN)] : {}
+ if colors_file
begin
- File.read(colors_file).scan(/(\w+)=([^:\n]*)/) do |n, c|
+ File.read(colors_file).scan(COLORS_PATTERN) do |n, c|
colors[n] ||= c
end
rescue Errno::ENOENT
end
end
@colors = colors
- @reset = "#{@beg}m"
end
end
self
end
+ COLORS_PATTERN = /(\w+)=([^:\n]*)/
+ private_constant :COLORS_PATTERN
+
DEFAULTS = {
# color names
"black"=>"30", "red"=>"31", "green"=>"32", "yellow"=>"33",
"blue"=>"34", "magenta"=>"35", "cyan"=>"36", "white"=>"37",
- "bold"=>"1", "underline"=>"4", "reverse"=>"7",
+ "bold"=>"1", "faint"=>"2", "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",
+ "bg_black"=>"40", "bg_red"=>"41", "bg_green"=>"42", "bg_yellow"=>"43",
+ "bg_blue"=>"44", "bg_magenta"=>"45", "bg_cyan"=>"46", "bg_white"=>"47",
+ "bg_bright_black"=>"100", "bg_bright_red"=>"101",
+ "bg_bright_green"=>"102", "bg_bright_yellow"=>"103",
+ "bg_bright_blue"=>"104", "bg_bright_magenta"=>"105",
+ "bg_bright_cyan"=>"106", "bg_bright_white"=>"107",
# 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
+ }.freeze
+ private_constant :DEFAULTS
# colorize.decorate(str, name = color_name)
def decorate(str, name = @color)
if coloring? and color = resolve_color(name)
- "#{@beg}#{color}m#{str}#{@reset}"
+ "#{@beg}#{color}m#{str}#{reset_color(color)}"
else
str
end
end
+ DEFAULTS.each_key do |name|
+ define_method(name) {|str|
+ decorate(str, name)
+ }
+ end
+
+ private
+
+ def coloring?
+ STDOUT.tty? && (!(nc = ENV['NO_COLOR']) || nc.empty?)
+ end
+
def resolve_color(color = @color, seen = {}, colors = nil)
return unless @colors
color.to_s.gsub(/\b[a-z][\w ]+/) do |n|
@@ -69,10 +90,23 @@ class Colorize
end
end
- DEFAULTS.each_key do |name|
- define_method(name) {|str|
- decorate(str, name)
- }
+ def reset_color(colors)
+ resets = []
+ colors.scan(/\G;*\K(?:[34]8;(?:5;\d+|2(?:;\d+){3})|\d+)/) do |c|
+ case c
+ when '1', '2'
+ resets << '22'
+ when '4'
+ resets << '24'
+ when '7'
+ resets << '27'
+ when /\A[39]\d(?:;|\z)/
+ resets << '39'
+ when /\A(?:4|10)\d(?:;|\z)/
+ resets << '49'
+ end
+ end
+ "#{@beg}#{resets.reverse.join(';')}m"
end
end
diff --git a/tool/lib/core_assertions.rb b/tool/lib/core_assertions.rb
index 47cc6574c8..5ca318a598 100644
--- a/tool/lib/core_assertions.rb
+++ b/tool/lib/core_assertions.rb
@@ -75,9 +75,18 @@ module Test
require_relative 'envutil'
require 'pp'
begin
- require '-test-/asan'
+ 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
@@ -97,9 +106,11 @@ module Test
end
def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil,
- success: nil, failed: nil, **opt)
+ success: nil, failed: nil, gems: false, **opt)
args = Array(args).dup
- args.insert((Hash === args[0] ? 1 : 0), '--disable=gems')
+ 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]
@@ -160,7 +171,7 @@ module Test
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?
+ 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)
@@ -292,9 +303,34 @@ module Test
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 {
- out.puts "#{token}<error>", [Marshal.dump($!)].pack('m'), "#{token}</error>", "#{token}assertions=#{self._assertions}"
+ 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)
@@ -328,7 +364,16 @@ 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
@@ -339,15 +384,16 @@ eom
end
raise if $!
abort = status.coredump? || (status.signaled? && ABORT_SIGNALS.include?(status.termsig))
+ marshal_error = nil
assert(!abort, FailDesc[status, nil, stderr])
- self._assertions += res[/^#{token_re}assertions=(\d+)/, 1].to_i
- begin
- res = Marshal.load(res[/^#{token_re}<error>\n\K.*\n(?=#{token_re}<\/error>$)/m].unpack1("m"))
+ 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
- end
- if res and !(SystemExit === res)
+ else
+ next if SystemExit === res
if bt = res.backtrace
bt.each do |l|
l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"}
@@ -359,7 +405,7 @@ eom
raise res
end
- # really is it succeed?
+ # 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])
@@ -370,13 +416,17 @@ eom
# 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)
- return unless defined?(Ractor)
+ 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)
- require = "require #{require.inspect}" if require
+ 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)
@@ -832,6 +882,9 @@ eom
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
@@ -858,10 +911,11 @@ eom
first = seq.first
*arg = pre.call(first)
- times = (0..(rehearsal || (2 * first))).map do
+ raw_times = (0..(rehearsal || (2 * first))).map do
measure[arg, "rehearsal"].nonzero?
end
- times.compact!
+ times = raw_times.compact
+ raise "all measurements are zero: #{raw_times.inspect}" if times.empty?
tmin, tmax = times.minmax
# safe_factor * tmax * rehearsal_time_variance_factor(equals to 1 when variance is small)
diff --git a/tool/lib/envutil.rb b/tool/lib/envutil.rb
index ab5e8d84e9..b4c7d1d035 100644
--- a/tool/lib/envutil.rb
+++ b/tool/lib/envutil.rb
@@ -63,6 +63,14 @@ module EnvUtil
end
end
+ if RUBY_ENGINE == "truffleruby"
+ # Tests relying on timeout have high variance on TruffleRuby due to the highly-optimizing JIT, deoptimization, profiling interpreter, different GC, etc.
+ # Setting a default timeout scale helps avoid transient failures for tests relying on timeouts.
+ # We choose 10 because it is the same number used in CRuby CI on macOS:
+ # https://github.com/ruby/ruby/blob/9d46b0c735877f152a0b4b16b8153c6f395dee28/.github/workflows/macos.yml#L133
+ self.timeout_scale = 10
+ end
+
def apply_timeout_scale(t)
if scale = EnvUtil.timeout_scale
t * scale
@@ -104,9 +112,11 @@ module EnvUtil
else
return unless dpid
[[timeout, :TERM], [reprieve, :KILL]].find do |t, sig|
- return EnvUtil.timeout(t) {Process.wait(dpid)}
- rescue Timeout::Error
- Process.kill(sig, dpid)
+ begin
+ return EnvUtil.timeout(t) {Process.wait(dpid)}
+ rescue Timeout::Error
+ Process.kill(sig, dpid)
+ end
end
true
end
@@ -225,7 +235,8 @@ module EnvUtil
args = [args] if args.kind_of?(String)
# use the same parser as current ruby
- if args.none? { |arg| arg.start_with?("--parser=") }
+ 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)
@@ -276,7 +287,7 @@ module EnvUtil
module_function :invoke_ruby
def current_parser
- features = RUBY_DESCRIPTION[%r{\)\K [-+*/%._0-9a-zA-Z ]*(?=\[[-+*/%._0-9a-zA-Z]+\]\z)}]
+ 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
diff --git a/tool/lib/leakchecker.rb b/tool/lib/leakchecker.rb
index 69aeb2c254..33a546699f 100644
--- a/tool/lib/leakchecker.rb
+++ b/tool/lib/leakchecker.rb
@@ -77,6 +77,7 @@ class LeakChecker
end
(h[fd] ||= []) << [io, autoclose, inspect]
}
+ inspect = {}
fd_leaked.select! {|fd|
str = ''.dup
pos = nil
@@ -98,6 +99,7 @@ class LeakChecker
s = io.stat
rescue Errno::EBADF
# something un-stat-able
+ live2.delete(fd)
next
else
next if /darwin/ =~ RUBY_PLATFORM and [0, -1].include?(s.dev)
@@ -106,15 +108,41 @@ class LeakChecker
io&.close
end
end
- puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
- puts " The IO was created at #{pos}" if pos
+ inspect[fd] = [str, pos]
true
}
unless fd_leaked.empty?
unless @@try_lsof == false
- @@try_lsof |= system(*%W[lsof -a -d #{fd_leaked.minmax.uniq.join("-")} -p #$$], out: Test::Unit::Runner.output)
+ begin
+ open_list = IO.popen(%W[lsof -w -a -d #{fd_leaked.minmax.uniq.join("-")} -p #$$], &:readlines)
+ rescue
+ @@try_lsof = false
+ else
+ @@try_lsof |= $?.success?
+ end
+ if header = open_list&.shift
+ columns = header.split
+ fd_index, node_index = columns.index('FD'), columns.index('NODE')
+ open_list.reject! do |of|
+ of = of.chomp.split(' ', node_index + 2)
+ if of[node_index] == 'TCP' and of.last.end_with?('(CLOSE_WAIT)')
+ fd = of[fd_index].to_i
+ inspect.delete(fd)
+ h.delete(fd)
+ live2.delete(fd)
+ true
+ else
+ false
+ end
+ end
+ puts(header, open_list) unless open_list.empty?
+ end
end
end
+ inspect.each {|fd, (str, pos)|
+ puts "Leaked file descriptor: #{test_name}: #{fd}#{str}"
+ puts " The IO was created at #{pos}" if pos
+ }
h.each {|fd, list|
next if list.length <= 1
if 1 < list.count {|io, autoclose, inspect| autoclose }
@@ -156,7 +184,7 @@ class LeakChecker
[prev_count, []]
else
tempfiles = ObjectSpace.each_object(Tempfile).reject {|t|
- t.instance_variables.empty? || t.closed?
+ t.instance_variables.empty? || (t.closed? rescue true)
}
[count, tempfiles]
end
diff --git a/tool/lib/memory_status.rb b/tool/lib/memory_status.rb
index 60632523a8..429e5f6a1d 100644
--- a/tool/lib/memory_status.rb
+++ b/tool/lib/memory_status.rb
@@ -20,48 +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.push(:size, :rss, :peak)
- def self.read_status
- if info = Win32.memory_info
- yield :size, info.PagefileUsage
- yield :rss, info.WorkingSetSize
- yield :peak, info.PeakWorkingSetSize
+ 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'
diff --git a/tool/lib/output.rb b/tool/lib/output.rb
index 8cb426ae4a..8590e0ffe2 100644
--- a/tool/lib/output.rb
+++ b/tool/lib/output.rb
@@ -31,8 +31,8 @@ class Output
@vpath.def_options(opt)
end
- def write(data, overwrite: @overwrite, create_only: @create_only)
- unless @path
+ def write(data, overwrite: @overwrite, create_only: @create_only, name: nil, newer: nil, quiet: false)
+ unless (name = name ? (@path ? File.join(@path, name) : name) : @path)
$stdout.print data
return true
end
@@ -41,20 +41,21 @@ class Output
updated = color.fail("updated")
outpath = nil
- if (@ifchange or overwrite or create_only) and (@vpath.open(@path, "rb") {|f|
+ if (@ifchange or overwrite or create_only or newer) and (@vpath.open(name, "rb") {|f|
outpath = f.path
+ next true if newer and f.mtime > newer
if @ifchange or create_only
original = f.read
(@ifchange and original == data) or (create_only and !original.empty?)
end
} rescue false)
- puts "#{outpath} #{unchanged}"
+ puts "#{outpath} #{unchanged}" unless quiet
written = false
else
unless overwrite and outpath and (File.binwrite(outpath, data) rescue nil)
- File.binwrite(outpath = @path, data)
+ File.binwrite(outpath = name, data)
end
- puts "#{outpath} #{updated}"
+ puts "#{outpath} #{updated}" unless quiet
written = true
end
if timestamp = @timestamp
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 7d43e825e1..2663b7b76a 100644
--- a/tool/lib/test/unit.rb
+++ b/tool/lib/test/unit.rb
@@ -19,6 +19,7 @@ require_relative '../envutil'
require_relative '../colorize'
require_relative '../leakchecker'
require_relative '../test/unit/testcase'
+require_relative '../test/jobserver'
require 'optparse'
# See Test::Unit
@@ -262,27 +263,8 @@ module Test
def non_options(files, options)
@jobserver = nil
- makeflags = ENV.delete("MAKEFLAGS")
- if !options[:parallel] and
- /(?:\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 if r
- nil
- else
- r.close_on_exec = true
- w.close_on_exec = true
- @jobserver = [r, w]
- options[:parallel] ||= 256 # number of tokens to acquire first
- 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
diff --git a/tool/lib/test/unit/assertions.rb b/tool/lib/test/unit/assertions.rb
index 19581fc3ab..0908666166 100644
--- a/tool/lib/test/unit/assertions.rb
+++ b/tool/lib/test/unit/assertions.rb
@@ -128,8 +128,16 @@ module Test
def assert_in_delta exp, act, delta = 0.001, msg = nil
n = (exp - act).abs
+ loadavg = begin
+ if File.readable?("/proc/loadavg")
+ " (/proc/loadavg=#{File.read("/proc/loadavg").strip})"
+ end
+ rescue StandardError
+ nil
+ end
+ loadavg ||= ""
msg = message(msg) {
- "Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}"
+ "Expected |#{exp} - #{act}| (#{n}) to be <= #{delta}#{loadavg}"
}
assert delta >= n, msg
end
diff --git a/tool/lib/vcs.rb b/tool/lib/vcs.rb
index 2c019d81fd..d6374f9de0 100644
--- a/tool/lib/vcs.rb
+++ b/tool/lib/vcs.rb
@@ -51,23 +51,9 @@ module DebugPOpen
end
using DebugPOpen
module DebugSystem
- def system(*args)
+ def system(*args, exception: true, **opts)
VCS.dump(args, "args: ") 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
+ super(*args, exception: exception, **opts)
end
end
@@ -183,19 +169,7 @@ class VCS
)
last or raise VCS::NotFoundError, "last revision not found"
changed or raise VCS::NotFoundError, "changed revision not found"
- if modified
- /\A(\d+)-(\d+)-(\d+)\D(\d+):(\d+):(\d+(?:\.\d+)?)\s*(?:Z|([-+]\d\d)(\d\d))\z/ =~ modified or
- raise "unknown time format - #{modified}"
- match = $~[1..6].map { |x| x.to_i }
- off = $7 ? "#{$7}:#{$8}" : "+00:00"
- match << off
- begin
- modified = Time.new(*match)
- rescue ArgumentError
- modified = Time.utc(*$~[1..6]) + $7.to_i * 3600 + $8.to_i * 60
- end
- modified = modified.getlocal(@zone)
- end
+ modified &&= parse_iso_date(modified)
return last, changed, modified, *rest
end
@@ -204,9 +178,9 @@ class VCS
modified
end
- def relative_to(path)
+ def relative_to(path, srcdir = @srcdir)
if path
- srcdir = File.realpath(@srcdir)
+ srcdir = File.realpath(srcdir || @srcdir)
path = File.realdirpath(path)
list1 = srcdir.split(%r{/})
list2 = path.split(%r{/})
@@ -224,6 +198,20 @@ class VCS
end
end
+ def parse_iso_date(date)
+ /\A(\d+)-(\d+)-(\d+)\D(\d+):(\d+):(\d+(?:\.\d+)?)\s*(?:Z|([-+]\d\d)(\d\d))\z/ =~ date or
+ raise "unknown time format - #{date}"
+ match = $~[1..6].map { |x| x.to_i }
+ off = $7 ? "#{$7}:#{$8}" : "+00:00"
+ match << off
+ begin
+ date = Time.new(*match)
+ rescue ArgumentError
+ date = Time.utc(*$~[1..6]) + $7.to_i * 3600 + $8.to_i * 60
+ end
+ date.getlocal(@zone)
+ end
+
def after_export(dir)
FileUtils.rm_rf(Dir.glob("#{dir}/.git*"))
FileUtils.rm_rf(Dir.glob("#{dir}/.mailmap"))
@@ -376,6 +364,11 @@ class VCS
[last, changed, modified, branch, title]
end
+ def author_date(path, srcdir = @srcdir)
+ log = cmd_read_at(srcdir, [[COMMAND, 'log', '-n1', '--pretty=%at', path]])
+ Time.at(log.to_i, in: @zone)
+ end
+
def self.revision_name(rev)
short_revision(rev)
end
@@ -462,17 +455,19 @@ class VCS
end
def branch_beginning(url)
- cmd_read(%W[ #{COMMAND} log -n1 --format=format:%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.to_str} -- version.h include/ruby/version.h])
+ #{url} -- version.h include/ruby/version.h])[/.*/]
end
- def export_changelog(url = '@', from = nil, to = nil, _path = nil, path: _path, base_url: nil)
+ 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
rev unless rev.empty?
end
- unless (from && /./.match(from)) or ((from = branch_beginning(url)) && /./.match(from))
+ to ||= url.to_str
+ unless from&.match?(/./) or (from = branch_beginning(to))&.match?(/./)
warn "no starting commit found", uplevel: 1
from = nil
end
@@ -483,13 +478,13 @@ class VCS
else
warn "Could not fetch notes/commits tree", uplevel: 1
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!
@@ -508,9 +503,10 @@ class VCS
end
LOG_FIX_REGEXP_SEPARATORS = '/!:;|,#%&'
+ CHANGELOG_ENV = {'TZ' => 'JST-9', 'LANG' => 'C', 'LC_ALL' => 'C'}
def changelog_formatter(path, arg, base_url = nil)
- env = {'TZ' => 'JST-9', 'LANG' => 'C', 'LC_ALL' => 'C'}
+ 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]
@@ -606,11 +602,6 @@ class VCS
s = s.join('')
end
- if %r[^ +(https://github\.com/[^/]+/[^/]+/)commit/\h+\n(?=(?: +\n(?i: +Co-authored-by: .*\n)+)?(?:\n|\Z))] =~ s
- issue = "#{$1}pull/"
- s.gsub!(/\b(?:(?i:fix(?:e[sd])?) +|GH-)\K#(?=\d+\b)|\(\K#(?=\d+\))/) {issue}
- end
-
s.gsub!(/ +\n/, "\n")
s.sub!(/^Notes:/, ' \&')
w.print sep, h, s