Skip to content

Commit

Permalink
Update the latest CoreAssetion for assert_linear_performance
Browse files Browse the repository at this point in the history
  • Loading branch information
hsbt committed Sep 10, 2024
1 parent df1ca41 commit 5c400d3
Show file tree
Hide file tree
Showing 6 changed files with 450 additions and 180 deletions.
130 changes: 106 additions & 24 deletions tool/lib/core_assertions.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
# frozen_string_literal: true

module Test

class << self
##
# Filter object for backtraces.

attr_accessor :backtrace_filter
end

class BacktraceFilter # :nodoc:
def filter bt
return ["No backtrace"] unless bt

new_bt = []
pattern = %r[/(?:lib\/test/|core_assertions\.rb:)]

unless $DEBUG then
bt.each do |line|
break if pattern.match?(line)
new_bt << line
end

new_bt = bt.reject { |line| pattern.match?(line) } if new_bt.empty?
new_bt = bt.dup if new_bt.empty?
else
new_bt = bt.dup
end

new_bt
end
end

self.backtrace_filter = BacktraceFilter.new

def self.filter_backtrace bt # :nodoc:
backtrace_filter.filter bt
end

module Unit
module Assertions
def assert_raises(*exp, &b)
raise NoMethodError, "use assert_raise", caller
end

def _assertions= n # :nodoc:
@_assertions = n
end
Expand Down Expand Up @@ -33,6 +74,11 @@ def message msg = nil, ending = nil, &default
module CoreAssertions
require_relative 'envutil'
require 'pp'
begin
require '-test-/asan'
rescue LoadError
end

nil.pretty_inspect

def mu_pp(obj) #:nodoc:
Expand Down Expand Up @@ -107,8 +153,13 @@ def syntax_check(code, fname, line)
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)
Expand Down Expand Up @@ -244,7 +295,11 @@ def separated_runner(token, out = nil)
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)
Expand All @@ -254,7 +309,7 @@ def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **o
line ||= loc.lineno
end
capture_stdout = true
unless /mswin|mingw/ =~ RUBY_PLATFORM
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
Expand Down Expand Up @@ -531,11 +586,11 @@ def assert_not_respond_to(obj, (meth, *priv), msg = nil)
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
Expand All @@ -544,11 +599,13 @@ def assert_pattern_list(pattern_list, actual, message=nil)
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
Expand All @@ -565,7 +622,7 @@ def assert_pattern_list(pattern_list, actual, message=nil)
}
assert false, msg
end
rest = match.post_match
rest = post_match
anchored = true
end
}
Expand All @@ -592,14 +649,14 @@ def assert_warn(*args)

def assert_deprecated_warning(mesg = /deprecated/)
assert_warning(mesg) do
Warning[:deprecated] = true
Warning[:deprecated] = true if Warning.respond_to?(:[]=)
yield
end
end

def assert_deprecated_warn(mesg = /deprecated/)
assert_warn(mesg) do
Warning[:deprecated] = true
Warning[:deprecated] = true if Warning.respond_to?(:[]=)
yield
end
end
Expand Down Expand Up @@ -691,7 +748,7 @@ def assert_join_threads(threads, message = nil)
msg = "exceptions on #{errs.length} threads:\n" +
errs.map {|t, err|
"#{t.inspect}:\n" +
RUBY_VERSION >= "2.5.0" ? err.full_message(highlight: false, order: :top) : err.message
(err.respond_to?(:full_message) ? err.full_message(highlight: false, order: :top) : err.message)
}.join("\n---\n")
if message
msg = "#{message}\n#{msg}"
Expand Down Expand Up @@ -726,35 +783,60 @@ def assert_all_assertions_foreach(msg = nil, *keys, &block)
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
st = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield(*arg)
t = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - st)
assert_operator 0, :<=, t
t.nonzero?
measure[arg, "rehearsal"].nonzero?
end
times.compact!
tmin, tmax = times.minmax
tmax *= tmax / tmin
tmax = 10**Math.log10(tmax).ceil

# 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 = tmax * i.fdiv(first)
t = tbase * i.fdiv(first)
*arg = pre.call(i)
message = "[#{i}]: in #{t}s"
message = "[#{i}]: in #{t}s #{info}"
Timeout.timeout(t, Timeout::Error, message) do
st = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield(*arg)
assert_operator (Process.clock_gettime(Process::CLOCK_MONOTONIC) - st), :<=, t, message
measure[arg, message]
end
end
end
Expand Down
81 changes: 59 additions & 22 deletions tool/lib/envutil.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,22 @@
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
Expand All @@ -53,7 +52,14 @@ def capture_global_values
@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

Expand Down Expand Up @@ -152,7 +158,12 @@ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr =
if RUBYLIB and lib = child_env["RUBYLIB"]
child_env["RUBYLIB"] = [lib, RUBYLIB].join(File::PATH_SEPARATOR)
end
child_env['ASAN_OPTIONS'] = ENV['ASAN_OPTIONS'] if ENV['ASAN_OPTIONS']

# remain env
%w(ASAN_OPTIONS RUBY_ON_BUG).each{|name|
child_env[name] = ENV[name] if !child_env.key?(name) and ENV.key?(name)
}

args = [args] if args.kind_of?(String)
pid = spawn(child_env, *precommand, rubybin, *args, opt)
in_c.close
Expand Down Expand Up @@ -241,6 +252,24 @@ def under_gc_stress(stress = true)
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
Expand Down Expand Up @@ -292,16 +321,24 @@ def self.diagnostic_reports(signame, pid, now)
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
Expand Down
Loading

0 comments on commit 5c400d3

Please sign in to comment.