# frozen_string_literal: false require 'test/unit' class TestRactor < Test::Unit::TestCase def test_shareability_of_iseq_proc assert_raise Ractor::IsolationError do foo = [] Ractor.shareable_proc{ foo } end end def test_shareability_of_method_proc # TODO: fix with Ractor.shareable_proc/lambda =begin str = +"" x = str.instance_exec { proc { to_s } } assert_unshareable(x, /Proc\'s self is not shareable/) x = str.instance_exec { method(:to_s) } assert_unshareable(x, "can not make shareable object for #", exception: Ractor::Error) x = str.instance_exec { method(:to_s).to_proc } assert_unshareable(x, "can not make shareable object for #", exception: Ractor::Error) x = str.instance_exec { method(:itself).to_proc } assert_unshareable(x, "can not make shareable object for #", exception: Ractor::Error) str.freeze x = str.instance_exec { proc { to_s } } assert_make_shareable(x) x = str.instance_exec { method(:to_s) } assert_unshareable(x, "can not make shareable object for #", exception: Ractor::Error) x = str.instance_exec { method(:to_s).to_proc } assert_unshareable(x, "can not make shareable object for #", exception: Ractor::Error) x = str.instance_exec { method(:itself).to_proc } assert_unshareable(x, "can not make shareable object for #", exception: Ractor::Error) =end end def test_shareability_error_uses_inspect x = (+"").instance_exec { method(:to_s) } def x.to_s raise "this should not be called" end assert_unshareable(x, "can not make shareable object for # because it refers unshareable objects", exception: Ractor::Error) end def test_default_thread_group assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; Warning[:experimental] = false main_ractor_id = Thread.current.group.object_id ractor_id = Ractor.new { Thread.current.group.object_id }.value refute_equal main_ractor_id, ractor_id end; end def test_class_instance_variables assert_ractor(<<~'RUBY') # Once we're in multi-ractor mode, the codepaths # for class instance variables are a bit different. Ractor.new {}.value class TestClass @a = 1 @b = 2 @c = 3 @d = 4 end assert_equal 4, TestClass.remove_instance_variable(:@d) assert_nil TestClass.instance_variable_get(:@d) assert_equal 4, TestClass.instance_variable_set(:@d, 4) assert_equal 4, TestClass.instance_variable_get(:@d) RUBY end def test_struct_instance_variables assert_ractor(<<~'RUBY') StructIvar = Struct.new(:member) do def initialize(*) super @ivar = "ivar" end attr_reader :ivar end obj = StructIvar.new("member") obj_copy = Ractor.new { Ractor.receive }.send(obj).value assert_equal obj.ivar, obj_copy.ivar refute_same obj.ivar, obj_copy.ivar assert_equal obj.member, obj_copy.member refute_same obj.member, obj_copy.member RUBY end def test_move_nested_hash_during_gc_with_yjit assert_ractor(<<~'RUBY', args: [{ "RUBY_YJIT_ENABLE" => "1" }]) GC.stress = true hash = { foo: { bar: "hello" }, baz: { qux: "there" } } result = Ractor.new { Ractor.receive }.send(hash, move: true).value assert_equal "hello", result[:foo][:bar] assert_equal "there", result[:baz][:qux] RUBY end def test_fork_raise_isolation_error assert_ractor(<<~'RUBY') ractor = Ractor.new do Process.fork rescue Ractor::IsolationError => e e end assert_equal Ractor::IsolationError, ractor.value.class RUBY end if Process.respond_to?(:fork) def test_require_raises_and_no_ractor_belonging_issue assert_ractor(<<~'RUBY') require "tempfile" f = Tempfile.new(["file_to_require_from_ractor", ".rb"]) f.write("raise 'uh oh'") f.flush err_msg = Ractor.new(f.path) do |path| begin require path rescue RuntimeError => e e.message # had confirm belonging issue here else nil end end.value assert_equal "uh oh", err_msg RUBY end def test_require_non_string assert_ractor(<<~'RUBY') require "tempfile" require "pathname" f = Tempfile.new(["file_to_require_from_ractor", ".rb"]) f.write("") f.flush result = Ractor.new(f.path) do |path| require Pathname.new(path) "success" end.value assert_equal "success", result RUBY end # [Bug #21398] def test_port_receive_dnt_with_port_send omit 'unstable on windows and macos-14' if RUBY_PLATFORM =~ /mswin|mingw|darwin/ assert_ractor(<<~'RUBY', timeout: 90) THREADS = 10 JOBS_PER_THREAD = 50 ARRAY_SIZE = 20_000 def ractor_job(job_count, array_size) port = Ractor::Port.new workers = (1..4).map do |i| Ractor.new(port) do |job_port| while job = Ractor.receive result = job.map { |x| x * 2 }.sum job_port.send result end end end jobs = Array.new(job_count) { Array.new(array_size) { rand(1000) } } jobs.each_with_index do |job, i| w_idx = i % 4 workers[w_idx].send(job) end results = [] jobs.size.times do result = port.receive # dnt receive results << result end results end threads = [] # creates 40 ractors (THREADSx4) THREADS.times do threads << Thread.new do ractor_job(JOBS_PER_THREAD, ARRAY_SIZE) end end threads.each(&:join) RUBY end # [Bug #20146] def test_max_cpu_1 assert_ractor(<<~'RUBY', args: [{ "RUBY_MAX_CPU" => "1" }]) assert_equal :ok, Ractor.new { :ok }.value RUBY end def test_symbol_proc_is_shareable pr = :symbol.to_proc assert_make_shareable(pr) end # [Bug #21775] def test_ifunc_proc_not_shareable h = Hash.new { self } pr = h.to_proc assert_unshareable(pr, /not supported yet/, exception: RuntimeError) end def assert_make_shareable(obj) refute Ractor.shareable?(obj), "object was already shareable" Ractor.make_shareable(obj) assert Ractor.shareable?(obj), "object didn't become shareable" end def assert_unshareable(obj, msg=nil, exception: Ractor::IsolationError) refute Ractor.shareable?(obj), "object is already shareable" assert_raise_with_message(exception, msg) do Ractor.make_shareable(obj) end refute Ractor.shareable?(obj), "despite raising, object became shareable" end end