summaryrefslogtreecommitdiff
path: root/bootstraptest
diff options
context:
space:
mode:
Diffstat (limited to 'bootstraptest')
-rwxr-xr-xbootstraptest/runner.rb65
-rw-r--r--bootstraptest/test_eval.rb2
-rw-r--r--bootstraptest/test_fiber.rb5
-rw-r--r--bootstraptest/test_flow.rb4
-rw-r--r--bootstraptest/test_fork.rb27
-rw-r--r--bootstraptest/test_gc.rb2
-rw-r--r--bootstraptest/test_insns.rb20
-rw-r--r--bootstraptest/test_io.rb2
-rw-r--r--bootstraptest/test_literal.rb8
-rw-r--r--bootstraptest/test_load.rb2
-rw-r--r--bootstraptest/test_method.rb66
-rw-r--r--bootstraptest/test_ractor.rb1824
-rw-r--r--bootstraptest/test_rjit.rb80
-rw-r--r--bootstraptest/test_syntax.rb12
-rw-r--r--bootstraptest/test_thread.rb5
-rw-r--r--bootstraptest/test_yjit.rb674
-rw-r--r--bootstraptest/test_yjit_rust_port.rb8
17 files changed, 2003 insertions, 803 deletions
diff --git a/bootstraptest/runner.rb b/bootstraptest/runner.rb
index 24324fa51f..04de0c93b9 100755
--- a/bootstraptest/runner.rb
+++ b/bootstraptest/runner.rb
@@ -16,6 +16,7 @@ rescue LoadError
$:.unshift File.join(File.dirname(__FILE__), '../lib')
retry
end
+require_relative '../tool/lib/test/jobserver'
if !Dir.respond_to?(:mktmpdir)
# copied from lib/tmpdir.rb
@@ -110,35 +111,7 @@ BT = Class.new(bt) do
def wn=(wn)
unless wn == 1
- if /(?:\A|\s)--jobserver-(?:auth|fds)=(?:(\d+),(\d+)|fifo:((?:\\.|\S)+))/ =~ ENV.delete("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
- else
- r.close_on_exec = true
- w.close_on_exec = true
- tokens = r.read_nonblock(wn > 0 ? wn : 1024, exception: false)
- r.close
- if String === tokens
- tokens.freeze
- auth = w
- w = nil
- at_exit {auth << tokens; auth.close}
- wn = tokens.size + 1
- else
- w.close
- wn = 1
- end
- end
- end
+ wn = Test::JobServer.max_jobs(wn > 0 ? wn : 1024, ENV.delete("MAKEFLAGS")) || wn
if wn <= 0
require 'etc'
wn = [Etc.nprocessors / 2, 1].max
@@ -163,7 +136,7 @@ def main
BT.tty = nil
BT.quiet = false
BT.timeout = 180
- BT.timeout_scale = (defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled? ? 3 : 1) # for --jit-wait
+ BT.timeout_scale = 1
if (ts = (ENV["RUBY_TEST_TIMEOUT_SCALE"] || ENV["RUBY_TEST_SUBPROCESS_TIMEOUT_SCALE"]).to_i) > 1
BT.timeout_scale *= ts
end
@@ -236,7 +209,19 @@ End
return true
end
- require_relative '../tool/lib/launchable'
+ begin
+ require_relative '../tool/lib/launchable'
+ rescue LoadError
+ # The following error sometimes happens, so we're going to skip writing Launchable report files in this case.
+ #
+ # ```
+ # /tmp/tmp.bISss9CtXZ/.ext/common/json/ext.rb:15:in 'Kernel#require':
+ # /tmp/tmp.bISss9CtXZ/.ext/x86_64-linux/json/ext/parser.so:
+ # undefined symbol: ruby_abi_version - ruby_abi_version (LoadError)
+ # ```
+ #
+ return true
+ end
BT.launchable_test_reports = writer = Launchable::JsonStreamWriter.new($1)
writer.write_array('testCases')
at_exit {
@@ -286,7 +271,7 @@ End
if defined?(RUBY_DESCRIPTION)
puts "Driver is #{RUBY_DESCRIPTION}"
elsif defined?(RUBY_PATCHLEVEL)
- puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}#{RUBY_PLATFORM}) [#{RUBY_PLATFORM}]"
+ puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}#{RUBY_PATCHLEVEL}) [#{RUBY_PLATFORM}]"
else
puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
end
@@ -342,6 +327,7 @@ def concurrent_exec_test
begin
while BT.wn != term_wn
if r = rq.pop
+ BT_STATE.count += 1
case
when BT.quiet
when BT.tty
@@ -399,7 +385,7 @@ module Launchable
}
)
@@duration = 0
- @@failure_log.clear
+ @@failure_log = ''
end
@@last_test_name = relative_path
@@duration += t
@@ -612,6 +598,8 @@ class Assertion < Struct.new(:src, :path, :lineno, :proc)
end
end
+ class Timeout < StandardError; end
+
def get_result_string(opt = '', timeout: BT.timeout, **argh)
if BT.ruby
timeout = BT.apply_timeout_scale(timeout)
@@ -621,7 +609,11 @@ class Assertion < Struct.new(:src, :path, :lineno, :proc)
out = IO.popen("#{BT.ruby} -W0 #{opt} #{filename}", **kw)
pid = out.pid
th = Thread.new {out.read.tap {Process.waitpid(pid); out.close}}
- th.value if th.join(timeout)
+ if th.join(timeout)
+ th.value
+ else
+ Timeout.new("timed out after #{timeout} seconds")
+ end
ensure
raise Interrupt if $? and $?.signaled? && $?.termsig == Signal.list["INT"]
@@ -878,9 +870,8 @@ def yjit_enabled?
ENV.key?('RUBY_YJIT_ENABLE') || ENV.fetch('RUN_OPTS', '').include?('yjit') || BT.ruby.include?('yjit')
end
-def rjit_enabled?
- # Don't check `RubyVM::RJIT.enabled?`. On btest-bruby, target Ruby != runner Ruby.
- ENV.fetch('RUN_OPTS', '').include?('rjit')
+def zjit_enabled?
+ ENV.key?('RUBY_ZJIT_ENABLE') || ENV.fetch('RUN_OPTS', '').include?('zjit') || BT.ruby.include?('zjit')
end
exit main
diff --git a/bootstraptest/test_eval.rb b/bootstraptest/test_eval.rb
index d923a957bc..20bd9615f4 100644
--- a/bootstraptest/test_eval.rb
+++ b/bootstraptest/test_eval.rb
@@ -217,7 +217,7 @@ assert_equal %q{[10, main]}, %q{
}
%w[break next redo].each do |keyword|
- assert_match %r"Can't escape from eval with #{keyword}\b", %{
+ assert_match %r"Invalid #{keyword}\b", %{
$stderr = STDOUT
begin
eval "0 rescue #{keyword}"
diff --git a/bootstraptest/test_fiber.rb b/bootstraptest/test_fiber.rb
index 2614dd13bf..ae809a5936 100644
--- a/bootstraptest/test_fiber.rb
+++ b/bootstraptest/test_fiber.rb
@@ -37,3 +37,8 @@ assert_normal_exit %q{
assert_normal_exit %q{
Fiber.new(&Object.method(:class_eval)).resume("foo")
}, '[ruby-dev:34128]'
+
+# [Bug #21400]
+assert_normal_exit %q{
+ Thread.new { Fiber.current.kill }.join
+}
diff --git a/bootstraptest/test_flow.rb b/bootstraptest/test_flow.rb
index 15528a4213..7a95def1e6 100644
--- a/bootstraptest/test_flow.rb
+++ b/bootstraptest/test_flow.rb
@@ -376,7 +376,7 @@ assert_equal %q{[1, 4, 7, 5, 8, 9]}, %q{$a = []; begin; ; $a << 1
; $a << 3
end; $a << 4
def m2; $a << 5
- m1(:a, :b, (return 1; :c)); $a << 6
+ m1(:a, :b, (return 1 if true; :c)); $a << 6
end; $a << 7
m2; $a << 8
; $a << 9
@@ -399,7 +399,7 @@ assert_equal %q{[1, 3, 11, 4, 5, 6, 7, 12, 13]}, %q{$a = []; begin; ; $a << 1
m2(begin; $a << 5
2; $a << 6
ensure; $a << 7
- return 3; $a << 8
+ return 3 if true; $a << 8
end); $a << 9
4; $a << 10
end; $a << 11
diff --git a/bootstraptest/test_fork.rb b/bootstraptest/test_fork.rb
index 83923dad97..860ef285d0 100644
--- a/bootstraptest/test_fork.rb
+++ b/bootstraptest/test_fork.rb
@@ -75,3 +75,30 @@ assert_equal '[1, 2]', %q{
end
}, '[ruby-dev:44005] [Ruby 1.9 - Bug #4950]'
+assert_equal 'ok', %q{
+ def now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+
+ Thread.new do
+ loop { sleep 0.0001 }
+ end
+
+ 10.times do
+ pid = fork{ exit!(0) }
+ deadline = now + 10
+ while true
+ _, status = Process.waitpid2(pid, Process::WNOHANG)
+ break if status
+ if now > deadline
+ Process.kill(:KILL, pid)
+ raise "failed"
+ end
+ sleep 0.001
+ end
+ unless status.success?
+ raise "child exited with status #{status}"
+ end
+ rescue NotImplementedError
+ end
+ :ok
+}, '[Bug #20670]'
+
diff --git a/bootstraptest/test_gc.rb b/bootstraptest/test_gc.rb
index 17bc497822..eb68c9845e 100644
--- a/bootstraptest/test_gc.rb
+++ b/bootstraptest/test_gc.rb
@@ -14,7 +14,7 @@ ms = "a".."k"
o.send(meth)
end
end
-}, '[ruby-dev:39453]' unless rjit_enabled? # speed up RJIT CI
+}, '[ruby-dev:39453]'
assert_normal_exit %q{
a = []
diff --git a/bootstraptest/test_insns.rb b/bootstraptest/test_insns.rb
index 18ab1800bd..1f70c8075c 100644
--- a/bootstraptest/test_insns.rb
+++ b/bootstraptest/test_insns.rb
@@ -86,7 +86,7 @@ tests = [
[ 'putobject', %q{ /(?<x>x)/ =~ "x"; x == "x" }, ],
[ 'putspecialobject', %q{ {//=>true}[//] }, ],
- [ 'putstring', %q{ "true" }, ],
+ [ 'dupstring', %q{ "true" }, ],
[ 'tostring / concatstrings', %q{ "#{true}" }, ],
[ 'toregexp', %q{ /#{true}/ =~ "true" && $~ }, ],
[ 'intern', %q{ :"#{true}" }, ],
@@ -214,8 +214,21 @@ tests = [
'true'.freeze
},
+ [ 'opt_duparray_send', %q{ x = :a; [:a, :b].include?(x) }, ],
+ [ 'opt_duparray_send', <<-'},', ], # {
+ class Array
+ def include?(i)
+ i == 1
+ end
+ end
+ x = 1
+ [:a, :b].include?(x)
+ },
+
[ 'opt_newarray_send', %q{ ![ ].hash.nil? }, ],
+ [ 'opt_newarray_send', %q{ v=2; [1, Object.new, 2].include?(v) }, ],
+
[ 'opt_newarray_send', %q{ [ ].max.nil? }, ],
[ 'opt_newarray_send', %q{ [1, x = 2, 3].max == 3 }, ],
[ 'opt_newarray_send', <<-'},', ], # {
@@ -413,11 +426,6 @@ tests = [
x&.x[true] ||= true # here
},
- [ 'opt_aref_with', %q{ { 'true' => true }['true'] }, ],
- [ 'opt_aref_with', %q{ Struct.new(:nil).new['nil'].nil? }, ],
- [ 'opt_aset_with', %q{ {}['true'] = true }, ],
- [ 'opt_aset_with', %q{ Struct.new(:true).new['true'] = true }, ],
-
[ 'opt_length', %q{ 'true' .length == 4 }, ],
[ 'opt_length', %q{ :true .length == 4 }, ],
[ 'opt_length', %q{ [ 'true' ] .length == 1 }, ],
diff --git a/bootstraptest/test_io.rb b/bootstraptest/test_io.rb
index 4e5d6d59c9..4081769a8c 100644
--- a/bootstraptest/test_io.rb
+++ b/bootstraptest/test_io.rb
@@ -85,7 +85,7 @@ assert_normal_exit %q{
ARGF.set_encoding "foo"
}
-/freebsd/ =~ RUBY_PLATFORM or
+/(freebsd|mswin)/ =~ RUBY_PLATFORM or
10.times do
assert_normal_exit %q{
at_exit { p :foo }
diff --git a/bootstraptest/test_literal.rb b/bootstraptest/test_literal.rb
index a30661a796..39e6527027 100644
--- a/bootstraptest/test_literal.rb
+++ b/bootstraptest/test_literal.rb
@@ -117,16 +117,16 @@ assert_equal '1', 'a = [obj = Object.new]; a.size'
assert_equal 'true', 'a = [obj = Object.new]; a[0] == obj'
assert_equal '5', 'a = [1,2,3]; a[1] = 5; a[1]'
assert_equal 'bar', '[*:foo];:bar'
-assert_equal '[1, 2]', 'def nil.to_a; [2]; end; [1, *nil]'
-assert_equal '[1, 2]', 'def nil.to_a; [1, 2]; end; [*nil]'
-assert_equal '[0, 1, {2=>3}]', '[0, *[1], 2=>3]', "[ruby-dev:31592]"
+assert_equal '[]', 'def nil.to_a; [1, 2]; end; [*nil]'
+assert_equal '[1]', 'def nil.to_a; [2]; end; [1, *nil]'
+assert_equal '[0, 1, {2 => 3}]', '[0, *[1], 2=>3]', "[ruby-dev:31592]"
# hash
assert_equal 'Hash', '{}.class'
assert_equal '{}', '{}.inspect'
assert_equal 'Hash', '{1=>2}.class'
-assert_equal '{1=>2}', '{1=>2}.inspect'
+assert_equal '{1 => 2}', '{1=>2}.inspect'
assert_equal '2', 'h = {1 => 2}; h[1]'
assert_equal '0', 'h = {1 => 2}; h.delete(1); h.size'
assert_equal '', 'h = {1 => 2}; h.delete(1); h[1]'
diff --git a/bootstraptest/test_load.rb b/bootstraptest/test_load.rb
index 5046d4fcea..fa8d31c098 100644
--- a/bootstraptest/test_load.rb
+++ b/bootstraptest/test_load.rb
@@ -12,7 +12,7 @@ assert_equal 'ok', %q{
}
}.map {|t| t.value }
vs[0] == M && vs[1] == M ? :ok : :ng
-}, '[ruby-dev:32048]' unless ENV.fetch('RUN_OPTS', '').include?('rjit') # Thread seems to be switching during JIT. To be fixed later.
+}, '[ruby-dev:32048]'
assert_equal 'ok', %q{
%w[a a/foo b].each {|d| Dir.mkdir(d)}
diff --git a/bootstraptest/test_method.rb b/bootstraptest/test_method.rb
index af9443b8c6..e894f6f601 100644
--- a/bootstraptest/test_method.rb
+++ b/bootstraptest/test_method.rb
@@ -1374,3 +1374,69 @@ assert_equal 'ok', %q{
foo(:foo, b: :ok)
foo(*["foo"], b: :ok)
}
+
+assert_equal 'ok', %q{
+ Thing = Struct.new(:value)
+
+ Obj = Thing.new("ok")
+
+ def delegate(...)
+ Obj.value(...)
+ end
+
+ def no_args
+ delegate
+ end
+
+ def splat_args(*args)
+ delegate(*args)
+ end
+
+ no_args
+ splat_args
+}
+
+assert_equal 'ok', %q{
+ class A
+ private
+ def foo = "ng"
+ end
+
+ class B
+ def initialize(o)
+ @o = o
+ end
+
+ def foo(...) = @o.foo(...)
+ def internal_foo = foo
+ end
+
+ b = B.new(A.new)
+
+ begin
+ b.internal_foo
+ rescue NoMethodError
+ "ok"
+ end
+}
+
+assert_equal 'ok', <<~RUBY
+ def test(*, kw: false)
+ "ok"
+ end
+
+ test
+RUBY
+
+assert_equal '[1, 2, 3]', %q{
+ def target(*args) = args
+ def x = [1]
+ def forwarder(...) = target(*x, 2, ...)
+ forwarder(3).inspect
+}, '[Bug #21832] post-splat args before forwarding'
+
+assert_equal '[nil, nil]', %q{
+ def self_reading(a = a, kw:) = a
+ def through_binding(a = binding.local_variable_get(:a), kw:) = a
+ [self_reading(kw: 1), through_binding(kw: 1)]
+}, 'nil initialization of optional parameters'
diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb
index b6a4ef605e..4fe90703fc 100644
--- a/bootstraptest/test_ractor.rb
+++ b/bootstraptest/test_ractor.rb
@@ -67,7 +67,7 @@ assert_equal "#<Ractor:#1 running>", %q{
# Return id, loc, and status for no-name ractor
assert_match /^#<Ractor:#([^ ]*?) .+:[0-9]+ terminated>$/, %q{
r = Ractor.new { '' }
- r.take
+ r.join
sleep 0.1 until r.inspect =~ /terminated/
r.inspect
}
@@ -75,7 +75,7 @@ assert_match /^#<Ractor:#([^ ]*?) .+:[0-9]+ terminated>$/, %q{
# Return id, name, loc, and status for named ractor
assert_match /^#<Ractor:#([^ ]*?) Test Ractor .+:[0-9]+ terminated>$/, %q{
r = Ractor.new(name: 'Test Ractor') { '' }
- r.take
+ r.join
sleep 0.1 until r.inspect =~ /terminated/
r.inspect
}
@@ -86,7 +86,7 @@ assert_equal 'ok', %q{
r = Ractor.new do
'ok'
end
- r.take
+ r.value
}
# Passed arguments to Ractor.new will be a block parameter
@@ -96,7 +96,7 @@ assert_equal 'ok', %q{
r = Ractor.new 'ok' do |msg|
msg
end
- r.take
+ r.value
}
# Pass multiple arguments to Ractor.new
@@ -105,7 +105,7 @@ assert_equal 'ok', %q{
r = Ractor.new 'ping', 'pong' do |msg, msg2|
[msg, msg2]
end
- 'ok' if r.take == ['ping', 'pong']
+ 'ok' if r.value == ['ping', 'pong']
}
# Ractor#send passes an object with copy to a Ractor
@@ -115,65 +115,23 @@ assert_equal 'ok', %q{
msg = Ractor.receive
end
r.send 'ok'
- r.take
+ r.value
}
# Ractor#receive_if can filter the message
-assert_equal '[2, 3, 1]', %q{
- r = Ractor.new Ractor.current do |main|
- main << 1
- main << 2
- main << 3
- end
- a = []
- a << Ractor.receive_if{|msg| msg == 2}
- a << Ractor.receive_if{|msg| msg == 3}
- a << Ractor.receive
-}
-
-# Ractor#receive_if with break
-assert_equal '[2, [1, :break], 3]', %q{
- r = Ractor.new Ractor.current do |main|
- main << 1
- main << 2
- main << 3
- end
-
- a = []
- a << Ractor.receive_if{|msg| msg == 2}
- a << Ractor.receive_if{|msg| break [msg, :break]}
- a << Ractor.receive
-}
+assert_equal '[1, 2, 3]', %q{
+ ports = 3.times.map{Ractor::Port.new}
-# Ractor#receive_if can't be called recursively
-assert_equal '[[:e1, 1], [:e2, 2]]', %q{
- r = Ractor.new Ractor.current do |main|
- main << 1
- main << 2
- main << 3
+ r = Ractor.new ports do |ports|
+ ports[0] << 3
+ ports[1] << 1
+ ports[2] << 2
end
-
a = []
-
- Ractor.receive_if do |msg|
- begin
- Ractor.receive
- rescue Ractor::Error
- a << [:e1, msg]
- end
- true # delete 1 from queue
- end
-
- Ractor.receive_if do |msg|
- begin
- Ractor.receive_if{}
- rescue Ractor::Error
- a << [:e2, msg]
- end
- true # delete 2 from queue
- end
-
- a #
+ a << ports[1].receive # 1
+ a << ports[2].receive # 2
+ a << ports[0].receive # 3
+ a
}
# dtoa race condition
@@ -184,73 +142,145 @@ assert_equal '[:ok, :ok, :ok]', %q{
10_000.times{ rand.to_s }
:ok
}
- }.map(&:take)
+ }.map(&:value)
+}
+
+assert_equal "42", %q{
+ a = 42
+ Ractor.shareable_lambda{ a }.call
}
-# Ractor.make_shareable issue for locals in proc [Bug #18023]
+# Ractor.shareable_proc issue for locals in proc [Bug #18023]
assert_equal '[:a, :b, :c, :d, :e]', %q{
v1, v2, v3, v4, v5 = :a, :b, :c, :d, :e
- closure = Ractor.current.instance_eval{ Proc.new { [v1, v2, v3, v4, v5] } }
+ closure = Proc.new { [v1, v2, v3, v4, v5] }
+ Ractor.shareable_proc(&closure).call
+}
+
+# Ractor.shareable_proc makes a copy of given Proc
+assert_equal '[true, true]', %q{
+ pr1 = Proc.new do
+ self
+ end
+ pr2 = Ractor.shareable_proc(&pr1)
- Ractor.make_shareable(closure).call
+ [pr1.call == self, pr2.call == nil]
}
-# Ractor.make_shareable issue for locals in proc [Bug #18023]
-assert_equal '[:a, :b, :c, :d, :e, :f, :g]', %q{
- a = :a
- closure = Ractor.current.instance_eval do
- -> {
- b, c, d = :b, :c, :d
- -> {
- e, f, g = :e, :f, :g
- -> { [a, b, c, d, e, f, g] }
- }.call
- }.call
+# Ractor.shareable_proc keeps the original Proc intact
+assert_equal '[SyntaxError, [Object, 43, 43], Binding]', %q{
+ a = 42
+ pr1 = Proc.new do
+ [self.class, eval("a"), binding.local_variable_get(:a)]
end
+ a += 1
+ pr2 = Ractor.shareable_proc(&pr1)
- Ractor.make_shareable(closure).call
+ r = []
+ begin
+ pr2.call
+ rescue SyntaxError
+ r << SyntaxError
+ end
+
+ r << pr1.call << pr1.binding.class
}
-# Now autoload in non-main Ractor is not supported
-assert_equal 'ok', %q{
- autoload :Foo, 'foo.rb'
- r = Ractor.new do
- p Foo
- rescue Ractor::UnsafeError
- :ok
+# Ractor.make_shareable mutates the original Proc
+# This is the current behavior, it's currently considered safe enough
+# because in most cases it would raise anyway due to not-shared self or not-shared captured variable value
+assert_equal '[[42, 42], Binding, true, SyntaxError, "Can\'t create Binding from isolated Proc"]', %q{
+ a = 42
+ pr1 = nil.instance_exec do
+ Proc.new do
+ [eval("a"), binding.local_variable_get(:a)]
+ end
+ end
+
+ r = [pr1.call, pr1.binding.class]
+
+ pr2 = Ractor.make_shareable(pr1)
+ r << pr1.equal?(pr2)
+
+ begin
+ pr1.call
+ rescue SyntaxError
+ r << SyntaxError
+ end
+
+ begin
+ r << pr1.binding
+ rescue ArgumentError
+ r << $!.message
+ end
+
+ r
+}
+
+# Ractor::IsolationError cases
+assert_equal '3', %q{
+ ok = 0
+
+ begin
+ a = 1
+ Ractor.shareable_proc{a}
+ a = 2
+ rescue Ractor::IsolationError => e
+ ok += 1
+ end
+
+ begin
+ cond = false
+ b = 1
+ b = 2 if cond
+ Ractor.shareable_proc{b}
+ rescue Ractor::IsolationError => e
+ ok += 1
+ end
+
+ begin
+ 1.times{|i|
+ i = 2
+ Ractor.shareable_proc{i}
+ }
+ rescue Ractor::IsolationError => e
+ ok += 1
end
- r.take
}
###
###
# Ractor still has several memory corruption so skip huge number of tests
-if ENV['GITHUB_WORKFLOW'] &&
- ENV['GITHUB_WORKFLOW'] == 'Compilations'
+if ENV['GITHUB_WORKFLOW'] == 'Compilations'
# ignore the follow
else
-# Ractor.select(*ractors) receives a values from a ractors.
-# It is similar to select(2) and Go's select syntax.
-# The return value is [ch, received_value]
+# Ractor.select with a Ractor argument
assert_equal 'ok', %q{
# select 1
r1 = Ractor.new{'r1'}
- r, obj = Ractor.select(r1)
- 'ok' if r == r1 and obj == 'r1'
+ port, obj = Ractor.select(r1)
+ if port == r1 and obj == 'r1'
+ 'ok'
+ else
+ # failed
+ [port, obj].inspect
+ end
}
# Ractor.select from two ractors.
assert_equal '["r1", "r2"]', %q{
# select 2
- r1 = Ractor.new{'r1'}
- r2 = Ractor.new{'r2'}
- rs = [r1, r2]
+ p1 = Ractor::Port.new
+ p2 = Ractor::Port.new
+ r1 = Ractor.new(p1){|p1| p1 << 'r1'}
+ r2 = Ractor.new(p2){|p2| p2 << 'r2'}
+ ps = [p1, p2]
as = []
- r, obj = Ractor.select(*rs)
- rs.delete(r)
+ port, obj = Ractor.select(*ps)
+ ps.delete(port)
as << obj
- r, obj = Ractor.select(*rs)
+ port, obj = Ractor.select(*ps)
as << obj
as.sort #=> ["r1", "r2"]
}
@@ -286,7 +316,7 @@ assert_equal 30.times.map { 'ok' }.to_s, %q{
} unless (ENV.key?('TRAVIS') && ENV['TRAVIS_CPU_ARCH'] == 'arm64') # https://bugs.ruby-lang.org/issues/17878
# Exception for empty select
-assert_match /specify at least one ractor/, %q{
+assert_match /specify at least one Ractor::Port or Ractor/, %q{
begin
Ractor.select
rescue ArgumentError => e
@@ -294,30 +324,12 @@ assert_match /specify at least one ractor/, %q{
end
}
-# Outgoing port of a ractor will be closed when the Ractor is terminated.
-assert_equal 'ok', %q{
- r = Ractor.new do
- 'finish'
- end
-
- r.take
- sleep 0.1 until r.inspect =~ /terminated/
-
- begin
- o = r.take
- rescue Ractor::ClosedError
- 'ok'
- else
- "ng: #{o}"
- end
-}
-
# Raise Ractor::ClosedError when try to send into a terminated ractor
assert_equal 'ok', %q{
r = Ractor.new do
end
- r.take # closed
+ r.join # closed
sleep 0.1 until r.inspect =~ /terminated/
begin
@@ -329,47 +341,16 @@ assert_equal 'ok', %q{
end
}
-# Raise Ractor::ClosedError when try to send into a closed actor
-assert_equal 'ok', %q{
- r = Ractor.new { Ractor.receive }
- r.close_incoming
-
- begin
- r.send(1)
- rescue Ractor::ClosedError
- 'ok'
- else
- 'ng'
- end
-}
-
-# Raise Ractor::ClosedError when try to take from closed actor
-assert_equal 'ok', %q{
- r = Ractor.new do
- Ractor.yield 1
- Ractor.receive
- end
-
- r.close_outgoing
- begin
- r.take
- rescue Ractor::ClosedError
- 'ok'
- else
- 'ng'
- end
-}
-
-# Can mix with Thread#interrupt and Ractor#take [Bug #17366]
+# Can mix with Thread#interrupt and Ractor#join [Bug #17366]
assert_equal 'err', %q{
- Ractor.new{
+ Ractor.new do
t = Thread.current
begin
Thread.new{ t.raise "err" }.join
rescue => e
e.message
end
- }.take
+ end.value
}
# Killed Ractor's thread yields nil
@@ -377,34 +358,18 @@ assert_equal 'nil', %q{
Ractor.new{
t = Thread.current
Thread.new{ t.kill }.join
- }.take.inspect #=> nil
+ }.value.inspect #=> nil
}
-# Ractor.yield raises Ractor::ClosedError when outgoing port is closed.
+# Raise Ractor::ClosedError when try to send into a ractor with closed default port
assert_equal 'ok', %q{
- r = Ractor.new Ractor.current do |main|
+ r = Ractor.new {
+ Ractor.current.close
+ Ractor.main << :ok
Ractor.receive
- main << true
- Ractor.yield 1
- end
-
- r.close_outgoing
- r << true
- Ractor.receive
-
- begin
- r.take
- rescue Ractor::ClosedError
- 'ok'
- else
- 'ng'
- end
-}
+ }
-# Raise Ractor::ClosedError when try to send into a ractor with closed incoming port
-assert_equal 'ok', %q{
- r = Ractor.new { Ractor.receive }
- r.close_incoming
+ Ractor.receive # wait for ok
begin
r.send(1)
@@ -415,154 +380,82 @@ assert_equal 'ok', %q{
end
}
-# A ractor with closed incoming port still can send messages out
-assert_equal '[1, 2]', %q{
- r = Ractor.new do
- Ractor.yield 1
- 2
- end
- r.close_incoming
-
- [r.take, r.take]
-}
-
-# Raise Ractor::ClosedError when try to take from a ractor with closed outgoing port
-assert_equal 'ok', %q{
- r = Ractor.new do
- Ractor.yield 1
- Ractor.receive
- end
-
- sleep 0.01 # wait for Ractor.yield in r
- r.close_outgoing
- begin
- r.take
- rescue Ractor::ClosedError
- 'ok'
- else
- 'ng'
- end
-}
-
-# A ractor with closed outgoing port still can receive messages from incoming port
-assert_equal 'ok', %q{
- r = Ractor.new do
- Ractor.receive
- end
-
- r.close_outgoing
- begin
- r.send(1)
- rescue Ractor::ClosedError
- 'ng'
- else
- 'ok'
- end
-}
-
# Ractor.main returns main ractor
assert_equal 'true', %q{
Ractor.new{
Ractor.main
- }.take == Ractor.current
+ }.value == Ractor.current
}
# a ractor with closed outgoing port should terminate
assert_equal 'ok', %q{
Ractor.new do
- close_outgoing
+ Ractor.current.close
end
true until Ractor.count == 1
:ok
}
-# multiple Ractors can receive (wait) from one Ractor
-assert_equal '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', %q{
- pipe = Ractor.new do
- loop do
- Ractor.yield Ractor.receive
- end
+# an exception in a Ractor main thread will be re-raised at Ractor#receive
+assert_equal '[RuntimeError, "ok", true]', %q{
+ r = Ractor.new do
+ raise 'ok' # exception will be transferred receiver
end
-
- RN = 10
- rs = RN.times.map{|i|
- Ractor.new pipe, i do |pipe, i|
- msg = pipe.take
- msg # ping-pong
- end
- }
- RN.times{|i|
- pipe << i
- }
- RN.times.map{
- r, n = Ractor.select(*rs)
- rs.delete r
- n
- }.sort
-} unless /mswin/ =~ RUBY_PLATFORM # randomly hangs on mswin https://github.com/ruby/ruby/actions/runs/3753871445/jobs/6377551069#step:20:131
-
-# Ractor.select also support multiple take, receive and yield
-assert_equal '[true, true, true]', %q{
- RN = 10
- CR = Ractor.current
-
- rs = (1..RN).map{
- Ractor.new do
- CR.send 'send' + CR.take #=> 'sendyield'
- 'take'
- end
- }
- received = []
- taken = []
- yielded = []
- until received.size == RN && taken.size == RN && yielded.size == RN
- r, v = Ractor.select(CR, *rs, yield_value: 'yield')
- case r
- when :receive
- received << v
- when :yield
- yielded << v
- else
- taken << v
- rs.delete r
- end
+ begin
+ r.join
+ rescue Ractor::RemoteError => e
+ [e.cause.class, #=> RuntimeError
+ e.cause.message, #=> 'ok'
+ e.ractor == r] #=> true
end
- r = [received == ['sendyield'] * RN,
- yielded == [nil] * RN,
- taken == ['take'] * RN,
- ]
-
- STDERR.puts [received, yielded, taken].inspect
- r
}
-# multiple Ractors can send to one Ractor
-assert_equal '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]', %q{
- pipe = Ractor.new do
- loop do
- Ractor.yield Ractor.receive
- end
+# an exception in a Ractor will be re-raised at Ractor#value
+assert_equal '[RuntimeError, "ok", true]', %q{
+ r = Ractor.new do
+ raise 'ok' # exception will be transferred receiver
end
+ begin
+ r.value
+ rescue Ractor::RemoteError => e
+ [e.cause.class, #=> RuntimeError
+ e.cause.message, #=> 'ok'
+ e.ractor == r] #=> true
+ end
+}
- RN = 10
- RN.times.map{|i|
- Ractor.new pipe, i do |pipe, i|
- pipe << i
+# an exception in a Ractor non-main thread will not be re-raised at Ractor#receive
+assert_equal 'ok', %q{
+ r = Ractor.new do
+ Thread.new do
+ raise 'ng'
end
- }
- RN.times.map{
- pipe.take
- }.sort
+ sleep 0.1
+ 'ok'
+ end
+ r.value
}
-# an exception in a Ractor will be re-raised at Ractor#receive
-assert_equal '[RuntimeError, "ok", true]', %q{
- r = Ractor.new do
- raise 'ok' # exception will be transferred receiver
+# SystemExit from a Ractor is re-raised
+# [Bug #21505]
+assert_equal '[SystemExit, "exit", true]', %q{
+ r = Ractor.new { exit }
+ begin
+ r.value
+ rescue Ractor::RemoteError => e
+ [e.cause.class, #=> RuntimeError
+ e.cause.message, #=> 'ok'
+ e.ractor == r] #=> true
end
+}
+
+# SystemExit from a Thread inside a Ractor is re-raised
+# [Bug #21505]
+assert_equal '[SystemExit, "exit", true]', %q{
+ r = Ractor.new { Thread.new { exit }.join }
begin
- r.take
+ r.value
rescue Ractor::RemoteError => e
[e.cause.class, #=> RuntimeError
e.cause.message, #=> 'ok'
@@ -571,7 +464,7 @@ assert_equal '[RuntimeError, "ok", true]', %q{
}
# threads in a ractor will killed
-assert_equal '{:ok=>3}', %q{
+assert_equal '{ok: 3}', %q{
Ractor.new Ractor.current do |main|
q = Thread::Queue.new
Thread.new do
@@ -601,7 +494,7 @@ assert_equal '{:ok=>3}', %q{
end
3.times.map{Ractor.receive}.tally
-} unless yjit_enabled? # `[BUG] Bus Error at 0x000000010b7002d0` in jit_exec()
+} unless yjit_enabled? # YJIT: `[BUG] Bus Error at 0x000000010b7002d0` in jit_exec()
# unshareable object are copied
assert_equal 'false', %q{
@@ -610,18 +503,18 @@ assert_equal 'false', %q{
msg.object_id
end
- obj.object_id == r.take
+ obj.object_id == r.value
}
# To copy the object, now Marshal#dump is used
-assert_equal "allocator undefined for Thread", %q{
+assert_match /can't clone unshareable instance of Thread/, %q{
obj = Thread.new{}
begin
r = Ractor.new obj do |msg|
msg
end
- rescue TypeError => e
- e.message #=> no _dump_data is defined for class Thread
+ rescue Ractor::Error => e
+ e.message
else
'ng'
end
@@ -629,10 +522,11 @@ assert_equal "allocator undefined for Thread", %q{
# send shareable and unshareable objects
assert_equal "ok", <<~'RUBY', frozen_string_literal: false
- echo_ractor = Ractor.new do
+ port = Ractor::Port.new
+ echo_ractor = Ractor.new port do |port|
loop do
v = Ractor.receive
- Ractor.yield v
+ port << v
end
end
@@ -680,13 +574,13 @@ assert_equal "ok", <<~'RUBY', frozen_string_literal: false
shareable_objects.map{|o|
echo_ractor << o
- o2 = echo_ractor.take
+ o2 = port.receive
results << "#{o} is copied" unless o.object_id == o2.object_id
}
unshareable_objects.map{|o|
echo_ractor << o
- o2 = echo_ractor.take
+ o2 = port.receive
results << "#{o.inspect} is not copied" if o.object_id == o2.object_id
}
@@ -712,7 +606,7 @@ assert_equal [false, true, false].inspect, <<~'RUBY', frozen_string_literal: fal
def check obj1
obj2 = Ractor.new obj1 do |obj|
obj
- end.take
+ end.value
obj1.object_id == obj2.object_id
end
@@ -734,7 +628,7 @@ assert_equal 'hello world', <<~'RUBY', frozen_string_literal: false
str = 'hello'
r.send str, move: true
- modified = r.take
+ modified = r.value
begin
str << ' exception' # raise Ractor::MovedError
@@ -754,7 +648,7 @@ assert_equal '[0, 1]', %q{
a1 = [0]
r.send a1, move: true
- a2 = r.take
+ a2 = r.value
begin
a1 << 2 # raise Ractor::MovedError
rescue Ractor::MovedError
@@ -762,70 +656,39 @@ assert_equal '[0, 1]', %q{
end
}
-# move with yield
-assert_equal 'hello', %q{
- r = Ractor.new do
- Thread.current.report_on_exception = false
- obj = 'hello'
- Ractor.yield obj, move: true
- obj << 'world'
- end
-
- str = r.take
- begin
- r.take
- rescue Ractor::RemoteError
- str #=> "hello"
- end
-}
-
-# yield/move should not make moved object when the yield is not succeeded
-assert_equal '"str"', %q{
- R = Ractor.new{}
- M = Ractor.current
- r = Ractor.new do
- s = 'str'
- selected_r, v = Ractor.select R, yield_value: s, move: true
- raise if selected_r != R # taken from R
- M.send s.inspect # s should not be a moved object
- end
-
- Ractor.receive
-}
-
-# yield/move can fail
-assert_equal "allocator undefined for Thread", %q{
+# unshareable frozen objects should still be frozen in new ractor after move
+assert_equal 'true', %q{
r = Ractor.new do
- obj = Thread.new{}
- Ractor.yield obj
- rescue => e
- e.message
+ obj = receive
+ { frozen: obj.frozen? }
end
- r.take
+ obj = [Object.new].freeze
+ r.send(obj, move: true)
+ r.value[:frozen]
}
-# Access to global-variables are prohibited
-assert_equal 'can not access global variables $gv from non-main Ractors', %q{
+# Access to global-variables are prohibited (read)
+assert_equal 'can not access global variable $gv from non-main Ractor', %q{
$gv = 1
r = Ractor.new do
$gv
end
begin
- r.take
+ r.join
rescue Ractor::RemoteError => e
e.cause.message
end
}
-# Access to global-variables are prohibited
-assert_equal 'can not access global variables $gv from non-main Ractors', %q{
+# Access to global-variables are prohibited (write)
+assert_equal 'can not access global variable $gv from non-main Ractor', %q{
r = Ractor.new do
$gv = 1
end
begin
- r.take
+ r.join
rescue Ractor::RemoteError => e
e.cause.message
end
@@ -839,7 +702,7 @@ assert_equal 'ok', %q{
}
end
- [$stdin, $stdout, $stderr].zip(r.take){|io, (oid, fno)|
+ [$stdin, $stdout, $stderr].zip(r.value){|io, (oid, fno)|
raise "should not be different object" if io.object_id == oid
raise "fd should be same" unless io.fileno == fno
}
@@ -855,7 +718,7 @@ assert_equal 'ok', %q{
'ok'
end
- r.take
+ r.value
}
# $DEBUG, $VERBOSE are Ractor local
@@ -913,7 +776,7 @@ assert_equal 'true', %q{
h = Ractor.new do
ractor_local_globals
- end.take
+ end.value
ractor_local_globals == h #=> true
}
@@ -922,7 +785,8 @@ assert_equal 'false', %q{
r = Ractor.new do
self.object_id
end
- r.take == self.object_id #=> false
+ ret = r.value
+ ret == self.object_id
}
# self is a Ractor instance
@@ -930,11 +794,16 @@ assert_equal 'true', %q{
r = Ractor.new do
self.object_id
end
- r.object_id == r.take #=> true
+ ret = r.value
+ if r.object_id == ret #=> true
+ true
+ else
+ raise [ret, r.object_id].inspect
+ end
}
# given block Proc will be isolated, so can not access outer variables.
-assert_equal 'ArgumentError', %q{
+assert_equal 'Ractor::IsolationError', %q{
begin
a = true
r = Ractor.new do
@@ -945,8 +814,39 @@ assert_equal 'ArgumentError', %q{
end
}
+# eval with outer locals in a Ractor raises SyntaxError
+# [Bug #21522]
+assert_equal 'SyntaxError', %q{
+ outer = 42
+ r = Ractor.new do
+ eval("outer")
+ end
+ begin
+ r.value
+ rescue Ractor::RemoteError => e
+ e.cause.class
+ end
+}
+
+# eval of an undefined name in a Ractor raises NameError
+assert_equal 'NameError', %q{
+ r = Ractor.new do
+ eval("totally_undefined_name")
+ end
+ begin
+ r.value
+ rescue Ractor::RemoteError => e
+ e.cause.class
+ end
+}
+
+# eval of a local defined inside the Ractor works
+assert_equal '99', %q{
+ Ractor.new { inner = 99; eval("inner").to_s }.value
+}
+
# ivar in shareable-objects are not allowed to access from non-main Ractor
-assert_equal "can not get unshareable values from instance variables of classes/modules from non-main Ractors", <<~'RUBY', frozen_string_literal: false
+assert_equal "can not get unshareable values from instance variables of classes/modules from non-main Ractors (@iv from C)", <<~'RUBY', frozen_string_literal: false
class C
@iv = 'str'
end
@@ -958,7 +858,7 @@ assert_equal "can not get unshareable values from instance variables of classes/
end
begin
- r.take
+ r.value
rescue Ractor::RemoteError => e
e.cause.message
end
@@ -974,7 +874,7 @@ assert_equal 'can not access instance variables of shareable objects from non-ma
end
begin
- r.take
+ r.value
rescue Ractor::RemoteError => e
e.cause.message
end
@@ -1000,7 +900,7 @@ assert_equal 'can not access instance variables of shareable objects from non-ma
end
begin
- r.take
+ r.value
rescue Ractor::RemoteError => e
e.cause.message
end
@@ -1021,7 +921,7 @@ assert_equal 'can not access instance variables of shareable objects from non-ma
end
begin
- r.take
+ r.value
rescue Ractor::RemoteError => e
e.cause.message
end
@@ -1035,7 +935,7 @@ assert_equal '11', %q{
Ractor.new obj do |obj|
obj.instance_variable_get('@a')
- end.take.to_s
+ end.value.to_s
}.join
}
@@ -1061,25 +961,25 @@ assert_equal '333', %q{
def self.fstr = @fstr
end
- a = Ractor.new{ C.int }.take
+ a = Ractor.new{ C.int }.value
b = Ractor.new do
C.str.to_i
rescue Ractor::IsolationError
10
- end.take
+ end.value
c = Ractor.new do
C.fstr.to_i
- end.take
+ end.value
- d = Ractor.new{ M.int }.take
+ d = Ractor.new{ M.int }.value
e = Ractor.new do
M.str.to_i
rescue Ractor::IsolationError
20
- end.take
+ end.value
f = Ractor.new do
M.fstr.to_i
- end.take
+ end.value
# 1 + 10 + 100 + 2 + 20 + 200
@@ -1097,32 +997,32 @@ assert_equal '["instance-variable", "instance-variable", nil]', %q{
Ractor.new{
[C.iv1, C.iv2, C.iv3]
- }.take
+ }.value
}
# moved objects have their shape properly set to original object's shape
assert_equal '1234', %q{
-class Obj
- attr_accessor :a, :b, :c, :d
- def initialize
- @a = 1
- @b = 2
- @c = 3
+ class Obj
+ attr_accessor :a, :b, :c, :d
+ def initialize
+ @a = 1
+ @b = 2
+ @c = 3
+ end
end
-end
-r = Ractor.new do
- obj = receive
- obj.d = 4
- [obj.a, obj.b, obj.c, obj.d]
-end
-obj = Obj.new
-r.send(obj, move: true)
-values = r.take
-values.join
+ r = Ractor.new do
+ obj = receive
+ obj.d = 4
+ [obj.a, obj.b, obj.c, obj.d]
+ end
+ obj = Obj.new
+ r.send(obj, move: true)
+ values = r.value
+ values.join
}
-# cvar in shareable-objects are not allowed to access from non-main Ractor
-assert_equal 'can not access class variables from non-main Ractors', %q{
+# Reading non-shareable cvar from non-main Ractor is not allowed
+assert_equal 'can not read non-shareable class variable @@cv from non-main Ractors (C)', %q{
class C
@@cv = 'str'
end
@@ -1134,14 +1034,14 @@ assert_equal 'can not access class variables from non-main Ractors', %q{
end
begin
- r.take
+ r.join
rescue Ractor::RemoteError => e
e.cause.message
end
}
-# also cached cvar in shareable-objects are not allowed to access from non-main Ractor
-assert_equal 'can not access class variables from non-main Ractors', %q{
+# also cached non-shareable cvar read from non-main Ractor is not allowed
+assert_equal 'can not read non-shareable class variable @@cv from non-main Ractors (C)', %q{
class C
@@cv = 'str'
def self.cv
@@ -1156,12 +1056,101 @@ assert_equal 'can not access class variables from non-main Ractors', %q{
end
begin
- r.take
+ r.join
rescue Ractor::RemoteError => e
e.cause.message
end
}
+# Reading shareable cvar from non-main Ractor is allowed
+assert_equal 'shareable', %q{
+ class C
+ @@cv = 'shareable'.freeze
+ def self.cv
+ @@cv
+ end
+ end
+
+ Ractor.new { C.cv }.value
+}
+
+# Reading shareable cvar (integer) from non-main Ractor is allowed
+assert_equal '42', %q{
+ class C
+ @@cv = 42
+ def self.cv
+ @@cv
+ end
+ end
+
+ Ractor.new { C.cv }.value.to_s
+}
+
+# Reading shareable cvar via module include from non-main Ractor is allowed
+assert_equal 'hello', %q{
+ module M
+ @@cv = 'hello'.freeze
+ def self.cv
+ @@cv
+ end
+ end
+
+ class C
+ include M
+ def self.cv
+ @@cv
+ end
+ end
+
+ Ractor.new { C.cv }.value
+}
+
+# Writing cvar from non-main Ractor is not allowed
+assert_equal 'can not set class variables from non-main Ractors (@@cv from C)', %q{
+ class C
+ @@cv = 'str'
+ def self.cv=(v)
+ @@cv = v
+ end
+ end
+
+ r = Ractor.new do
+ C.cv = 'new'
+ end
+
+ begin
+ r.join
+ rescue Ractor::RemoteError => e
+ e.cause.message
+ end
+}
+
+# Reading cvar that was made shareable after initial assignment
+assert_equal 'made shareable', %q{
+ class C
+ @@cv = +'made shareable'
+ Ractor.make_shareable(@@cv)
+ def self.cv
+ @@cv
+ end
+ end
+
+ Ractor.new { C.cv }.value
+}
+
+# cvar_defined? works from non-main Ractor
+assert_equal 'true', %q{
+ class C
+ @@cv = 42
+ def self.cv?
+ defined?(@@cv)
+ end
+ end
+
+ r = Ractor.new { C.cv? ? 'true' : 'false' }
+ r.value
+}
+
# Getting non-shareable objects via constants by other Ractors is not allowed
assert_equal 'can not access non-shareable objects in constant C::CONST by non-main Ractor.', <<~'RUBY', frozen_string_literal: false
class C
@@ -1171,19 +1160,33 @@ assert_equal 'can not access non-shareable objects in constant C::CONST by non-m
C::CONST
end
begin
- r.take
+ r.join
rescue Ractor::RemoteError => e
e.cause.message
end
RUBY
-# Constant cache should care about non-sharable constants
+# Constant cache should care about non-shareable constants
assert_equal "can not access non-shareable objects in constant Object::STR by non-main Ractor.", <<~'RUBY', frozen_string_literal: false
STR = "hello"
def str; STR; end
s = str() # fill const cache
begin
- Ractor.new{ str() }.take
+ Ractor.new{ str() }.join
+ rescue Ractor::RemoteError => e
+ e.cause.message
+ end
+RUBY
+
+# The correct constant path shall be reported
+assert_equal "can not access non-shareable objects in constant Object::STR by non-main Ractor.", <<~'RUBY', frozen_string_literal: false
+ STR = "hello"
+ module M
+ def self.str; STR; end
+ end
+
+ begin
+ Ractor.new{ M.str }.join
rescue Ractor::RemoteError => e
e.cause.message
end
@@ -1197,7 +1200,7 @@ assert_equal 'can not set constants with non-shareable objects by non-main Racto
C::CONST = 'str'
end
begin
- r.take
+ r.join
rescue Ractor::RemoteError => e
e.cause.message
end
@@ -1208,7 +1211,7 @@ assert_equal "defined with an un-shareable Proc in a different Ractor", %q{
str = "foo"
define_method(:buggy){|i| str << "#{i}"}
begin
- Ractor.new{buggy(10)}.take
+ Ractor.new{buggy(10)}.join
rescue => e
e.cause.message
end
@@ -1219,7 +1222,7 @@ assert_equal '[1000, 3]', %q{
A = Array.new(1000).freeze # [nil, ...]
H = {a: 1, b: 2, c: 3}.freeze
- Ractor.new{ [A.size, H.size] }.take
+ Ractor.new{ [A.size, H.size] }.value
}
# Ractor.count
@@ -1229,15 +1232,15 @@ assert_equal '[1, 4, 3, 2, 1]', %q{
ractors = (1..3).map { Ractor.new { Ractor.receive } }
counts << Ractor.count
- ractors[0].send('End 0').take
+ ractors[0].send('End 0').join
sleep 0.1 until ractors[0].inspect =~ /terminated/
counts << Ractor.count
- ractors[1].send('End 1').take
+ ractors[1].send('End 1').join
sleep 0.1 until ractors[1].inspect =~ /terminated/
counts << Ractor.count
- ractors[2].send('End 2').take
+ ractors[2].send('End 2').join
sleep 0.1 until ractors[2].inspect =~ /terminated/
counts << Ractor.count
@@ -1250,7 +1253,7 @@ assert_equal '0', %q{
n = 0
ObjectSpace.each_object{|o| n += 1 unless Ractor.shareable?(o)}
n
- }.take
+ }.value
}
# ObjectSpace._id2ref can not handle unshareable objects with Ractors
@@ -1263,7 +1266,25 @@ assert_equal 'ok', <<~'RUBY', frozen_string_literal: false
rescue => e
:ok
end
- end.take
+ end.value
+RUBY
+
+# Inserting into the id2ref table should be Ractor-safe
+assert_equal 'ok', <<~'RUBY'
+ # Force all calls to Kernel#object_id to insert into the id2ref table
+ obj = Object.new
+ ObjectSpace._id2ref(obj.object_id) rescue nil
+
+ 10.times.map do
+ Ractor.new do
+ 10_000.times do
+ a = Object.new
+ a.object_id
+ end
+ end
+ end.map(&:value)
+
+ :ok
RUBY
# Ractor.make_shareable(obj)
@@ -1347,19 +1368,42 @@ assert_equal 'true', %q{
[a.frozen?, a[0].frozen?] == [true, false]
}
-# Ractor.make_shareable(a_proc) makes a proc shareable.
-assert_equal 'true', %q{
- a = [1, [2, 3], {a: "4"}]
+# Ractor.make_shareable(a_proc) requires a shareable receiver
+assert_equal '[:ok, "Proc\'s self is not shareable:"]', %q{
+ pr1 = nil.instance_exec { Proc.new{} }
+ pr2 = Proc.new{}
- pr = Ractor.current.instance_eval do
- Proc.new do
- a
+ [pr1, pr2].map do |pr|
+ begin
+ Ractor.make_shareable(pr)
+ rescue Ractor::Error => e
+ e.message[/^.+?:/]
+ else
+ :ok
end
end
+}
- Ractor.make_shareable(a) # referred value should be shareable
- Ractor.make_shareable(pr)
- Ractor.shareable?(pr)
+# Ractor.make_shareable(Method/UnboundMethod)
+assert_equal 'true', %q{
+ # raise because receiver is unshareable
+ begin
+ _m0 = Ractor.make_shareable(self.method(:__id__))
+ rescue => e
+ raise e unless e.message =~ /can not make shareable object/
+ else
+ raise "no error"
+ end
+
+ # Method with shareable receiver
+ M1 = Ractor.make_shareable(Object.method(:__id__))
+
+ # UnboundMethod
+ M2 = Ractor.make_shareable(Object.instance_method(:__id__))
+
+ Ractor.new do
+ Object.__id__ == M1.call && M1.call == M2.bind_call(Object)
+ end.value
}
# Ractor.shareable?(recursive_objects)
@@ -1394,29 +1438,10 @@ assert_equal '[C, M]', %q{
assert_equal '1', %q{
class C
a = 1
- define_method "foo", Ractor.make_shareable(Proc.new{ a })
- a = 2
- end
-
- Ractor.new{ C.new.foo }.take
-}
-
-# Ractor.make_shareable(a_proc) makes a proc shareable.
-assert_equal 'can not make a Proc shareable because it accesses outer variables (a).', %q{
- a = b = nil
- pr = Ractor.current.instance_eval do
- Proc.new do
- c = b # assign to a is okay because c is block local variable
- # reading b is okay
- a = b # assign to a is not allowed #=> Ractor::Error
- end
+ define_method "foo", Ractor.shareable_proc{ a }
end
- begin
- Ractor.make_shareable(pr)
- rescue => e
- e.message
- end
+ Ractor.new{ C.new.foo }.value
}
# Ractor.make_shareable(obj, copy: true) makes copied shareable object.
@@ -1441,7 +1466,7 @@ assert_equal '[6, 10]', %q{
Ractor.new{ # line 5
a = 1
b = 2
- }.take
+ }.value
c = 3 # line 9
end
rs
@@ -1451,7 +1476,7 @@ assert_equal '[6, 10]', %q{
assert_equal '[true, false]', %q{
Ractor.new([[]].freeze) { |ary|
[ary.frozen?, ary.first.frozen? ]
- }.take
+ }.value
}
# Ractor deep copies frozen objects (str)
@@ -1459,7 +1484,7 @@ assert_equal '[true, false]', %q{
s = String.new.instance_eval { @x = []; freeze}
Ractor.new(s) { |s|
[s.frozen?, s.instance_variable_get(:@x).frozen?]
- }.take
+ }.value
}
# Can not trap with not isolated Proc on non-main ractor
@@ -1467,15 +1492,15 @@ assert_equal '[:ok, :ok]', %q{
a = []
Ractor.new{
trap(:INT){p :ok}
- }.take
+ }.join
a << :ok
begin
Ractor.new{
s = 'str'
trap(:INT){p s}
- }.take
- rescue => Ractor::RemoteError
+ }.join
+ rescue Ractor::RemoteError
a << :ok
end
}
@@ -1504,15 +1529,48 @@ assert_equal '[nil, "b", "a"]', %q{
ans = []
Ractor.current[:key] = 'a'
r = Ractor.new{
- Ractor.yield self[:key]
+ Ractor.main << self[:key]
self[:key] = 'b'
self[:key]
}
- ans << r.take
- ans << r.take
+ ans << Ractor.receive
+ ans << r.value
ans << Ractor.current[:key]
}
+assert_equal '1', %q{
+ N = 1_000
+ Ractor.new{
+ a = []
+ 1_000.times.map{|i|
+ Thread.new(i){|i|
+ Thread.pass if i < N
+ a << Ractor.store_if_absent(:i){ i }
+ a << Ractor.current[:i]
+ }
+ }.each(&:join)
+ a.uniq.size
+ }.value
+}
+
+# Ractor-local storage
+assert_equal '2', %q{
+ Ractor.new {
+ fails = 0
+ begin
+ Ractor.main[:key] # cannot get ractor local storage from non-main ractor
+ rescue => e
+ fails += 1 if e.message =~ /Cannot get ractor local/
+ end
+ begin
+ Ractor.main[:key] = 'val'
+ rescue => e
+ fails += 1 if e.message =~ /Cannot set ractor local/
+ end
+ fails
+ }.value
+}
+
###
### Synchronization tests
###
@@ -1526,10 +1584,28 @@ assert_equal "#{N}#{N}", %Q{
Ractor.new{
N.times{|i| -(i.to_s)}
}
- }.map{|r| r.take}.join
+ }.map{|r| r.value}.join
}
-# Generic ivtbl
+assert_equal "ok", %Q{
+ N = #{N}
+ a, b = 2.times.map{
+ Ractor.new{
+ N.times.map{|i| -(i.to_s)}
+ }
+ }.map{|r| r.value}
+ N.times do |i|
+ unless a[i].equal?(b[i])
+ raise [a[i], b[i]].inspect
+ end
+ unless a[i] == i.to_s
+ raise [i, a[i], b[i]].inspect
+ end
+ end
+ :ok
+}
+
+# Generic fields_tbl
n = N/2
assert_equal "#{n}#{n}", %Q{
2.times.map{
@@ -1542,21 +1618,20 @@ assert_equal "#{n}#{n}", %Q{
obj.instance_variable_defined?("@a")
end
end
- }.map{|r| r.take}.join
+ }.map{|r| r.value}.join
}
-# NameError
-assert_equal "ok", %q{
+# Now NoMethodError is copyable
+assert_equal "NoMethodError", %q{
obj = "".freeze # NameError refers the receiver indirectly
begin
obj.bar
rescue => err
end
- begin
- Ractor.new{} << err
- rescue TypeError
- 'ok'
- end
+
+ r = Ractor.new{ Ractor.receive }
+ r << err
+ r.value.class
}
assert_equal "ok", %q{
@@ -1574,54 +1649,27 @@ assert_equal "ok", %q{
# Can yield back values while GC is sweeping [Bug #18117]
assert_equal "ok", %q{
+ port = Ractor::Port.new
workers = (0...8).map do
- Ractor.new do
+ Ractor.new port do |port|
loop do
10_000.times.map { Object.new }
- Ractor.yield Time.now
+ port << Time.now
+ Ractor.receive
end
end
end
- 1_000.times { idle_worker, tmp_reporter = Ractor.select(*workers) }
+ 100.times {
+ workers.each do
+ port.receive
+ end
+ workers.each do |w|
+ w.send(nil)
+ end
+ }
"ok"
-} unless yjit_enabled? || rjit_enabled? # flaky
-
-assert_equal "ok", %q{
- def foo(*); ->{ super }; end
- begin
- Ractor.make_shareable(foo)
- rescue Ractor::IsolationError
- "ok"
- end
-}
-
-assert_equal "ok", %q{
- def foo(**); ->{ super }; end
- begin
- Ractor.make_shareable(foo)
- rescue Ractor::IsolationError
- "ok"
- end
-}
-
-assert_equal "ok", %q{
- def foo(...); ->{ super }; end
- begin
- Ractor.make_shareable(foo)
- rescue Ractor::IsolationError
- "ok"
- end
-}
-
-assert_equal "ok", %q{
- def foo((x), (y)); ->{ super }; end
- begin
- Ractor.make_shareable(foo([], []))
- rescue Ractor::IsolationError
- "ok"
- end
-}
+} if !yjit_enabled? && ENV['GITHUB_WORKFLOW'] != 'ModGC' # flaky
# check method cache invalidation
assert_equal "ok", %q{
@@ -1686,14 +1734,41 @@ assert_equal 'true', %q{
}
n = CS.inject(1){|r, c| r * c.foo} * LN
- rs.map{|r| r.take} == Array.new(RN){n}
+ rs.map{|r| r.value} == Array.new(RN){n}
+}
+
+# check method cache invalidation
+assert_equal 'true', %q{
+ class Foo
+ def hello = nil
+ end
+
+ r1 = Ractor.new do
+ 1000.times do
+ class Foo
+ def hello = nil
+ end
+ end
+ end
+
+ r2 = Ractor.new do
+ 1000.times do
+ o = Foo.new
+ o.hello
+ end
+ end
+
+ r1.value
+ r2.value
+
+ true
}
# check experimental warning
-assert_match /\Atest_ractor\.rb:1:\s+warning:\s+Ractor is experimental/, %q{
+assert_match /\Atest_ractor\.rb:1:\s+warning:\s+Ractor API is experimental/, %q{
Warning[:experimental] = $VERBOSE = true
STDERR.reopen(STDOUT)
- eval("Ractor.new{}.take", nil, "test_ractor.rb", 1)
+ eval("Ractor.new{}.value", nil, "test_ractor.rb", 1)
}, frozen_string_literal: false
# check moved object
@@ -1711,7 +1786,7 @@ assert_equal 'ok', %q{
end
r.send obj, move: true
- r.take
+ r.value
}
## Ractor::Selector
@@ -1787,10 +1862,11 @@ assert_equal '600', %q{
RN = 100
s = Ractor::Selector.new
+ port = Ractor::Port.new
rs = RN.times.map{
Ractor.new{
- Ractor.main << Ractor.new{ Ractor.yield :v3; :v4 }
- Ractor.main << Ractor.new{ Ractor.yield :v5; :v6 }
+ Ractor.main << Ractor.new(port){|port| port << :v3; :v4 }
+ Ractor.main << Ractor.new(port){|port| port << :v5; :v6 }
Ractor.yield :v1
:v2
}
@@ -1836,3 +1912,755 @@ assert_equal 'true', %q{
shareable = Ractor.make_shareable("chilled")
shareable == "chilled" && Ractor.shareable?(shareable)
}
+
+# require in Ractor
+assert_equal 'true', %q{
+ Module.new do
+ def require feature
+ return Ractor._require(feature) unless Ractor.main?
+ super
+ end
+ Object.prepend self
+ set_temporary_name 'Ractor#require'
+ end
+
+ Ractor.new{
+ begin
+ require 'tempfile'
+ Tempfile.new
+ rescue SystemStackError
+ # prism parser with -O0 build consumes a lot of machine stack
+ Data.define(:fileno).new(1)
+ end
+ }.value.fileno > 0
+}
+
+# require_relative in Ractor
+assert_equal 'true', %q{
+ dummyfile = File.join(__dir__, "dummy#{rand}.rb")
+ return true if File.exist?(dummyfile)
+
+ begin
+ File.write dummyfile, ''
+ rescue Exception
+ # skip on any errors
+ return true
+ end
+
+ begin
+ Ractor.new dummyfile do |f|
+ require_relative File.basename(f)
+ end.value
+ ensure
+ File.unlink dummyfile
+ end
+}
+
+# require_relative in Ractor
+assert_equal 'LoadError', %q{
+ dummyfile = File.join(__dir__, "not_existed_dummy#{rand}.rb")
+ return true if File.exist?(dummyfile)
+
+ Ractor.new dummyfile do |f|
+ begin
+ require_relative File.basename(f)
+ rescue LoadError => e
+ e.class
+ end
+ end.value
+}
+
+# autolaod in Ractor
+assert_equal 'true', %q{
+ autoload :Tempfile, 'tempfile'
+
+ r = Ractor.new do
+ begin
+ Tempfile.new
+ rescue SystemStackError
+ # prism parser with -O0 build consumes a lot of machine stack
+ Data.define(:fileno).new(1)
+ end
+ end
+ r.value.fileno > 0
+}
+
+# failed in autolaod in Ractor
+assert_equal 'LoadError', %q{
+ dummyfile = File.join(__dir__, "not_existed_dummy#{rand}.rb")
+ autoload :Tempfile, dummyfile
+
+ r = Ractor.new do
+ begin
+ Tempfile.new
+ rescue LoadError => e
+ e.class
+ end
+ end
+ r.value
+}
+
+# bind_call in Ractor [Bug #20934]
+assert_equal 'ok', %q{
+ 2.times.map do
+ Ractor.new do
+ 1000.times do
+ Object.instance_method(:itself).bind_call(self)
+ end
+ end
+ end.each(&:join)
+ GC.start
+ :ok.itself
+}
+
+# moved objects being corrupted if embeded (String)
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = "foobarbazfoobarbazfoobarbazfoobarbaz"
+ ractor.send(obj.dup, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj == obj ? :ok : roundtripped_obj
+}
+
+# moved objects being corrupted if embeded (Array)
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = Array.new(10, 42)
+ ractor.send(obj.dup, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj == obj ? :ok : roundtripped_obj
+}
+
+# moved objects being corrupted if embeded (Hash)
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = { foo: 1, bar: 2 }
+ ractor.send(obj.dup, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj == obj ? :ok : roundtripped_obj
+}
+
+# moved objects being corrupted if embeded (MatchData)
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = "foo".match(/o/)
+ ractor.send(obj.dup, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj == obj ? :ok : roundtripped_obj
+}
+
+# moved objects being corrupted if embeded (Struct)
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = Struct.new(:a, :b, :c, :d, :e, :f).new(1, 2, 3, 4, 5, 6)
+ ractor.send(obj.dup, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj == obj ? :ok : roundtripped_obj
+}
+
+# moved objects being corrupted if embeded (Object)
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ class SomeObject
+ attr_reader :a, :b, :c, :d, :e, :f
+ def initialize
+ @a = @b = @c = @d = @e = @f = 1
+ end
+
+ def ==(o)
+ @a == o.a &&
+ @b == o.b &&
+ @c == o.c &&
+ @d == o.d &&
+ @e == o.e &&
+ @f == o.f
+ end
+ end
+
+ SomeObject.new # initial non-embeded
+
+ obj = SomeObject.new
+ ractor.send(obj.dup, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj == obj ? :ok : roundtripped_obj
+}
+
+# moved arrays can't be used
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = [1]
+ ractor.send(obj, move: true)
+ begin
+ [].concat(obj)
+ rescue TypeError
+ :ok
+ else
+ :fail
+ end
+}
+
+# moved strings can't be used
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = "hello"
+ ractor.send(obj, move: true)
+ begin
+ "".replace(obj)
+ rescue TypeError
+ :ok
+ else
+ :fail
+ end
+}
+
+# moved hashes can't be used
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = { a: 1 }
+ ractor.send(obj, move: true)
+ begin
+ {}.merge(obj)
+ rescue TypeError
+ :ok
+ else
+ :fail
+ end
+}
+
+# move objects inside frozen containers
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = Array.new(10, 42)
+ original = obj.dup
+ ractor.send([obj].freeze, move: true)
+ roundtripped_obj = ractor.value[0]
+ roundtripped_obj == original ? :ok : roundtripped_obj
+}
+
+# move object with generic ivar
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = Array.new(10, 42)
+ obj.instance_variable_set(:@array, [1])
+
+ ractor.send(obj, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj.instance_variable_get(:@array) == [1] ? :ok : roundtripped_obj
+}
+
+# move object with many generic ivars
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = Array.new(10, 42)
+ 0.upto(300) do |i|
+ obj.instance_variable_set(:"@array#{i}", [i])
+ end
+
+ ractor.send(obj, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj
+}
+
+# move object with complex generic ivars
+assert_equal 'ok', %q{
+ # Make Array complex
+ 30.times { |i| [].instance_variable_set(:"@complex#{i}", 1) }
+
+ ractor = Ractor.new { Ractor.receive }
+ obj = Array.new(10, 42)
+ obj.instance_variable_set(:@array1, [1])
+
+ ractor.send(obj, move: true)
+ roundtripped_obj = ractor.value
+ roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj
+}
+
+# move object with generic ivars and existing id2ref table
+# [Bug #21664]
+assert_equal 'ok', %q{
+ obj = [1]
+ obj.instance_variable_set("@field", :ok)
+ ObjectSpace._id2ref(obj.object_id) # build id2ref table
+
+ ractor = Ractor.new { Ractor.receive }
+ ractor.send(obj, move: true)
+ obj = ractor.value
+ obj.instance_variable_get("@field")
+}
+
+# copy object with complex generic ivars
+assert_equal 'ok', %q{
+ # Make Array complex
+ 30.times { |i| [].instance_variable_set(:"@complex#{i}", 1) }
+
+ ractor = Ractor.new { Ractor.receive }
+ obj = Array.new(10, 42)
+ obj.instance_variable_set(:@array1, [1])
+
+ ractor.send(obj)
+ roundtripped_obj = ractor.value
+ roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj
+}
+
+# copy object with many generic ivars
+assert_equal 'ok', %q{
+ ractor = Ractor.new { Ractor.receive }
+ obj = Array.new(10, 42)
+ 0.upto(300) do |i|
+ obj.instance_variable_set(:"@array#{i}", [i])
+ end
+
+ ractor.send(obj)
+ roundtripped_obj = ractor.value
+ roundtripped_obj.instance_variable_get(:@array1) == [1] ? :ok : roundtripped_obj
+}
+
+# moved composite types move their non-shareable parts properly
+assert_equal 'ok', %q{
+ k, v = String.new("key"), String.new("value")
+ h = { k => v }
+ h.instance_variable_set("@b", String.new("b"))
+ a = [k,v]
+ o_singleton = Object.new
+ def o_singleton.a
+ @a
+ end
+ o_singleton.instance_variable_set("@a", String.new("a"))
+ class MyObject
+ attr_reader :a
+ def initialize(a)
+ @a = a
+ end
+ end
+ struct_class = Struct.new(:a)
+ struct = struct_class.new(String.new('a'))
+ o = MyObject.new(String.new('a'))
+ port = Ractor::Port.new
+
+ r = Ractor.new port do |port|
+ loop do
+ obj = Ractor.receive
+ val = case obj
+ when Hash
+ obj['key'] == 'value' && obj.instance_variable_get("@b") == 'b'
+ when Array
+ obj[0] == 'key'
+ when Struct
+ obj.a == 'a'
+ when Object
+ obj.a == 'a'
+ end
+ port << val
+ end
+ end
+
+ objs = [h, a, o_singleton, o, struct]
+ objs.each_with_index do |obj, i|
+ klass = obj.class
+ parts_moved = {}
+ case obj
+ when Hash
+ parts_moved[klass] = [obj['key'], obj.instance_variable_get("@b")]
+ when Array
+ parts_moved[klass] = obj.dup # the contents
+ when Struct, Object
+ parts_moved[klass] = [obj.a]
+ end
+ r.send(obj, move: true)
+ val = port.receive
+ if val != true
+ raise "bad val in ractor for obj at i:#{i}"
+ end
+ begin
+ p obj
+ rescue
+ else
+ raise "should be moved"
+ end
+ parts_moved.each do |klass, parts|
+ parts.each_with_index do |part, j|
+ case part
+ when Ractor::MovedObject
+ else
+ raise "part for class #{klass} at i:#{j} should be moved"
+ end
+ end
+ end
+ end
+ 'ok'
+}
+
+# fork after creating Ractor
+assert_equal 'ok', %q{
+begin
+ Ractor.new { Ractor.receive }
+ _, status = Process.waitpid2 fork { }
+ status.success? ? "ok" : status
+rescue NotImplementedError
+ :ok
+end
+}
+
+# Ractors should be terminated after fork
+assert_equal 'ok', %q{
+begin
+ r = Ractor.new { Ractor.receive }
+ _, status = Process.waitpid2 fork {
+ begin
+ raise if r.value != nil
+ end
+ }
+ r.send(123)
+ raise unless r.value == 123
+ status.success? ? "ok" : status
+rescue NotImplementedError
+ :ok
+end
+}
+
+# Ractors should be terminated after fork
+assert_equal 'ok', %q{
+begin
+ r = Ractor.new { Ractor.receive }
+ _, status = Process.waitpid2 fork {
+ begin
+ r.send(123)
+ rescue Ractor::ClosedError
+ end
+ }
+ r.send(123)
+ raise unless r.value == 123
+ status.success? ? "ok" : status
+rescue NotImplementedError
+ :ok
+end
+}
+
+# Creating classes inside of Ractors
+# [Bug #18119]
+assert_equal 'ok', %q{
+ port = Ractor::Port.new
+ workers = (0...8).map do
+ Ractor.new port do |port|
+ loop do
+ 100.times.map { Class.new }
+ port << nil
+ end
+ end
+ end
+
+ 100.times { port.receive }
+
+ 'ok'
+}
+
+# Using Symbol#to_proc inside ractors
+# [Bug #21354]
+assert_equal 'ok', %q{
+ :inspect.to_proc
+ Ractor.new do
+ # It should not use this cached proc, it should create a new one. If it used
+ # the cached proc, we would get a ractor_confirm_belonging error here.
+ :inspect.to_proc
+ end.join
+ 'ok'
+}
+
+# take vm lock when deleting generic ivars from the global table
+assert_equal 'ok', %q{
+ Ractor.new do
+ a = [1, 2, 3]
+ a.object_id
+ a.dup # this deletes generic ivar on dupped object
+ 'ok'
+ end.value
+}
+
+## Ractor#monitor
+
+# monitor port returns `:exited` when the monitering Ractor terminated.
+assert_equal 'true', %q{
+ r = Ractor.new do
+ Ractor.main << :ok1
+ :ok2
+ end
+
+ r.monitor port = Ractor::Port.new
+ Ractor.receive # :ok1
+ port.receive == :exited
+}
+
+# monitor port returns `:exited` even if the monitoring Ractor was terminated.
+assert_equal 'true', %q{
+ r = Ractor.new do
+ :ok
+ end
+
+ r.join # wait for r's terminateion
+
+ r.monitor port = Ractor::Port.new
+ port.receive == :exited
+}
+
+# monitor returns false if the monitoring Ractor was terminated.
+assert_equal 'false', %q{
+ r = Ractor.new do
+ :ok
+ end
+
+ r.join # wait for r's terminateion
+
+ r.monitor Ractor::Port.new
+}
+
+# monitor port returns `:aborted` when the monitering Ractor is aborted.
+assert_equal 'true', %q{
+ r = Ractor.new do
+ Ractor.main << :ok1
+ raise 'ok'
+ end
+
+ r.monitor port = Ractor::Port.new
+ Ractor.receive # :ok1
+ port.receive == :aborted
+}
+
+# monitor port returns `:aborted` even if the monitoring Ractor was aborted.
+assert_equal 'true', %q{
+ r = Ractor.new do
+ raise 'ok'
+ end
+
+ begin
+ r.join # wait for r's terminateion
+ rescue Ractor::RemoteError
+ # ignore
+ end
+
+ r.monitor port = Ractor::Port.new
+ port.receive == :aborted
+}
+
+## Ractor#join
+
+# Ractor#join returns self when the Ractor is terminated.
+assert_equal 'true', %q{
+ r = Ractor.new do
+ Ractor.receive
+ end
+
+ r << :ok
+ r.join
+ r.inspect in /terminated/
+} if false # TODO
+
+# Ractor#join raises RemoteError when the remote Ractor aborted with an exception
+assert_equal 'err', %q{
+ r = Ractor.new do
+ raise 'err'
+ end
+
+ begin
+ r.join
+ rescue Ractor::RemoteError => e
+ e.cause.message
+ end
+}
+
+## Ractor#value
+
+# Ractor#value returns the last expression even if it is unshareable
+assert_equal 'true', %q{
+ r = Ractor.new do
+ obj = [1, 2]
+ obj << obj.object_id
+ end
+
+ ret = r.value
+ ret == [1, 2, ret.object_id]
+}
+
+# Only one Ractor can call Ractor#value
+assert_equal '[["Only the successor ractor can take a value", 9], ["ok", 2]]', %q{
+ r = Ractor.new do
+ 'ok'
+ end
+
+ RN = 10
+
+ rs = RN.times.map do
+ Ractor.new r do |r|
+ begin
+ Ractor.main << r.value
+ Ractor.main << r.value # this ractor can get same result
+ rescue Ractor::Error => e
+ Ractor.main << e.message
+ end
+ end
+ end
+
+ (RN+1).times.map{
+ Ractor.receive
+ }.tally.sort
+}
+
+# Cause lots of inline CC misses.
+assert_equal 'ok', <<~'RUBY'
+ class A; def test; 1 + 1; end; end
+ class B; def test; 1 + 1; end; end
+ class C; def test; 1 + 1; end; end
+ class D; def test; 1 + 1; end; end
+ class E; def test; 1 + 1; end; end
+ class F; def test; 1 + 1; end; end
+ class G; def test; 1 + 1; end; end
+
+ objs = [A.new, B.new, C.new, D.new, E.new, F.new, G.new].freeze
+
+ def call_test(obj)
+ obj.test
+ end
+
+ ractors = 7.times.map do
+ Ractor.new(objs) do |objs|
+ objs = objs.shuffle
+ 100_000.times do
+ objs.each do |o|
+ call_test(o)
+ end
+ end
+ end
+ end
+ ractors.each(&:join)
+ :ok
+RUBY
+
+# This test checks that we do not trigger a GC when we have malloc with Ractor
+# locks. We cannot trigger a GC with Ractor locks because GC requires VM lock
+# and Ractor barrier. If another Ractor is waiting on this Ractor lock, then it
+# will deadlock because the other Ractor will never join the barrier.
+#
+# Creating Ractor::Port requires locking the Ractor and inserting into an
+# st_table, which can call malloc.
+assert_equal 'ok', <<~'RUBY'
+ r = Ractor.new do
+ loop do
+ Ractor::Port.new
+ end
+ end
+
+ 10.times do
+ 10_000.times do
+ r.send(nil)
+ end
+ sleep(0.01)
+ end
+ :ok
+RUBY
+
+assert_equal 'ok', <<~'RUBY'
+ begin
+ 100.times do |i|
+ Ractor.new(i) do |j|
+ 1000.times do |i|
+ "#{j}-#{i}"
+ end
+ Ractor.receive
+ end
+ pid = fork { }
+ _, status = Process.waitpid2 pid
+ raise unless status.success?
+ end
+
+ :ok
+ rescue NotImplementedError
+ :ok
+ end
+RUBY
+
+assert_equal 'ok', <<~'RUBY'
+ begin
+ 100.times do |i|
+ Ractor.new(i) do |j|
+ 1000.times do |i|
+ "#{j}-#{i}"
+ end
+ end
+ pid = fork do
+ GC.verify_internal_consistency
+ end
+ _, status = Process.waitpid2 pid
+ raise unless status.success?
+ end
+
+ :ok
+ rescue NotImplementedError
+ :ok
+ end
+RUBY
+
+# When creating bmethods in Ractors, they should only be usable from their
+# defining ractor, even if it is GC'd
+assert_equal 'ok', <<~'RUBY'
+
+begin
+ CLASSES = 1000.times.map { Class.new }.freeze
+
+ # This would be better to run in parallel, but there's a bug with lambda
+ # creation and YJIT causing crashes in dev mode
+ ractors = CLASSES.map do |klass|
+ Ractor.new(klass) do |klass|
+ Ractor.receive
+ klass.define_method(:foo) {}
+ end
+ end
+
+ ractors.each do |ractor|
+ ractor << nil
+ ractor.join
+ end
+
+ ractors.clear
+ GC.start
+
+ any = 1000.times.map do
+ Ractor.new do
+ CLASSES.any? do |klass|
+ begin
+ klass.new.foo
+ true
+ rescue RuntimeError
+ false
+ end
+ end
+ end
+ end.map(&:value).none? && :ok
+rescue ThreadError => e
+ # ignore limited memory machine
+ if /can\'t create Thread/ =~ e.message
+ :ok
+ else
+ raise
+ end
+end
+RUBY
+
+# Concurrent super calls with keyword arguments must not race on the
+# callinfo kwarg reference count. [Bug #22075]
+assert_equal 'ok', %q{
+ class Base
+ def foo(a:, b:, c:) = a
+ end
+
+ class Sub < Base
+ def foo(a:, b:, c:) = super(a: a, b: b, c: c)
+ end
+
+ 4.times.map do
+ Ractor.new do
+ obj = Sub.new
+ 100_000.times { obj.foo(a: 1, b: 2, c: 3) }
+ end
+ end.each(&:join)
+
+ :ok
+}
diff --git a/bootstraptest/test_rjit.rb b/bootstraptest/test_rjit.rb
deleted file mode 100644
index 4c8cd26dff..0000000000
--- a/bootstraptest/test_rjit.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# VM_CALL_OPT_SEND + VM_METHOD_TYPE_ATTRSET
-assert_equal '1', %q{
- class Foo
- attr_writer :foo
-
- def bar
- send(:foo=, 1)
- end
- end
-
- Foo.new.bar
-}
-
-# VM_CALL_OPT_SEND + OPTIMIZED_METHOD_TYPE_CALL
-assert_equal 'foo', %q{
- def bar(&foo)
- foo.send(:call)
- end
-
- bar { :foo }
-}
-
-# VM_CALL_OPT_SEND + OPTIMIZED_METHOD_TYPE_STRUCT_AREF
-assert_equal 'bar', %q{
- def bar(foo)
- foo.send(:bar)
- end
-
- bar(Struct.new(:bar).new(:bar))
-}
-
-# AND with offset DISP32
-assert_equal '2', %q{
- def foo
- a = 6;
- b = {a: 1, b: 1, c: 1, d: 1, e: 1, f: 1, g: 1, h: a&3}
- b[:h]
- end
-
- foo
-}
-
-# OR with offset DISP32
-assert_equal '6', %q{
- def foo
- a = 4;
- b = {a: 1, b: 1, c: 1, d: 1, e: 1, f: 1, g: 1, h: a|2}
- b[:h]
- end
-
- foo
-}
-
-# kwargs default w/ checkkeyword + locals (which shouldn't overwrite unspecified_bits)
-assert_equal '1', %q{
- def foo(bar: 1.to_s)
- _ = 1
- bar
- end
-
- def entry
- foo
- end
-
- entry
-}
-
-# Updating local type in Context
-assert_normal_exit %q{
- def foo(flag, object)
- klass = if flag
- object
- end
- klass ||= object
- return klass.new
- end
-
- foo(false, Object)
- foo(true, Object)
-}
diff --git a/bootstraptest/test_syntax.rb b/bootstraptest/test_syntax.rb
index 8301b344c6..29bf93cb8f 100644
--- a/bootstraptest/test_syntax.rb
+++ b/bootstraptest/test_syntax.rb
@@ -571,7 +571,7 @@ assert_equal 'ok', %q{
assert_equal 'ok', %q{
1.times{
- p(1, (next; 2))
+ p(1, (next if true; 2))
}; :ok
}
assert_equal '3', %q{
@@ -585,7 +585,7 @@ assert_equal '3', %q{
i = 0
1 + (while true
break 2 if (i+=1) > 1
- p(1, (next; 2))
+ p(1, (next if true; 2))
end)
}
# redo
@@ -594,7 +594,7 @@ assert_equal 'ok', %q{
1.times{
break if i>1
i+=1
- p(1, (redo; 2))
+ p(1, (redo if true; 2))
}; :ok
}
assert_equal '3', %q{
@@ -608,7 +608,7 @@ assert_equal '3', %q{
i = 0
1 + (while true
break 2 if (i+=1) > 1
- p(1, (redo; 2))
+ p(1, (redo if true; 2))
end)
}
assert_equal '1', %q{
@@ -848,7 +848,7 @@ assert_normal_exit %q{
def x(a=1, b, *rest); nil end
end
end
-}, bug2415 unless rjit_enabled? # flaky
+}, bug2415
assert_normal_exit %q{
0.times do
@@ -880,7 +880,7 @@ assert_normal_exit %q{
end
end
end
-}, bug2415 unless rjit_enabled? # flaky
+}, bug2415
assert_normal_exit %q{
a {
diff --git a/bootstraptest/test_thread.rb b/bootstraptest/test_thread.rb
index 4040b68a27..7ff5bb4a38 100644
--- a/bootstraptest/test_thread.rb
+++ b/bootstraptest/test_thread.rb
@@ -301,7 +301,7 @@ assert_normal_exit %q{
}.each {|t|
t.join
}
-} unless rjit_enabled? # flaky
+}
assert_equal 'ok', %q{
def m
@@ -493,7 +493,8 @@ assert_equal 'foo', %q{
[th1, th2].each {|t| t.join }
GC.start
f.call.source
-} unless rjit_enabled? # flaky
+}
+
assert_normal_exit %q{
class C
def inspect
diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb
index eccaa542c8..e9ce905e2c 100644
--- a/bootstraptest/test_yjit.rb
+++ b/bootstraptest/test_yjit.rb
@@ -37,27 +37,35 @@ assert_equal "ok", %q{
}
# test discarding extra yield arguments
-assert_equal "2210150001501015", %q{
+assert_equal "22131300500015901015", %q{
def splat_kw(ary) = yield *ary, a: 1
def splat(ary) = yield *ary
- def kw = yield 1, 2, a: 0
+ def kw = yield 1, 2, a: 3
+
+ def kw_only = yield a: 0
def simple = yield 0, 1
+ def none = yield
+
def calls
[
splat([1, 1, 2]) { |x, y| x + y },
splat([1, 1, 2]) { |y, opt = raise| opt + y},
splat_kw([0, 1]) { |a:| a },
kw { |a:| a },
- kw { |a| a },
+ kw { |one| one },
+ kw { |one, a:| a },
+ kw_only { |a:| a },
+ kw_only { |a: 1| a },
simple { 5.itself },
simple { |a| a },
simple { |opt = raise| opt },
simple { |*rest| rest },
simple { |opt_kw: 5| opt_kw },
+ none { |a: 9| a },
# autosplat ineractions
[0, 1, 2].yield_self { |a, b| [a, b] },
[0, 1, 2].yield_self { |a, opt = raise| [a, opt] },
@@ -106,7 +114,7 @@ assert_equal '[:ae, :ae]', %q{
end
[test(Array.new 5), test([])]
-} unless rjit_enabled? # Not yet working on RJIT
+}
# regression test for arity check with splat and send
assert_equal '[:ae, :ae]', %q{
@@ -123,7 +131,7 @@ assert_equal '[:ae, :ae]', %q{
# regression test for GC marking stubs in invalidated code
assert_normal_exit %q{
- skip true unless defined?(GC.compact)
+ skip true unless GC.respond_to?(:compact)
garbage = Array.new(10_000) { [] } # create garbage to cause iseq movement
eval(<<~RUBY)
def foo(n, garbage)
@@ -158,7 +166,7 @@ assert_equal '0', "0.abs(&nil)"
# regression test for invokeblock iseq guard
assert_equal 'ok', %q{
- skip :ok unless defined?(GC.compact)
+ skip :ok unless GC.respond_to?(:compact)
def foo = yield
10.times do |i|
ret = eval("foo { #{i} }")
@@ -166,7 +174,7 @@ assert_equal 'ok', %q{
GC.compact
end
:ok
-} unless rjit_enabled? # Not yet working on RJIT
+}
# regression test for overly generous guard elision
assert_equal '[0, :sum, 0, :sum]', %q{
@@ -212,6 +220,15 @@ assert_equal 'Sub', %q{
call(Sub.new('o')).class
}
+# String#dup with generic ivars
+assert_equal '["str", "ivar"]', %q{
+ def str_dup(str) = str.dup
+ str = "str"
+ str.instance_variable_set(:@ivar, "ivar")
+ str = str_dup(str)
+ [str, str.instance_variable_get(:@ivar)]
+}
+
# test splat filling required and feeding rest
assert_equal '[0, 1, 2, [3, 4]]', %q{
public def lead_rest(a, b, *rest)
@@ -294,7 +311,7 @@ assert_equal '[:ok]', %q{
# Used to crash due to GC run in rb_ensure_iv_list_size()
# not marking the newly allocated [:ok].
RegressionTest.new.extender.itself
-} unless rjit_enabled? # Skip on RJIT since this uncovers a crash
+}
assert_equal 'true', %q{
# regression test for tracking type of locals for too long
@@ -451,91 +468,6 @@ assert_normal_exit %q{
end
}
-assert_equal '0', %q{
- # This is a regression test for incomplete invalidation from
- # opt_setinlinecache. This test might be brittle, so
- # feel free to remove it in the future if it's too annoying.
- # This test assumes --yjit-call-threshold=2.
- module M
- Foo = 1
- def foo
- Foo
- end
-
- def pin_self_type_then_foo
- _ = @foo
- foo
- end
-
- def only_ints
- 1 + self
- foo
- end
- end
-
- class Integer
- include M
- end
-
- class Sub
- include M
- end
-
- foo_method = M.instance_method(:foo)
-
- dbg = ->(message) do
- return # comment this out to get printouts
-
- $stderr.puts RubyVM::YJIT.disasm(foo_method)
- $stderr.puts message
- end
-
- 2.times { 42.only_ints }
-
- dbg["There should be two versions of getinlineache"]
-
- module M
- remove_const(:Foo)
- end
-
- dbg["There should be no getinlinecaches"]
-
- 2.times do
- 42.only_ints
- rescue NameError => err
- _ = "caught name error #{err}"
- end
-
- dbg["There should be one version of getinlineache"]
-
- 2.times do
- Sub.new.pin_self_type_then_foo
- rescue NameError
- _ = 'second specialization'
- end
-
- dbg["There should be two versions of getinlineache"]
-
- module M
- Foo = 1
- end
-
- dbg["There should still be two versions of getinlineache"]
-
- 42.only_ints
-
- dbg["There should be no getinlinecaches"]
-
- # Find name of the first VM instruction in M#foo.
- insns = RubyVM::InstructionSequence.of(foo_method).to_a
- if defined?(RubyVM::YJIT.blocks_for) && (insns.last.find { Array === _1 }&.first == :opt_getinlinecache)
- RubyVM::YJIT.blocks_for(RubyVM::InstructionSequence.of(foo_method))
- .filter { _1.iseq_start_index == 0 }.count
- else
- 0 # skip the test
- end
-}
-
# Check that frozen objects are respected
assert_equal 'great', %q{
class Foo
@@ -1230,7 +1162,7 @@ assert_equal 'special', %q{
# Test that object references in generated code get marked and moved
assert_equal "good", %q{
- skip :good unless defined?(GC.compact)
+ skip :good unless GC.respond_to?(:compact)
def bar
"good"
end
@@ -1398,7 +1330,7 @@ assert_equal '[42, :default]', %q{
]
}
-# Test default value block for Hash with opt_aref_with
+# Test default value block for Hash
assert_equal "false", <<~RUBY, frozen_string_literal: false
def index_with_string(h)
h["foo"]
@@ -1798,7 +1730,7 @@ assert_equal '{}', %q{
}
# test building hash with values
-assert_equal '{:foo=>:bar}', %q{
+assert_equal '{foo: :bar}', %q{
def build_hash(val)
{ foo: val }
end
@@ -2048,7 +1980,7 @@ assert_equal '[97, :nil, 97, :nil, :raised]', %q{
getbyte("a", 0)
[getbyte("a", 0), getbyte("a", 1), getbyte("a", -1), getbyte("a", -2), getbyte("a", "a")]
-} unless rjit_enabled? # Not yet working on RJIT
+}
# Basic test for String#setbyte
assert_equal 'AoZ', %q{
@@ -2256,6 +2188,34 @@ assert_equal '7', %q{
foo(5,2)
}
+# regression test for argument registers with invalidation
+assert_equal '[0, 1, 2]', %q{
+ def test(n)
+ ret = n
+ binding
+ ret
+ end
+
+ [0, 1, 2].map do |n|
+ test(n)
+ end
+}
+
+# regression test for argument registers
+assert_equal 'true', %q{
+ class Foo
+ def ==(other)
+ other == nil
+ end
+ end
+
+ def test
+ [Foo.new].include?(Foo.new)
+ end
+
+ test
+}
+
# test pattern matching
assert_equal '[:ok, :ok]', %q{
class C
@@ -2323,7 +2283,7 @@ assert_equal '123', %q{
# Test EP == BP invalidation with moving ISEQs
assert_equal 'ok', %q{
- skip :ok unless defined?(GC.compact)
+ skip :ok unless GC.respond_to?(:compact)
def entry
ok = proc { :ok } # set #entry as an EP-escaping ISEQ
[nil].reverse_each do # avoid exiting the JIT frame on the constant
@@ -2523,6 +2483,32 @@ assert_equal '[0, 2]', %q{
B.new.foo
}
+# invokesuper in a weird block
+assert_equal '["block->A#itself", "block->singleton#itself"]', %q{
+ # This test runs the same block as first as a block and then as a method,
+ # testing the routine that finds the currently running method, which is
+ # relevant for `super`.
+ class BlockIseqDuality
+ prepend(Module.new do
+ def itself
+ nested = -> { "block->" + super() }
+ @singleton_itself.define_singleton_method(:itself, &nested)
+ nested
+ end
+ end)
+
+ attr_reader :singleton_itself
+ def initialize = (@singleton_itself = "singleton#itself")
+
+ def itself = "A#itself"
+ end
+
+ tester = BlockIseqDuality.new
+ super_lambda = tester.itself
+ super_lambda.call # warmup
+ [super_lambda.call, tester.singleton_itself.itself]
+}
+
# invokesuper zsuper in a bmethod
assert_equal 'ok', %q{
class Foo
@@ -2718,7 +2704,23 @@ assert_equal '[1, 2]', %q{
expandarray_redefined_nilclass
expandarray_redefined_nilclass
-} unless rjit_enabled?
+}
+
+assert_equal 'not_array', %q{
+ def expandarray_not_array(obj)
+ a, = obj
+ a
+ end
+
+ obj = Object.new
+ def obj.method_missing(m, *args, &block)
+ return [:not_array] if m == :to_ary
+ super
+ end
+
+ expandarray_not_array(obj)
+ expandarray_not_array(obj)
+}
assert_equal '[1, 2, nil]', %q{
def expandarray_rhs_too_small
@@ -2830,7 +2832,7 @@ assert_equal '[[:c_return, :String, :string_alias, "events_to_str"]]', %q{
events.compiled(events)
events
-} unless rjit_enabled? # RJIT calls extra Ruby methods
+}
# test enabling a TracePoint that targets a particular line in a C method call
assert_equal '[true]', %q{
@@ -2912,7 +2914,7 @@ assert_equal '[[:c_call, :itself]]', %q{
tp.enable { shouldnt_compile }
events
-} unless rjit_enabled? # RJIT calls extra Ruby methods
+}
# test enabling c_return tracing before compiling
assert_equal '[[:c_return, :itself, main]]', %q{
@@ -2927,7 +2929,7 @@ assert_equal '[[:c_return, :itself, main]]', %q{
tp.enable { shouldnt_compile }
events
-} unless rjit_enabled? # RJIT calls extra Ruby methods
+}
# test c_call invalidation
assert_equal '[[:c_call, :itself]]', %q{
@@ -2973,15 +2975,16 @@ assert_equal '[:itself]', %q{
itself
end
- tracing_ractor = Ractor.new do
+ port = Ractor::Port.new
+ tracing_ractor = Ractor.new port do |port|
# 1: start tracing
events = []
tp = TracePoint.new(:c_call) { events << _1.method_id }
tp.enable
- Ractor.yield(nil)
+ port << nil
# 3: run compiled method on tracing ractor
- Ractor.yield(nil)
+ port << nil
traced_method
events
@@ -2989,13 +2992,13 @@ assert_equal '[:itself]', %q{
tp&.disable
end
- tracing_ractor.take
+ port.receive
# 2: compile on non tracing ractor
traced_method
- tracing_ractor.take
- tracing_ractor.take
+ port.receive
+ tracing_ractor.value
}
# Try to hit a lazy branch stub while another ractor enables tracing
@@ -3009,17 +3012,18 @@ assert_equal '42', %q{
end
end
- ractor = Ractor.new do
+ port = Ractor::Port.new
+ ractor = Ractor.new port do |port|
compiled(false)
- Ractor.yield(nil)
+ port << nil
compiled(41)
end
tp = TracePoint.new(:line) { itself }
- ractor.take
+ port.receive
tp.enable
- ractor.take
+ ractor.value
}
# Test equality with changing types
@@ -3095,7 +3099,7 @@ assert_equal '42', %q{
A.foo
A.foo
- Ractor.new { A.foo }.take
+ Ractor.new { A.foo }.value
}
assert_equal '["plain", "special", "sub", "plain"]', %q{
@@ -3347,7 +3351,7 @@ assert_equal '[[1, 2, 3, 4]]', %q{
}
# cfunc kwargs
-assert_equal '{:foo=>123}', %q{
+assert_equal '{foo: 123}', %q{
def foo(bar)
bar.store(:value, foo: 123)
bar[:value]
@@ -3358,7 +3362,7 @@ assert_equal '{:foo=>123}', %q{
}
# cfunc kwargs
-assert_equal '{:foo=>123}', %q{
+assert_equal '{foo: 123}', %q{
def foo(bar)
bar.replace(foo: 123)
end
@@ -3368,7 +3372,7 @@ assert_equal '{:foo=>123}', %q{
}
# cfunc kwargs
-assert_equal '{:foo=>123, :bar=>456}', %q{
+assert_equal '{foo: 123, bar: 456}', %q{
def foo(bar)
bar.replace(foo: 123, bar: 456)
end
@@ -3378,7 +3382,7 @@ assert_equal '{:foo=>123, :bar=>456}', %q{
}
# variadic cfunc kwargs
-assert_equal '{:foo=>123}', %q{
+assert_equal '{foo: 123}', %q{
def foo(bar)
bar.merge(foo: 123)
end
@@ -3502,7 +3506,7 @@ assert_equal "true", %q{
}
# duphash
-assert_equal '{:foo=>123}', %q{
+assert_equal '{foo: 123}', %q{
def foo
{foo: 123}
end
@@ -3512,7 +3516,7 @@ assert_equal '{:foo=>123}', %q{
}
# newhash
-assert_equal '{:foo=>2}', %q{
+assert_equal '{foo: 2}', %q{
def foo
{foo: 1+1}
end
@@ -3622,6 +3626,74 @@ assert_equal 'new', %q{
test
}
+# Bug #21257 (infinite jmp)
+assert_equal 'ok', %q{
+ Good = :ok
+
+ def first
+ second
+ end
+
+ def second
+ ::Good
+ end
+
+ # Make `second` side exit on its first instruction
+ trace = TracePoint.new(:line) { }
+ trace.enable(target: method(:second))
+
+ first
+ # Recompile now that the constant cache is populated, so we get a fallthrough from `first` to `second`
+ # (this is need to reproduce with --yjit-call-threshold=1)
+ RubyVM::YJIT.code_gc if defined?(RubyVM::YJIT)
+ first
+
+ # Trigger a constant cache miss in rb_vm_opt_getconstant_path (in `second`) next time it's called
+ module InvalidateConstantCache
+ Good = nil
+ end
+
+ RubyVM::YJIT.simulate_oom! if defined?(RubyVM::YJIT)
+
+ first
+ first
+}
+
+assert_equal 'ok', %q{
+ # Multiple incoming branches into second
+ Good = :ok
+
+ def incoming_one
+ second
+ end
+
+ def incoming_two
+ second
+ end
+
+ def second
+ ::Good
+ end
+
+ # Make `second` side exit on its first instruction
+ trace = TracePoint.new(:line) { }
+ trace.enable(target: method(:second))
+
+ incoming_one
+ # Recompile now that the constant cache is populated, so we get a fallthrough from `incoming_one` to `second`
+ # (this is need to reproduce with --yjit-call-threshold=1)
+ RubyVM::YJIT.code_gc if defined?(RubyVM::YJIT)
+ incoming_one
+ incoming_two
+
+ # Trigger a constant cache miss in rb_vm_opt_getconstant_path (in `second`) next time it's called
+ module InvalidateConstantCache
+ Good = nil
+ end
+
+ incoming_one
+}
+
assert_equal 'ok', %q{
# Try to compile new method while OOM
def foo
@@ -3746,36 +3818,6 @@ assert_equal '3,12', %q{
pt_inspect(p)
}
-# Regression test for deadlock between branch_stub_hit and ractor_receive_if
-assert_equal '10', %q{
- r = Ractor.new Ractor.current do |main|
- main << 1
- main << 2
- main << 3
- main << 4
- main << 5
- main << 6
- main << 7
- main << 8
- main << 9
- main << 10
- end
-
- a = []
- a << Ractor.receive_if{|msg| msg == 10}
- a << Ractor.receive_if{|msg| msg == 9}
- a << Ractor.receive_if{|msg| msg == 8}
- a << Ractor.receive_if{|msg| msg == 7}
- a << Ractor.receive_if{|msg| msg == 6}
- a << Ractor.receive_if{|msg| msg == 5}
- a << Ractor.receive_if{|msg| msg == 4}
- a << Ractor.receive_if{|msg| msg == 3}
- a << Ractor.receive_if{|msg| msg == 2}
- a << Ractor.receive_if{|msg| msg == 1}
-
- a.length
-}
-
# checktype
assert_equal 'false', %q{
def function()
@@ -4081,6 +4123,26 @@ assert_equal '1', %q{
bar { }
}
+# unshareable bmethod call through Method#to_proc#call
+assert_equal '1000', %q{
+ define_method(:bmethod) do
+ self
+ end
+
+ Ractor.new do
+ errors = 0
+ 1000.times do
+ p = method(:bmethod).to_proc
+ begin
+ p.call
+ rescue RuntimeError
+ errors += 1
+ end
+ end
+ errors
+ end.value
+}
+
# test for return stub lifetime issue
assert_equal '1', %q{
def foo(n)
@@ -4321,7 +4383,7 @@ assert_equal "ArgumentError", %q{
# Rest with block
# Simplified code from railsbench
-assert_equal '[{"/a"=>"b", :as=>:c, :via=>:post}, [], nil]', %q{
+assert_equal '[{"/a" => "b", as: :c, via: :post}, [], nil]', %q{
def match(path, *rest, &block)
[path, rest, block]
end
@@ -4449,7 +4511,7 @@ assert_equal 'true', %q{
rescue ArgumentError
true
end
-} unless rjit_enabled? # Not yet working on RJIT
+}
# Regression test: register allocator on expandarray
assert_equal '[]', %q{
@@ -4566,6 +4628,11 @@ assert_equal '[2, 4611686018427387904]', %q{
[1.succ, 4611686018427387903.succ]
}
+# Integer pred and overflow
+assert_equal '[0, -4611686018427387905]', %q{
+ [1.pred, -4611686018427387904.pred]
+}
+
# Integer right shift
assert_equal '[0, 1, -4]', %q{
[0 >> 1, 2 >> 1, -7 >> 1]
@@ -4582,7 +4649,7 @@ assert_equal '[nil, "yield"]', %q{
}
# splat with ruby2_keywords into rest parameter
-assert_equal '[[{:a=>1}], {}]', %q{
+assert_equal '[[{a: 1}], {}]', %q{
ruby2_keywords def foo(*args) = args
def bar(*args, **kw) = [args, kw]
@@ -4623,7 +4690,7 @@ assert_normal_exit %q{
}
# a kwrest case
-assert_equal '[1, 2, {:complete=>false}]', %q{
+assert_equal '[1, 2, {complete: false}]', %q{
def rest(foo: 1, bar: 2, **kwrest)
[foo, bar, kwrest]
end
@@ -4682,7 +4749,7 @@ assert_equal '[[1, 2, 3], [0, 2, 3], [1, 2, 3], [2, 2, 3], [], [], [{}]]', %q{
}
# Class#new (arity=-1), splat, and ruby2_keywords
-assert_equal '[0, {1=>1}]', %q{
+assert_equal '[0, {1 => 1}]', %q{
class KwInit
attr_reader :init_args
def initialize(x = 0, **kw)
@@ -4698,7 +4765,7 @@ assert_equal '[0, {1=>1}]', %q{
}
# Chilled string setivar trigger warning
-assert_equal 'literal string will be frozen in the future', %q{
+assert_match(/literal string will be frozen in the future/, %q{
Warning[:deprecated] = true
$VERBOSE = true
$warning = "no-warning"
@@ -4726,7 +4793,7 @@ assert_equal 'literal string will be frozen in the future', %q{
setivar!("chilled") # Emit warning
$warning
-}
+})
# arity=-2 cfuncs
assert_equal '["", "1/2", [0, [:ok, 1]]]', %q{
@@ -4848,6 +4915,16 @@ assert_equal '[:ok, :ok, :ok, :ok, :ok]', %q{
tests
}
+# regression test for splat with &proc{} when the target has rest (Bug #21266)
+assert_equal '[]', %q{
+ def foo(args) = bar(*args, &proc { _1 })
+ def bar(_, _, _, _, *rest) = yield rest
+
+ GC.stress = true
+ foo([1,2,3,4])
+ foo([1,2,3,4])
+}
+
# regression test for invalidating an empty block
assert_equal '0', %q{
def foo = (* = 1).pred
@@ -4859,7 +4936,7 @@ assert_equal '0', %q{
end
foo # try again
-} unless rjit_enabled? # doesn't work on RJIT
+}
# test integer left shift with constant rhs
assert_equal [0x80000000000, 'a+', :ok].inspect, %q{
@@ -4967,7 +5044,7 @@ assert_equal '[[true, false, false], [true, true, false], [true, :error, :error]
end
results << test
-} unless rjit_enabled? # Not yet working on RJIT
+}
# test FalseClass#=== before and after redefining FalseClass#==
assert_equal '[[true, false, false], [true, false, true], [true, :error, :error]]', %q{
@@ -5002,7 +5079,7 @@ assert_equal '[[true, false, false], [true, false, true], [true, :error, :error]
end
results << test
-} unless rjit_enabled? # Not yet working on RJIT
+}
# test NilClass#=== before and after redefining NilClass#==
assert_equal '[[true, false, false], [true, false, true], [true, :error, :error]]', %q{
@@ -5037,7 +5114,7 @@ assert_equal '[[true, false, false], [true, false, true], [true, :error, :error]
end
results << test
-} unless rjit_enabled? # Not yet working on RJIT
+}
# test struct accessors fire c_call events
assert_equal '[[:c_call, :x=], [:c_call, :x]]', %q{
@@ -5186,6 +5263,7 @@ end
test
RUBY
+# opt_newarray_send pack/buffer
assert_equal '[true, true]', <<~'RUBY'
def pack
v = 1.23
@@ -5201,3 +5279,279 @@ assert_equal '[true, true]', <<~'RUBY'
[pack, with_buffer]
RUBY
+
+# String#[] / String#slice
+assert_equal 'ok', <<~'RUBY'
+ def error(klass)
+ yield
+ rescue klass
+ true
+ end
+
+ def test
+ str = "こんにちは"
+ substr = "にち"
+ failures = []
+
+ # Use many small statements to keep context for each slice call smaller than MAX_CTX_TEMPS
+
+ str[1] == "ん" && str.slice(4) == "は" || failures << :index
+ str[5].nil? && str.slice(5).nil? || failures << :index_end
+
+ str[1, 2] == "んに" && str.slice(2, 1) == "に" || failures << :beg_len
+ str[5, 1] == "" && str.slice(5, 1) == "" || failures << :beg_len_end
+
+ str[1..2] == "んに" && str.slice(2..2) == "に" || failures << :range
+
+ str[/に./] == "にち" && str.slice(/に./) == "にち" || failures << :regexp
+
+ str[/に./, 0] == "にち" && str.slice(/に./, 0) == "にち" || failures << :regexp_cap0
+
+ str[/に(.)/, 1] == "ち" && str.slice(/に(.)/, 1) == "ち" || failures << :regexp_cap1
+
+ str[substr] == substr && str.slice(substr) == substr || failures << :substr
+
+ error(TypeError) { str[Object.new] } && error(TypeError) { str.slice(Object.new, 1) } || failures << :type_error
+ error(RangeError) { str[Float::INFINITY] } && error(RangeError) { str.slice(Float::INFINITY) } || failures << :range_error
+
+ return "ok" if failures.empty?
+ {failures: failures}
+ end
+
+ test
+RUBY
+
+# opt_duparray_send :include?
+assert_equal '[true, false]', <<~'RUBY'
+ def test(x)
+ [:a, :b].include?(x)
+ end
+
+ [
+ test(:b),
+ test(:c),
+ ]
+RUBY
+
+# opt_newarray_send :include?
+assert_equal '[true, false]', <<~'RUBY'
+ def test(x)
+ [Object.new, :a, :b].include?(x.to_sym)
+ end
+
+ [
+ test("b"),
+ test("c"),
+ ]
+RUBY
+
+# YARV: swap and opt_reverse
+assert_equal '["x", "Y", "c", "A", "t", "A", "b", "C", "d"]', <<~'RUBY'
+ class Swap
+ def initialize(s)
+ @a, @b, @c, @d = s.split("")
+ end
+
+ def swap
+ a, b = @a, @b
+ b = b.upcase
+ @a, @b = a, b
+ end
+
+ def reverse_odd
+ a, b, c = @a, @b, @c
+ b = b.upcase
+ @a, @b, @c = a, b, c
+ end
+
+ def reverse_even
+ a, b, c, d = @a, @b, @c, @d
+ a = a.upcase
+ c = c.upcase
+ @a, @b, @c, @d = a, b, c, d
+ end
+ end
+
+ Swap.new("xy").swap + Swap.new("cat").reverse_odd + Swap.new("abcd").reverse_even
+RUBY
+
+assert_normal_exit %{
+ class Bug20997
+ def foo(&) = self.class.name(&)
+
+ new.foo
+ end
+}
+
+# This used to trigger a "try to mark T_NONE"
+# due to an uninitialized local in foo.
+assert_normal_exit %{
+ def foo(...)
+ _local_that_should_nil_on_call = GC.start
+ end
+
+ def test_bug21021
+ puts [], [], [], [], [], []
+ foo []
+ end
+
+ GC.stress = true
+ test_bug21021
+}
+
+assert_equal 'nil', %{
+ def foo(...)
+ _a = _b = _c = binding.local_variable_get(:_c)
+
+ _c
+ end
+
+ # [Bug #21021]
+ def test_local_fill_in_forwardable
+ puts [], [], [], [], []
+ foo []
+ end
+
+ test_local_fill_in_forwardable.inspect
+}
+
+# Test defined?(yield) and block_given? in non-method context.
+# It's good that the body of this runs at true top level and isn't wrapped in a block.
+assert_equal 'false', %{
+ RESULT = []
+ RESULT << defined?(yield)
+ RESULT << block_given?
+
+ 1.times do
+ RESULT << defined?(yield)
+ RESULT << block_given?
+ end
+
+ module ModuleContext
+ 1.times do
+ RESULT << defined?(yield)
+ RESULT << block_given?
+ end
+ end
+
+ class << self
+ RESULT << defined?(yield)
+ RESULT << block_given?
+ end
+
+ RESULT.any?
+}
+
+# throw and String#dup with GC stress
+assert_equal 'foo', %{
+ GC.stress = true
+
+ def foo
+ 1.times { return "foo".dup }
+ end
+
+ 10.times.map { foo.dup }.last
+}
+
+# regression test for [Bug #21772]
+# local variable type tracking desync
+assert_normal_exit %q{
+ def some_method = 0
+
+ def test_body(key)
+ some_method
+ key = key.to_s # setting of local relevant
+
+ key == "symbol"
+ end
+
+ def jit_caller = test_body("session_id")
+
+ jit_caller # first iteration, non-escaped environment
+ alias some_method binding # induce environment escape
+ test_body(:symbol)
+}
+
+# regression test for missing check in identity method inlining
+assert_normal_exit %q{
+ # Use dead code (if false) to create a local
+ # without initialization instructions.
+ def foo(a)
+ if false
+ x = nil
+ end
+ x
+ end
+ def test = foo(1)
+ test
+ test
+}
+
+# regression test for tracing invalidation with on-stack compiled methods
+# Exercises the on_stack_iseqs path in rb_yjit_tracing_invalidate_all
+# where delayed deallocation must not create aliasing &mut references
+# to IseqPayload (use-after-free of version_map backing storage).
+assert_normal_exit %q{
+ def deep = 42
+ def mid = deep
+ def outer = mid
+
+ # Compile all three methods with YJIT
+ 10.times { outer }
+
+ # Enable tracing from within a call chain so that outer/mid/deep
+ # are on the stack when rb_yjit_tracing_invalidate_all runs.
+ # This triggers the on_stack_iseqs (delayed deallocation) path.
+ def deep
+ TracePoint.new(:line) {}.enable
+ 42
+ end
+
+ outer
+
+ # After invalidation, verify YJIT can recompile and run correctly
+ def deep = 42
+ 10.times { outer }
+}
+
+# regression test for tracing invalidation with on-stack fibers
+# Suspended fibers have iseqs on their stack that must survive invalidation.
+assert_equal '42', %q{
+ def compiled_method
+ Fiber.yield
+ 42
+ end
+
+ # Compile the method
+ 10.times { compiled_method rescue nil }
+
+ fiber = Fiber.new { compiled_method }
+ fiber.resume # suspends inside compiled_method — it's now on the fiber's stack
+
+ # Enable tracing while compiled_method is on the fiber's stack.
+ # This triggers rb_yjit_tracing_invalidate_all with on-stack iseqs.
+ TracePoint.new(:call) {}.enable
+
+ # Resume the fiber — compiled_method's iseq must still be valid
+ fiber.resume.to_s
+}
+
+# regression test for register mapping of methods with over 256 locals
+# [Bug #22074]
+assert_equal "ok", %q{
+ source = +"def many_locals\n"
+ source << " total = 0\n"
+
+ 128.times do |i|
+ source << " y#{i} = 1\n"
+ source << " x#{i} = Object.new\n"
+ end
+
+ source << " total += 1\n"
+ source << " raise total.inspect unless total == 1\n"
+ source << "end\n"
+
+ eval(source)
+ many_locals
+ "ok"
+}
diff --git a/bootstraptest/test_yjit_rust_port.rb b/bootstraptest/test_yjit_rust_port.rb
index e399e0e49e..2dbcebc03a 100644
--- a/bootstraptest/test_yjit_rust_port.rb
+++ b/bootstraptest/test_yjit_rust_port.rb
@@ -374,7 +374,7 @@ assert_equal 'ok', %q{
r = Ractor.new do
'ok'
end
- r.take
+ r.value
}
# Passed arguments to Ractor.new will be a block parameter
@@ -384,7 +384,7 @@ assert_equal 'ok', %q{
r = Ractor.new 'ok' do |msg|
msg
end
- r.take
+ r.value
}
# Pass multiple arguments to Ractor.new
@@ -393,7 +393,7 @@ assert_equal 'ok', %q{
r = Ractor.new 'ping', 'pong' do |msg, msg2|
[msg, msg2]
end
- 'ok' if r.take == ['ping', 'pong']
+ 'ok' if r.value == ['ping', 'pong']
}
# Ractor#send passes an object with copy to a Ractor
@@ -403,7 +403,7 @@ assert_equal 'ok', %q{
msg = Ractor.receive
end
r.send 'ok'
- r.take
+ r.value
}
assert_equal '[1, 2, 3]', %q{