diff options
Diffstat (limited to 'bootstraptest/test_ractor.rb')
| -rw-r--r-- | bootstraptest/test_ractor.rb | 2666 |
1 files changed, 2666 insertions, 0 deletions
diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb new file mode 100644 index 0000000000..4fe90703fc --- /dev/null +++ b/bootstraptest/test_ractor.rb @@ -0,0 +1,2666 @@ +# Ractor.current returns a current ractor +assert_equal 'Ractor', %q{ + Ractor.current.class +} + +# Ractor.new returns new Ractor +assert_equal 'Ractor', %q{ + Ractor.new{}.class +} + +# Ractor.allocate is not supported +assert_equal "[:ok, :ok]", %q{ + rs = [] + begin + Ractor.allocate + rescue => e + rs << :ok if e.message == 'allocator undefined for Ractor' + end + + begin + Ractor.new{}.dup + rescue + rs << :ok if e.message == 'allocator undefined for Ractor' + end + + rs +} + +# A Ractor can have a name +assert_equal 'test-name', %q{ + r = Ractor.new name: 'test-name' do + end + r.name +} + +# If Ractor doesn't have a name, Ractor#name returns nil. +assert_equal 'nil', %q{ + r = Ractor.new do + end + r.name.inspect +} + +# Raises exceptions if initialize with an invalid name +assert_equal 'ok', %q{ + begin + r = Ractor.new(name: [{}]) {} + rescue TypeError => e + 'ok' + end +} + +# Ractor.new must call with a block +assert_equal "must be called with a block", %q{ + begin + Ractor.new + rescue ArgumentError => e + e.message + end +} + +# Ractor#inspect +# Return only id and status for main ractor +assert_equal "#<Ractor:#1 running>", %q{ + Ractor.current.inspect +} + +# Return id, loc, and status for no-name ractor +assert_match /^#<Ractor:#([^ ]*?) .+:[0-9]+ terminated>$/, %q{ + r = Ractor.new { '' } + r.join + sleep 0.1 until r.inspect =~ /terminated/ + r.inspect +} + +# 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.join + sleep 0.1 until r.inspect =~ /terminated/ + r.inspect +} + +# A return value of a Ractor block will be a message from the Ractor. +assert_equal 'ok', %q{ + # join + r = Ractor.new do + 'ok' + end + r.value +} + +# Passed arguments to Ractor.new will be a block parameter +# The values are passed with Ractor-communication pass. +assert_equal 'ok', %q{ + # ping-pong with arg + r = Ractor.new 'ok' do |msg| + msg + end + r.value +} + +# Pass multiple arguments to Ractor.new +assert_equal 'ok', %q{ + # ping-pong with two args + r = Ractor.new 'ping', 'pong' do |msg, msg2| + [msg, msg2] + end + 'ok' if r.value == ['ping', 'pong'] +} + +# Ractor#send passes an object with copy to a Ractor +# and Ractor.receive in the Ractor block can receive the passed value. +assert_equal 'ok', %q{ + r = Ractor.new do + msg = Ractor.receive + end + r.send 'ok' + r.value +} + +# Ractor#receive_if can filter the message +assert_equal '[1, 2, 3]', %q{ + ports = 3.times.map{Ractor::Port.new} + + r = Ractor.new ports do |ports| + ports[0] << 3 + ports[1] << 1 + ports[2] << 2 + end + a = [] + a << ports[1].receive # 1 + a << ports[2].receive # 2 + a << ports[0].receive # 3 + a +} + +# dtoa race condition +assert_equal '[:ok, :ok, :ok]', %q{ + n = 3 + n.times.map{ + Ractor.new{ + 10_000.times{ rand.to_s } + :ok + } + }.map(&:value) +} + +assert_equal "42", %q{ + a = 42 + Ractor.shareable_lambda{ a }.call +} + +# 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 = 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) + + [pr1.call == self, pr2.call == nil] +} + +# 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) + + r = [] + begin + pr2.call + rescue SyntaxError + r << SyntaxError + end + + r << pr1.call << pr1.binding.class +} + +# 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 +} + +### +### +# Ractor still has several memory corruption so skip huge number of tests +if ENV['GITHUB_WORKFLOW'] == 'Compilations' + # ignore the follow +else + +# Ractor.select with a Ractor argument +assert_equal 'ok', %q{ + # select 1 + r1 = Ractor.new{'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 + 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 = [] + port, obj = Ractor.select(*ps) + ps.delete(port) + as << obj + port, obj = Ractor.select(*ps) + as << obj + as.sort #=> ["r1", "r2"] +} + +# Ractor.select from multiple ractors. +assert_equal 30.times.map { 'ok' }.to_s, %q{ + def test n + rs = (1..n).map do |i| + Ractor.new(i) do |i| + "r#{i}" + end + end + as = [] + all_rs = rs.dup + + n.times{ + r, obj = Ractor.select(*rs) + as << [r, obj] + rs.delete(r) + } + + if as.map{|r, o| r.object_id}.sort == all_rs.map{|r| r.object_id}.sort && + as.map{|r, o| o}.sort == (1..n).map{|i| "r#{i}"}.sort + 'ok' + else + 'ng' + end + end + + 30.times.map{|i| + test i + } +} 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::Port or Ractor/, %q{ + begin + Ractor.select + rescue ArgumentError => e + e.message + end +} + +# Raise Ractor::ClosedError when try to send into a terminated ractor +assert_equal 'ok', %q{ + r = Ractor.new do + end + + r.join # closed + sleep 0.1 until r.inspect =~ /terminated/ + + begin + r.send(1) + rescue Ractor::ClosedError + 'ok' + else + 'ng' + end +} + +# Can mix with Thread#interrupt and Ractor#join [Bug #17366] +assert_equal 'err', %q{ + Ractor.new do + t = Thread.current + begin + Thread.new{ t.raise "err" }.join + rescue => e + e.message + end + end.value +} + +# Killed Ractor's thread yields nil +assert_equal 'nil', %q{ + Ractor.new{ + t = Thread.current + Thread.new{ t.kill }.join + }.value.inspect #=> nil +} + +# Raise Ractor::ClosedError when try to send into a ractor with closed default port +assert_equal 'ok', %q{ + r = Ractor.new { + Ractor.current.close + Ractor.main << :ok + Ractor.receive + } + + Ractor.receive # wait for ok + + begin + r.send(1) + rescue Ractor::ClosedError + 'ok' + else + 'ng' + end +} + +# Ractor.main returns main ractor +assert_equal 'true', %q{ + Ractor.new{ + Ractor.main + }.value == Ractor.current +} + +# a ractor with closed outgoing port should terminate +assert_equal 'ok', %q{ + Ractor.new do + Ractor.current.close + end + + true until Ractor.count == 1 + :ok +} + +# 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 + begin + r.join + rescue Ractor::RemoteError => e + [e.cause.class, #=> RuntimeError + e.cause.message, #=> 'ok' + e.ractor == r] #=> true + 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 +} + +# 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 + sleep 0.1 + 'ok' + end + r.value +} + +# 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.value + rescue Ractor::RemoteError => e + [e.cause.class, #=> RuntimeError + e.cause.message, #=> 'ok' + e.ractor == r] #=> true + end +} + +# threads in a ractor will killed +assert_equal '{ok: 3}', %q{ + Ractor.new Ractor.current do |main| + q = Thread::Queue.new + Thread.new do + q << true + loop{} + ensure + main << :ok + end + + Thread.new do + q << true + while true + end + ensure + main << :ok + end + + Thread.new do + q << true + sleep 1 + ensure + main << :ok + end + + # wait for the start of all threads + 3.times{q.pop} + end + + 3.times.map{Ractor.receive}.tally +} unless yjit_enabled? # YJIT: `[BUG] Bus Error at 0x000000010b7002d0` in jit_exec() + +# unshareable object are copied +assert_equal 'false', %q{ + obj = 'str'.dup + r = Ractor.new obj do |msg| + msg.object_id + end + + obj.object_id == r.value +} + +# To copy the object, now Marshal#dump is used +assert_match /can't clone unshareable instance of Thread/, %q{ + obj = Thread.new{} + begin + r = Ractor.new obj do |msg| + msg + end + rescue Ractor::Error => e + e.message + else + 'ng' + end +} + +# send shareable and unshareable objects +assert_equal "ok", <<~'RUBY', frozen_string_literal: false + port = Ractor::Port.new + echo_ractor = Ractor.new port do |port| + loop do + v = Ractor.receive + port << v + end + end + + class C; end + module M; end + S = Struct.new(:a, :b, :c, :d) + + shareable_objects = [ + true, + false, + nil, + 1, + 1.1, # Float + 1+2r, # Rational + 3+4i, # Complex + 2**128, # Bignum + :sym, # Symbol + 'xyzzy'.to_sym, # dynamic symbol + 'frozen'.freeze, # frozen String + /regexp/, # regexp literal + /reg{true}exp/.freeze, # frozen dregexp + [1, 2].freeze, # frozen Array which only refers to shareable + {a: 1}.freeze, # frozen Hash which only refers to shareable + [{a: 1}.freeze, 'str'.freeze].freeze, # nested frozen container + S.new(1, 2).freeze, # frozen Struct + S.new(1, 2, 3, 4).freeze, # frozen Struct + (1..2), # Range on Struct + (1..), # Range on Struct + (..1), # Range on Struct + C, # class + M, # module + Ractor.current, # Ractor + ] + + unshareable_objects = [ + 'mutable str'.dup, + [:array], + {hash: true}, + S.new(1, 2), + S.new(1, 2, 3, 4), + S.new("a", 2).freeze, # frozen, but refers to an unshareable object + ] + + results = [] + + shareable_objects.map{|o| + echo_ractor << o + o2 = port.receive + results << "#{o} is copied" unless o.object_id == o2.object_id + } + + unshareable_objects.map{|o| + echo_ractor << o + o2 = port.receive + results << "#{o.inspect} is not copied" if o.object_id == o2.object_id + } + + if results.empty? + :ok + else + results.inspect + end +RUBY + +# frozen Objects are shareable +assert_equal [false, true, false].inspect, <<~'RUBY', frozen_string_literal: false + class C + def initialize freeze + @a = 1 + @b = :sym + @c = 'frozen_str' + @c.freeze if freeze + @d = true + end + end + + def check obj1 + obj2 = Ractor.new obj1 do |obj| + obj + end.value + + obj1.object_id == obj2.object_id + end + + results = [] + results << check(C.new(true)) # false + results << check(C.new(true).freeze) # true + results << check(C.new(false).freeze) # false +RUBY + +# move example2: String +# touching moved object causes an error +assert_equal 'hello world', <<~'RUBY', frozen_string_literal: false + # move + r = Ractor.new do + obj = Ractor.receive + obj << ' world' + end + + str = 'hello' + r.send str, move: true + modified = r.value + + begin + str << ' exception' # raise Ractor::MovedError + rescue Ractor::MovedError + modified #=> 'hello world' + else + raise 'unreachable' + end +RUBY + +# move example2: Array +assert_equal '[0, 1]', %q{ + r = Ractor.new do + ary = Ractor.receive + ary << 1 + end + + a1 = [0] + r.send a1, move: true + a2 = r.value + begin + a1 << 2 # raise Ractor::MovedError + rescue Ractor::MovedError + a2.inspect + end +} + +# unshareable frozen objects should still be frozen in new ractor after move +assert_equal 'true', %q{ + r = Ractor.new do + obj = receive + { frozen: obj.frozen? } + end + obj = [Object.new].freeze + r.send(obj, move: true) + r.value[:frozen] +} + +# 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.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# 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.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# $stdin,out,err is Ractor local, but shared fds +assert_equal 'ok', %q{ + r = Ractor.new do + [$stdin, $stdout, $stderr].map{|io| + [io.object_id, io.fileno] + } + end + + [$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 + } + 'ok' +} + +# $stdin,out,err belong to Ractor +assert_equal 'ok', %q{ + r = Ractor.new do + $stdin.itself + $stdout.itself + $stderr.itself + 'ok' + end + + r.value +} + +# $DEBUG, $VERBOSE are Ractor local +assert_equal 'true', %q{ + $DEBUG = true + $VERBOSE = true + + def ractor_local_globals + /a(b)(c)d/ =~ 'abcd' # for $~ + `echo foo` unless /solaris/ =~ RUBY_PLATFORM + + { + # ractor-local (derived from created ractor): debug + '$DEBUG' => $DEBUG, + '$-d' => $-d, + + # ractor-local (derived from created ractor): verbose + '$VERBOSE' => $VERBOSE, + '$-w' => $-w, + '$-W' => $-W, + '$-v' => $-v, + + # process-local (readonly): other commandline parameters + '$-p' => $-p, + '$-l' => $-l, + '$-a' => $-a, + + # process-local (readonly): getpid + '$$' => $$, + + # thread local: process result + '$?' => $?, + + # scope local: match + '$~' => $~.inspect, + '$&' => $&, + '$`' => $`, + '$\'' => $', + '$+' => $+, + '$1' => $1, + + # scope local: last line + '$_' => $_, + + # scope local: last backtrace + '$@' => $@, + '$!' => $!, + + # ractor local: stdin, out, err + '$stdin' => $stdin.inspect, + '$stdout' => $stdout.inspect, + '$stderr' => $stderr.inspect, + } + end + + h = Ractor.new do + ractor_local_globals + end.value + ractor_local_globals == h #=> true +} + +# selfs are different objects +assert_equal 'false', %q{ + r = Ractor.new do + self.object_id + end + ret = r.value + ret == self.object_id +} + +# self is a Ractor instance +assert_equal 'true', %q{ + r = Ractor.new do + self.object_id + end + 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 'Ractor::IsolationError', %q{ + begin + a = true + r = Ractor.new do + a + end + rescue => e + e.class + 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 (@iv from C)", <<~'RUBY', frozen_string_literal: false + class C + @iv = 'str' + end + + r = Ractor.new do + class C + p @iv + end + end + + begin + r.value + rescue Ractor::RemoteError => e + e.cause.message + end +RUBY + +# ivar in shareable-objects are not allowed to access from non-main Ractor +assert_equal 'can not access instance variables of shareable objects from non-main Ractors', %q{ + shared = Ractor.new{} + shared.instance_variable_set(:@iv, 'str') + + r = Ractor.new shared do |shared| + p shared.instance_variable_get(:@iv) + end + + begin + r.value + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# ivar in shareable-objects are not allowed to access from non-main Ractor, by @iv (get) +assert_equal 'can not access instance variables of shareable objects from non-main Ractors', %q{ + class Ractor + def setup + @foo = '' + end + + def foo + @foo + end + end + + shared = Ractor.new{} + shared.setup + + r = Ractor.new shared do |shared| + p shared.foo + end + + begin + r.value + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# ivar in shareable-objects are not allowed to access from non-main Ractor, by @iv (set) +assert_equal 'can not access instance variables of shareable objects from non-main Ractors', %q{ + class Ractor + def setup + @foo = '' + end + end + + shared = Ractor.new{} + + r = Ractor.new shared do |shared| + p shared.setup + end + + begin + r.value + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# But a shareable object is frozen, it is allowed to access ivars from non-main Ractor +assert_equal '11', %q{ + [Object.new, [], ].map{|obj| + obj.instance_variable_set('@a', 1) + Ractor.make_shareable obj = obj.freeze + + Ractor.new obj do |obj| + obj.instance_variable_get('@a') + end.value.to_s + }.join +} + +# and instance variables of classes/modules are accessible if they refer shareable objects +assert_equal '333', %q{ + class C + @int = 1 + @str = '-1000'.dup + @fstr = '100'.freeze + + def self.int = @int + def self.str = @str + def self.fstr = @fstr + end + + module M + @int = 2 + @str = '-2000'.dup + @fstr = '200'.freeze + + def self.int = @int + def self.str = @str + def self.fstr = @fstr + end + + a = Ractor.new{ C.int }.value + b = Ractor.new do + C.str.to_i + rescue Ractor::IsolationError + 10 + end.value + c = Ractor.new do + C.fstr.to_i + end.value + + d = Ractor.new{ M.int }.value + e = Ractor.new do + M.str.to_i + rescue Ractor::IsolationError + 20 + end.value + f = Ractor.new do + M.fstr.to_i + end.value + + + # 1 + 10 + 100 + 2 + 20 + 200 + a + b + c + d + e + f +} + +assert_equal '["instance-variable", "instance-variable", nil]', %q{ + class C + @iv1 = "" + @iv2 = 42 + def self.iv1 = defined?(@iv1) # "instance-variable" + def self.iv2 = defined?(@iv2) # "instance-variable" + def self.iv3 = defined?(@iv3) # nil + end + + Ractor.new{ + [C.iv1, C.iv2, C.iv3] + }.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 + 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.value + values.join +} + +# 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 + + r = Ractor.new do + class C + p @@cv + end + end + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# 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 + @@cv + end + end + + C.cv # cache + + r = Ractor.new do + C.cv + end + + begin + 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 + CONST = 'str' + end + r = Ractor.new do + C::CONST + end + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end + RUBY + +# 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() }.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 +RUBY + +# Setting non-shareable objects into constants by other Ractors is not allowed +assert_equal 'can not set constants with non-shareable objects by non-main Ractors', <<~'RUBY', frozen_string_literal: false + class C + end + r = Ractor.new do + C::CONST = 'str' + end + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +RUBY + +# define_method is not allowed +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)}.join + rescue => e + e.cause.message + end +} + +# Immutable Array and Hash are shareable, so it can be shared with constants +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] }.value +} + +# Ractor.count +assert_equal '[1, 4, 3, 2, 1]', %q{ + counts = [] + counts << Ractor.count + ractors = (1..3).map { Ractor.new { Ractor.receive } } + counts << Ractor.count + + ractors[0].send('End 0').join + sleep 0.1 until ractors[0].inspect =~ /terminated/ + counts << Ractor.count + + ractors[1].send('End 1').join + sleep 0.1 until ractors[1].inspect =~ /terminated/ + counts << Ractor.count + + ractors[2].send('End 2').join + sleep 0.1 until ractors[2].inspect =~ /terminated/ + counts << Ractor.count + + counts.inspect +} + +# ObjectSpace.each_object can not handle unshareable objects with Ractors +assert_equal '0', %q{ + Ractor.new{ + n = 0 + ObjectSpace.each_object{|o| n += 1 unless Ractor.shareable?(o)} + n + }.value +} + +# ObjectSpace._id2ref can not handle unshareable objects with Ractors +assert_equal 'ok', <<~'RUBY', frozen_string_literal: false + s = 'hello' + + Ractor.new s.object_id do |id ;s| + begin + s = ObjectSpace._id2ref(id) + rescue => e + :ok + end + 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) +assert_equal 'true', <<~'RUBY', frozen_string_literal: false + class C + def initialize + @a = 'foo' + @b = 'bar' + end + + def freeze + @c = [:freeze_called] + super + end + + attr_reader :a, :b, :c + end + S = Struct.new(:s1, :s2) + str = "hello" + str.instance_variable_set("@iv", "hello") + /a/ =~ 'a' + m = $~ + class N < Numeric + def /(other) + 1 + end + end + ary = []; ary << ary + + a = [[1, ['2', '3']], + {Object.new => "hello"}, + C.new, + S.new("x", "y"), + ("a".."b"), + str, + ary, # cycle + /regexp/, + /#{'r'.upcase}/, + m, + Complex(N.new,0), + Rational(N.new,0), + true, + false, + nil, + 1, 1.2, 1+3r, 1+4i, # Numeric + ] + Ractor.make_shareable(a) + + # check all frozen + a.each{|o| + raise o.inspect unless o.frozen? + + case o + when C + raise o.a.inspect unless o.a.frozen? + raise o.b.inspect unless o.b.frozen? + raise o.c.inspect unless o.c.frozen? && o.c == [:freeze_called] + when Rational + raise o.numerator.inspect unless o.numerator.frozen? + when Complex + raise o.real.inspect unless o.real.frozen? + when Array + if o[0] == 1 + raise o[1][1].inspect unless o[1][1].frozen? + end + when Hash + o.each{|k, v| + raise k.inspect unless k.frozen? + raise v.inspect unless v.frozen? + } + end + } + + Ractor.shareable?(a) +RUBY + +# Ractor.make_shareable(obj) doesn't freeze shareable objects +assert_equal 'true', %q{ + r = Ractor.new{} + Ractor.make_shareable(a = [r]) + [a.frozen?, a[0].frozen?] == [true, false] +} + +# 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{} + + [pr1, pr2].map do |pr| + begin + Ractor.make_shareable(pr) + rescue Ractor::Error => e + e.message[/^.+?:/] + else + :ok + end + end +} + +# 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) +assert_equal '[false, false]', %q{ + y = [] + x = [y, {}].freeze + y << x + y.freeze + [Ractor.shareable?(x), Ractor.shareable?(y)] +} + +# Ractor.make_shareable(recursive_objects) +assert_equal '[:ok, false, false]', %q{ + o = Object.new + def o.freeze; raise; end + y = [] + x = [y, o].freeze + y << x + y.freeze + [(Ractor.make_shareable(x) rescue :ok), Ractor.shareable?(x), Ractor.shareable?(y)] +} + +# Ractor.make_shareable with Class/Module +assert_equal '[C, M]', %q{ + class C; end + module M; end + + Ractor.make_shareable(ary = [C, M]) +} + +# define_method() can invoke different Ractor's proc if the proc is shareable. +assert_equal '1', %q{ + class C + a = 1 + define_method "foo", Ractor.shareable_proc{ a } + end + + Ractor.new{ C.new.foo }.value +} + +# Ractor.make_shareable(obj, copy: true) makes copied shareable object. +assert_equal '[false, false, true, true]', %q{ + r = [] + o1 = [1, 2, ["3"]] + + o2 = Ractor.make_shareable(o1, copy: true) + r << Ractor.shareable?(o1) # false + r << (o1.object_id == o2.object_id) # false + + o3 = Ractor.make_shareable(o1) + r << Ractor.shareable?(o1) # true + r << (o1.object_id == o3.object_id) # false + r +} + +# TracePoint with normal Proc should be Ractor local +assert_equal '[6, 10]', %q{ + rs = [] + TracePoint.new(:line){|tp| rs << tp.lineno if tp.path == __FILE__}.enable do + Ractor.new{ # line 5 + a = 1 + b = 2 + }.value + c = 3 # line 9 + end + rs +} + +# Ractor deep copies frozen objects (ary) +assert_equal '[true, false]', %q{ + Ractor.new([[]].freeze) { |ary| + [ary.frozen?, ary.first.frozen? ] + }.value +} + +# Ractor deep copies frozen objects (str) +assert_equal '[true, false]', %q{ + s = String.new.instance_eval { @x = []; freeze} + Ractor.new(s) { |s| + [s.frozen?, s.instance_variable_get(:@x).frozen?] + }.value +} + +# Can not trap with not isolated Proc on non-main ractor +assert_equal '[:ok, :ok]', %q{ + a = [] + Ractor.new{ + trap(:INT){p :ok} + }.join + a << :ok + + begin + Ractor.new{ + s = 'str' + trap(:INT){p s} + }.join + rescue Ractor::RemoteError + a << :ok + end +} + +# Ractor.select is interruptible +assert_normal_exit %q{ + trap(:INT) do + exit + end + + r = Ractor.new do + loop do + sleep 1 + end + end + + Thread.new do + sleep 0.5 + Process.kill(:INT, Process.pid) + end + Ractor.select(r) +} + +# Ractor-local storage +assert_equal '[nil, "b", "a"]', %q{ + ans = [] + Ractor.current[:key] = 'a' + r = Ractor.new{ + Ractor.main << self[:key] + self[:key] = 'b' + self[:key] + } + 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 +### + +N = 100_000 + +# fstring pool +assert_equal "#{N}#{N}", %Q{ + N = #{N} + 2.times.map{ + Ractor.new{ + N.times{|i| -(i.to_s)} + } + }.map{|r| r.value}.join +} + +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{ + Ractor.new do + #{n}.times do + obj = +'' + obj.instance_variable_set("@a", 1) + obj.instance_variable_set("@b", 1) + obj.instance_variable_set("@c", 1) + obj.instance_variable_defined?("@a") + end + end + }.map{|r| r.value}.join +} + +# Now NoMethodError is copyable +assert_equal "NoMethodError", %q{ + obj = "".freeze # NameError refers the receiver indirectly + begin + obj.bar + rescue => err + end + + r = Ractor.new{ Ractor.receive } + r << err + r.value.class +} + +assert_equal "ok", %q{ + GC.disable + Ractor.new {} + raise "not ok" unless GC.disable + + foo = [] + 10.times { foo << 1 } + + GC.start + + 'ok' +} + +# 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 port do |port| + loop do + 10_000.times.map { Object.new } + port << Time.now + Ractor.receive + end + end + end + + 100.times { + workers.each do + port.receive + end + workers.each do |w| + w.send(nil) + end + } + "ok" +} if !yjit_enabled? && ENV['GITHUB_WORKFLOW'] != 'ModGC' # flaky + +# check method cache invalidation +assert_equal "ok", %q{ + module M + def foo + @foo + end + end + + class A + include M + + def initialize + 100.times { |i| instance_variable_set(:"@var_#{i}", "bad: #{i}") } + @foo = 2 + end + end + + class B + include M + + def initialize + @foo = 1 + end + end + + Ractor.new do + b = B.new + 100_000.times do + raise unless b.foo == 1 + end + end + + a = A.new + 100_000.times do + raise unless a.foo == 2 + end + + "ok" +} + +# check method cache invalidation +assert_equal 'true', %q{ + class C1; def self.foo = 1; end + class C2; def self.foo = 2; end + class C3; def self.foo = 3; end + class C4; def self.foo = 5; end + class C5; def self.foo = 7; end + class C6; def self.foo = 11; end + class C7; def self.foo = 13; end + class C8; def self.foo = 17; end + + LN = 10_000 + RN = 10 + CS = [C1, C2, C3, C4, C5, C6, C7, C8] + rs = RN.times.map{|i| + Ractor.new(CS.shuffle){|cs| + LN.times.sum{ + cs.inject(1){|r, c| r * c.foo} # c.foo invalidates method cache entry + } + } + } + + n = CS.inject(1){|r, c| r * c.foo} * LN + 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 API is experimental/, %q{ + Warning[:experimental] = $VERBOSE = true + STDERR.reopen(STDOUT) + eval("Ractor.new{}.value", nil, "test_ractor.rb", 1) +}, frozen_string_literal: false + +# check moved object +assert_equal 'ok', %q{ + r = Ractor.new do + Ractor.receive + GC.start + :ok + end + + obj = begin + raise + rescue => e + e = Marshal.load(Marshal.dump(e)) + end + + r.send obj, move: true + r.value +} + +## Ractor::Selector + +# Selector#empty? returns true +assert_equal 'true', %q{ + skip true unless defined? Ractor::Selector + + s = Ractor::Selector.new + s.empty? +} + +# Selector#empty? returns false if there is target ractors +assert_equal 'false', %q{ + skip false unless defined? Ractor::Selector + + s = Ractor::Selector.new + s.add Ractor.new{} + s.empty? +} + +# Selector#clear removes all ractors from the waiting list +assert_equal 'true', %q{ + skip true unless defined? Ractor::Selector + + s = Ractor::Selector.new + s.add Ractor.new{10} + s.add Ractor.new{20} + s.clear + s.empty? +} + +# Selector#wait can wait multiple ractors +assert_equal '[10, 20, true]', %q{ + skip [10, 20, true] unless defined? Ractor::Selector + + s = Ractor::Selector.new + s.add Ractor.new{10} + s.add Ractor.new{20} + r, v = s.wait + vs = [] + vs << v + r, v = s.wait + vs << v + [*vs.sort, s.empty?] +} if defined? Ractor::Selector + +# Selector#wait can wait multiple ractors with receiving. +assert_equal '30', %q{ + skip 30 unless defined? Ractor::Selector + + RN = 30 + rs = RN.times.map{ + Ractor.new{ :v } + } + s = Ractor::Selector.new(*rs) + + results = [] + until s.empty? + results << s.wait + + # Note that s.wait can raise an exception because other Ractors/Threads + # can take from the same ractors in the waiting set. + # In this case there is no other takers so `s.wait` doesn't raise an error. + end + + results.size +} if defined? Ractor::Selector + +# Selector#wait can support dynamic addition +assert_equal '600', %q{ + skip 600 unless defined? Ractor::Selector + + RN = 100 + s = Ractor::Selector.new + port = Ractor::Port.new + rs = RN.times.map{ + Ractor.new{ + Ractor.main << Ractor.new(port){|port| port << :v3; :v4 } + Ractor.main << Ractor.new(port){|port| port << :v5; :v6 } + Ractor.yield :v1 + :v2 + } + } + + rs.each{|r| s.add(r)} + h = {v1: 0, v2: 0, v3: 0, v4: 0, v5: 0, v6: 0} + + loop do + case s.wait receive: true + in :receive, r + s.add r + in r, v + h[v] += 1 + break if h.all?{|k, v| v == RN} + end + end + + h.sum{|k, v| v} +} unless yjit_enabled? # http://ci.rvm.jp/results/trunk-yjit@ruby-sp2-docker/4466770 + +# Selector should be GCed (free'ed) without trouble +assert_equal 'ok', %q{ + skip :ok unless defined? Ractor::Selector + + RN = 30 + rs = RN.times.map{ + Ractor.new{ :v } + } + s = Ractor::Selector.new(*rs) + :ok +} + +end # if !ENV['GITHUB_WORKFLOW'] + +# Chilled strings are not shareable +assert_equal 'false', %q{ + Ractor.shareable?("chilled") +} + +# Chilled strings can be made shareable +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 +} |
