ractor-shim 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5132b882154ee4c0af587fb5e4b2f10030ef9834237abbbae5417e3fb51b62a8
4
- data.tar.gz: 0ed22147461e8c6d3feae42d18fd06247ed6d8df800dc6fb2211e56ba9f807dc
3
+ metadata.gz: 8bb91e5a7525967249c155d6193ebacf3b261d6db8c93c06e904aabc11810c79
4
+ data.tar.gz: 81882745a9a7393a06a1916c19b5324272ff5001a9883c61ea4d7d2fadb032c2
5
5
  SHA512:
6
- metadata.gz: bbabf61d3ee523150b793e177a2c60d14cad3ed94848a2290bd9d02f86175f251a641a24cb1841eb01a749e14b7dcbbb94fdcac1c8f23cc1b927a3ac3d533b4e
7
- data.tar.gz: 9264645ee001fbb8f301163a28f88e1e3431639312ad4c249060a75d80f45d69afaae5b7504a3b3c6175eac7325d204a31e1a78a264c7d50a47052be4b7a18b7
6
+ metadata.gz: 80866fae0411eb23c5e1900430763c7d1ce552f0704c911e9c65a7a7b518959f3ee85d2ce0836f4ea33291c37e0061ffa8266086ad2f78d1fc5509991729690a
7
+ data.tar.gz: 42fae31b67236b12e74f84b4d4942e34fe7a4f73fa9280fa971733e30c16edef9a60a8a29312aca99fcac9374050a328d86c0148121c1fffe0b9a5dfd0249477
data/README.md CHANGED
@@ -12,7 +12,7 @@ require 'ractor/shim'
12
12
 
13
13
  ## Development
14
14
 
15
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
15
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `ractor-shim.gemspec`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
16
16
 
17
17
  ## Contributing
18
18
 
data/Rakefile CHANGED
@@ -1,4 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- task default: %i[]
4
+ task default: [:test]
5
+
6
+ task :test do
7
+ exec RbConfig.ruby, "-Ilib", "test/run_tests.rb"
8
+ end
data/lib/ractor/shim.rb CHANGED
@@ -1,3 +1,385 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "shim/version"
3
+ builtin_ractor = !!defined?(Ractor)
4
+ class Ractor
5
+ end
6
+ Ractor.define_singleton_method(:builtin?) { builtin_ractor }
7
+ Ractor.define_singleton_method(:shim?) { !builtin_ractor }
8
+
9
+ if Ractor.shim?
10
+ class Ractor
11
+ class Error < RuntimeError
12
+ end
13
+
14
+ class RemoteError < Error
15
+ attr_reader :ractor
16
+ def initialize(ractor)
17
+ @ractor = ractor
18
+ end
19
+ end
20
+
21
+ class ClosedError < StopIteration
22
+ end
23
+
24
+ @count = 1
25
+ COUNT_MUTEX = Mutex.new
26
+ CHANGE_COUNT = -> delta {
27
+ COUNT_MUTEX.synchronize { @count += delta }
28
+ }
29
+
30
+ @id = 0
31
+ ID_MUTEX = Mutex.new
32
+ GET_ID = -> {
33
+ ID_MUTEX.synchronize { @id += 1 }
34
+ }
35
+
36
+ SELECT_MUTEX = Mutex.new
37
+ SELECT_CV = ConditionVariable.new
38
+
39
+ def self.main
40
+ MAIN_RACTOR
41
+ end
42
+
43
+ def self.main?
44
+ current == main
45
+ end
46
+
47
+ def self.current
48
+ Thread.current.thread_variable_get(:current_ractor) or raise "Could not find current Ractor"
49
+ end
50
+
51
+ def self.count
52
+ COUNT_MUTEX.synchronize { @count }
53
+ end
54
+
55
+ def self.receive
56
+ Ractor.current.__send__(:receive)
57
+ end
58
+
59
+ # def self.select_simple_spinning(*ractors)
60
+ # raise ArgumentError, "specify at least one ractor or `yield_value`" if ractors.empty?
61
+ # while true
62
+ # ractors.each do |ractor_or_port|
63
+ # if Ractor === ractor_or_port
64
+ # queue = ractor_or_port.out_queue
65
+ # elsif Ractor::Port === ractor_or_port
66
+ # queue = ractor_or_port.queue
67
+ # else
68
+ # raise ArgumentError, "Unexpected argument for Ractor.select: #{ractor_or_port}"
69
+ # end
70
+ #
71
+ # begin
72
+ # value = queue.pop(true)
73
+ # return [ractor_or_port, value]
74
+ # rescue ThreadError
75
+ # Thread.pass
76
+ # end
77
+ # end
78
+ # end
79
+ # end
80
+
81
+ def self.select(*ractors)
82
+ raise ArgumentError, "specify at least one ractor or `yield_value`" if ractors.empty?
83
+
84
+ SELECT_MUTEX.synchronize do
85
+ while true
86
+ ractors.each do |ractor_or_port|
87
+ if Ractor === ractor_or_port
88
+ queue = ractor_or_port.out_queue
89
+ elsif Ractor::Port === ractor_or_port
90
+ queue = ractor_or_port.queue
91
+ else
92
+ raise ArgumentError, "Unexpected argument for Ractor.select: #{ractor_or_port}"
93
+ end
94
+
95
+ begin
96
+ value = queue.pop(true)
97
+ return [ractor_or_port, value]
98
+ rescue ThreadError
99
+ # keep looping
100
+ end
101
+ end
102
+
103
+ # Wait until an item is added to a relevant Queue
104
+ SELECT_CV.wait(SELECT_MUTEX)
105
+ end
106
+ end
107
+ end
108
+
109
+ def self.make_shareable(object, copy: false)
110
+ # no copy, just return the object
111
+ object
112
+ end
113
+
114
+ def self.shareable?(object)
115
+ true
116
+ end
117
+
118
+ def self.store_if_absent(var, &block)
119
+ Ractor.current.__send__(:store_if_absent, var, &block)
120
+ end
121
+
122
+ def self._require(feature)
123
+ Kernel.require(feature)
124
+ end
125
+
126
+ attr_reader :name
127
+
128
+ attr_reader :in_queue, :out_queue
129
+
130
+ def initialize(*args, name: nil, &block)
131
+ raise ArgumentError, "must be called with a block" unless block
132
+ initialize_common(name, block)
133
+
134
+ @thread = Thread.new {
135
+ Thread.current.thread_variable_set(:current_ractor, self)
136
+ CHANGE_COUNT.call(1)
137
+ begin
138
+ result = self.instance_exec(*args, &block)
139
+ SELECT_MUTEX.synchronize {
140
+ unless @out_queue.closed?
141
+ @out_queue << result
142
+ SELECT_CV.broadcast
143
+ end
144
+ }
145
+ result
146
+ rescue Exception => e
147
+ @exception = e
148
+ nil
149
+ ensure
150
+ @termination_mutex.synchronize {
151
+ CHANGE_COUNT.call(-1)
152
+ @status = :terminated
153
+
154
+ monitor_message = @exception ? :aborted : :exited
155
+ @monitors.each { |monitor|
156
+ monitor << monitor_message
157
+ }
158
+ @monitors.clear
159
+ }
160
+ end
161
+ }
162
+ end
163
+
164
+ private def initialize_main
165
+ initialize_common(nil, nil)
166
+ @thread = nil
167
+ end
168
+
169
+ private def initialize_common(name, block)
170
+ @name = name.nil? ? nil : (String.try_convert(name) or raise TypeError)
171
+ @id = GET_ID.call
172
+ @status = :running
173
+ @from = block ? block.source_location.join(":") : nil
174
+ @in_queue = Queue.new
175
+ @out_queue = Queue.new
176
+ @storage = {}
177
+ @exception = nil
178
+ @monitors = []
179
+ @termination_mutex = Mutex.new
180
+ end
181
+
182
+ def [](var)
183
+ raise "Cannot get ractor local storage for non-current ractor" unless Ractor.current == self
184
+ @storage[var]
185
+ end
186
+
187
+ def []=(var, value)
188
+ raise "Cannot set ractor local storage for non-current ractor" unless Ractor.current == self
189
+ @storage[var] = value
190
+ end
191
+
192
+ private def store_if_absent(var, &block)
193
+ if value = @storage[var]
194
+ value
195
+ else
196
+ value = block.call
197
+ @storage[var] = value
198
+ value
199
+ end
200
+ end
201
+
202
+ def send(message, move: false)
203
+ raise Ractor::ClosedError, "The port was already closed" if @status == :terminated || @in_queue.closed?
204
+ @in_queue << message
205
+ self
206
+ end
207
+ alias_method :<<, :send
208
+
209
+ private def receive
210
+ @in_queue.pop
211
+ end
212
+
213
+ # def take
214
+ # @out_queue.pop
215
+ # end
216
+
217
+ def close_incoming
218
+ @in_queue.close
219
+ self
220
+ end
221
+
222
+ def close_outgoing
223
+ SELECT_MUTEX.synchronize {
224
+ @out_queue.close
225
+ SELECT_CV.broadcast
226
+ }
227
+ self
228
+ end
229
+
230
+ def monitor(port)
231
+ @termination_mutex.synchronize {
232
+ if @status == :terminated
233
+ port << (@exception ? :aborted : :exited)
234
+ false
235
+ else
236
+ @monitors << port
237
+ end
238
+ }
239
+ end
240
+
241
+ def unmonitor(port)
242
+ @termination_mutex.synchronize {
243
+ @monitors.delete port
244
+ }
245
+ end
246
+
247
+ def join
248
+ value
249
+ self
250
+ end
251
+
252
+ def value
253
+ @thread.join
254
+
255
+ if exc = @exception
256
+ remote_error = RemoteError.new(self)
257
+ raise remote_error, cause: exc
258
+ end
259
+
260
+ @thread.value
261
+ end
262
+
263
+ def inspect
264
+ ["#<Ractor:##{@id}", @name, @from, "#{@status}>"].compact.join(' ')
265
+ end
266
+
267
+ MAIN_RACTOR = Ractor.allocate
268
+ MAIN_RACTOR.__send__(:initialize_main)
269
+ Thread.main.thread_variable_set(:current_ractor, MAIN_RACTOR)
270
+ end
271
+ end
272
+
273
+ if Ractor.builtin?
274
+ class Ractor
275
+ unless method_defined?(:join)
276
+ alias_method :join, :take
277
+ end
278
+
279
+ unless method_defined?(:value)
280
+ alias_method :value, :take
281
+ end
282
+
283
+ unless respond_to?(:main?)
284
+ def self.main?
285
+ self == Ractor.main
286
+ end
287
+ end
288
+ end
289
+ end
290
+
291
+ # common
292
+ class Ractor
293
+ unless method_defined?(:close)
294
+ def close
295
+ close_incoming
296
+ close_outgoing
297
+ end
298
+ end
299
+ end
300
+
301
+ # Ractor.{shareable_proc,shareable_lambda}
302
+
303
+ class << Ractor
304
+ if Ractor.builtin?
305
+ unless method_defined?(:shareable_proc)
306
+ def shareable_proc(&b)
307
+ Ractor.make_shareable(b)
308
+ end
309
+ end
310
+
311
+ unless method_defined?(:shareable_lambda)
312
+ def shareable_lambda(&b)
313
+ Ractor.make_shareable(b)
314
+ end
315
+ end
316
+ else
317
+ unless method_defined?(:shareable_proc)
318
+ alias_method :shareable_proc, :proc
319
+ public :shareable_proc
320
+ end
321
+
322
+ unless method_defined?(:shareable_lambda)
323
+ alias_method :shareable_lambda, :lambda
324
+ public :shareable_lambda
325
+ end
326
+ end
327
+ end
328
+
329
+ # Ractor::Port
330
+
331
+ if Ractor.builtin?
332
+ class Ractor::Port
333
+ QUIT = Object.new.freeze
334
+
335
+ def initialize
336
+ @pipe = Ractor.new do
337
+ while true
338
+ msg = Ractor.receive
339
+ break if QUIT.equal?(msg)
340
+ Ractor.yield msg
341
+ end
342
+ end
343
+ end
344
+
345
+ def <<(message)
346
+ @pipe.send(message)
347
+ end
348
+
349
+ def receive
350
+ @pipe.take
351
+ end
352
+
353
+ def close
354
+ @pipe.send(QUIT)
355
+ end
356
+ end unless defined?(Ractor::Port)
357
+ else
358
+ class Ractor::Port
359
+ attr_reader :queue
360
+
361
+ def initialize
362
+ @queue = Queue.new
363
+ end
364
+
365
+ def <<(message)
366
+ Ractor::SELECT_MUTEX.synchronize {
367
+ @queue << message
368
+ Ractor::SELECT_CV.broadcast
369
+ }
370
+ self
371
+ end
372
+
373
+ def receive
374
+ @queue.pop
375
+ end
376
+
377
+ def close
378
+ Ractor::SELECT_MUTEX.synchronize {
379
+ @queue.close
380
+ Ractor::SELECT_CV.broadcast
381
+ }
382
+ self
383
+ end
384
+ end
385
+ end
data/test/run_tests.rb ADDED
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ractor/shim'
4
+
5
+ BUILTIN_SKIPS = [
6
+ "send shareable and unshareable objects", # leaks Ractors on 3.3
7
+ "Ractor::IsolationError cases", # Ractor.make_shareable too permissive on 3.4
8
+ "Ractor.select from two ractors.", # Ractor.select with ports doesn't work on 3.4
9
+ "SystemExit from a Ractor is re-raised", # Ractor::ClosedError on 3.4 instead of Ractor::RemoteError
10
+ "SystemExit from a Thread inside a Ractor is re-raised", # Ractor::ClosedError on 3.4 instead of Ractor::RemoteError
11
+ "Access to global-variables are prohibited (read)", # different error message on 3.4
12
+ "Access to global-variables are prohibited (write)", # different error message on 3.4
13
+ "Ractor.make_shareable(a_proc) is not supported now.", # no error on 3.4
14
+ "Ractor-local storage", # more permissive on 3.4
15
+ "Now NoMethodError is copyable", # fails on 3.4
16
+ "bind_call in Ractor [Bug #20934]", # SEGV on 3.4
17
+ "moved objects being corrupted if embeded (Hash)", # broken on 3.4
18
+ "moved objects being corrupted if embeded (MatchData)", # SEGV on 3.4
19
+ "moved objects being corrupted if embeded (Struct)", # broken on 3.4
20
+ "moved objects being corrupted if embeded (Object)", # broken on 3.4
21
+ "moved arrays can't be used", # broken on 3.4
22
+ "moved strings can't be used", # broken on 3.4
23
+ "moved hashes can't be used", # broken on 3.4
24
+ "move objects inside frozen containers", # broken on 3.4
25
+ "moved composite types move their non-shareable parts properly", # broken on 3.4
26
+ "Creating classes inside of Ractors", # OOM on 3.4
27
+ "Ractor#join raises RemoteError when the remote Ractor aborted with an exception", # needs join semantics for exceptions
28
+ "Only one Ractor can call Ractor#value", # need value semantics
29
+ "monitor port returns `:exited` when the monitering Ractor terminated.", # Ractor#monitor
30
+ "monitor port returns `:exited` even if the monitoring Ractor was terminated.", # Ractor#monitor
31
+ "monitor returns false if the monitoring Ractor was terminated.", # Ractor#monitor
32
+ "monitor port returns `:aborted` when the monitering Ractor is aborted.", # Ractor#monitor
33
+ "monitor port returns `:aborted` even if the monitoring Ractor was aborted.", # Ractor#monitor
34
+ ]
35
+
36
+ SHIM_SKIPS = [
37
+ "Ractor.allocate is not supported",
38
+ "Ractor::IsolationError cases",
39
+ "$DEBUG, $VERBOSE are Ractor local", # mutates global variables like $DEBUG and $VERBOSE
40
+ "SystemExit from a Thread inside a Ractor is re-raised", # internal error on TruffleRuby
41
+ "unshareable object are copied", # expected due to no copy
42
+ "To copy the object, now Marshal#dump is used", # expected due to no copy
43
+ "send shareable and unshareable objects", # expected due to no copy
44
+ "frozen Objects are shareable", # expected due to no copy
45
+ "touching moved object causes an error", # expected due to no move
46
+ "move example2: Array", # expected due to no move
47
+ "Access to global-variables are prohibited (read)", # can't check
48
+ "Access to global-variables are prohibited (write)", # can't check
49
+ "$stdin,out,err is Ractor local, but shared fds", # can't do
50
+ "given block Proc will be isolated, so can not access outer variables.", # can't check
51
+ "ivar in shareable-objects are not allowed to access from non-main Ractor", # not checked
52
+ "ivar in shareable-objects are not allowed to access from non-main Ractor, by @iv (get)", # not checked
53
+ "ivar in shareable-objects are not allowed to access from non-main Ractor, by @iv (set)", # not checked
54
+ "and instance variables of classes/modules are accessible if they refer shareable objects", # not checked
55
+ "cvar in shareable-objects are not allowed to access from non-main Ractor", # can't check
56
+ "also cached cvar in shareable-objects are not allowed to access from non-main Ractor", # can't check
57
+ "Getting non-shareable objects via constants by other Ractors is not allowed", # can't check
58
+ "Constant cache should care about non-shareable constants", # can't check
59
+ "Setting non-shareable objects into constants by other Ractors is not allowed", # can't check
60
+ "define_method is not allowed", # can't check
61
+ "ObjectSpace._id2ref can not handle unshareable objects with Ractors", # can't check
62
+ "Ractor.make_shareable(obj)", # expected due to no freeze
63
+ "Ractor.make_shareable(obj) doesn't freeze shareable objects", # expected due to no freeze
64
+ "Ractor.make_shareable(a_proc) is not supported now.", # can't check
65
+ "Ractor.shareable?(recursive_objects)", # expected, due to all are shareable
66
+ "Ractor.make_shareable(recursive_objects)", # expected, due to all are shareable
67
+ "Ractor.make_shareable(obj, copy: true) makes copied shareable object.", # expected, due to all are shareable
68
+ "Can not trap with not isolated Proc on non-main ractor", # can't check
69
+ "Ractor-local storage with Thread inheritance of current Ractor", # hard
70
+ "Chilled strings are not shareable", # expected, due to all are shareable
71
+ "moved arrays can't be used", # expected due to no move
72
+ "moved strings can't be used", # expected due to no move
73
+ "moved hashes can't be used", # expected due to no move
74
+ "moved composite types move their non-shareable parts properly", # expected due to no move
75
+ "Only one Ractor can call Ractor#value", # could be implemented
76
+ ]
77
+
78
+ GLOBAL_SKIPS = [
79
+ "threads in a ractor will killed", # leaks Ractors
80
+ "TracePoint with normal Proc should be Ractor local", # line numbers differ due to test harness
81
+ "Can yield back values while GC is sweeping [Bug #18117]", # too slow and leaks
82
+ "check experimental warning", # harness disables experimental warnings
83
+ "failed in autolaod in Ractor", # need more isolation
84
+ "fork after creating Ractor", # fork, leaks
85
+ "Ractors should be terminated after fork", # fork, hangs
86
+ "st_table, which can call malloc.", # leaks
87
+ "Creating classes inside of Ractors", # leaks
88
+ ]
89
+
90
+ skips = GLOBAL_SKIPS
91
+ skips += BUILTIN_SKIPS if Ractor.builtin? && RUBY_VERSION < "3.5"
92
+ skips += SHIM_SKIPS if Ractor.shim?
93
+ if Ractor.builtin? && RUBY_VERSION < "3.4"
94
+ # fails on 3.3
95
+ skips += [
96
+ "unshareable frozen objects should still be frozen in new ractor after move",
97
+ "ivar in shareable-objects are not allowed to access from non-main Ractor",
98
+ "ivar in shareable-objects are not allowed to access from non-main Ractor, by @iv (get)",
99
+ "ivar in shareable-objects are not allowed to access from non-main Ractor, by @iv (set)",
100
+ 900,
101
+ "moved objects have their shape properly set to original object's shape",
102
+ "Ractor-local storage with Thread inheritance of current Ractor",
103
+ "require in Ractor",
104
+ "autolaod in Ractor",
105
+ "moved objects being corrupted if embeded (String)",
106
+ "move object with generic ivar",
107
+ "move object with many generic ivars", # SEGV
108
+ "move object with complex generic ivars",
109
+ ]
110
+ end
111
+ if Ractor.builtin? && RUBY_VERSION < "3.3"
112
+ # fails on 3.2
113
+ skips += [
114
+ "check moved object", # SEGV
115
+ ]
116
+ end
117
+ if Ractor.builtin? && RUBY_VERSION < "3.1"
118
+ # fails on 3.0
119
+ skips += [
120
+ "and instance variables of classes/modules are accessible if they refer shareable objects",
121
+ "define_method is not allowed",
122
+ "check method cache invalidation", # SEGV
123
+ ]
124
+ end
125
+ if Ractor.shim? && RUBY_ENGINE == "ruby" && RUBY_VERSION < "3.0"
126
+ # fails on 2.7
127
+ skips += [
128
+ "Ractor.count",
129
+ "fstring pool 2", # spurious, probably a fstring table bug
130
+ "check method cache invalidation", # syntax
131
+ ]
132
+ end
133
+ if Ractor.shim? && RUBY_ENGINE == "jruby"
134
+ skips += [
135
+ "Ractor.count",
136
+ "ObjectSpace.each_object can not handle unshareable objects with Ractors",
137
+ "fstring pool 2", # spurious, probably a fstring table bug
138
+ ]
139
+ end
140
+ if $DEBUG
141
+ # fails probably Due to Thread.abort_on_exception being true with $DEBUG true
142
+ skips += [
143
+ "an exception in a Ractor non-main thread will not be re-raised at Ractor#receive",
144
+ ]
145
+ end
146
+ SKIPS = skips
147
+
148
+ def new_empty_binding
149
+ # we need a shareable self for shareable procs/lambdas, but also preserve the default definee of Object
150
+ Object.class_eval { binding }
151
+ end
152
+
153
+ def skip(reason = nil)
154
+ throw :skip, :skip
155
+ end
156
+
157
+ TEST_LINES = [""] + File.readlines("#{__dir__}/test_ractor.rb").map(&:chomp)
158
+
159
+ def generic_assert(expected, code, check, &error_message)
160
+ test_info = caller_locations(2, 1)[0]
161
+ line = test_info.lineno
162
+ test_name = TEST_LINES[line-1]
163
+ test_name = TEST_LINES[line-2] if test_name.start_with?("# [Bug")
164
+ warn "Could not find test name for test at line #{line}" unless test_name.start_with?("# ")
165
+ test_name = test_name[2..-1]
166
+
167
+ print "Running test from line #{line}: "
168
+ if SKIPS.include?(line) or SKIPS.include?(test_name)
169
+ puts "SKIP"
170
+ return
171
+ end
172
+
173
+ begin
174
+ actual = catch(:skip) do
175
+ eval(code, new_empty_binding, test_info.path, line).to_s
176
+ end
177
+ return if :skip == actual
178
+ rescue => e
179
+ puts "ERROR"
180
+ e.set_backtrace(e.backtrace.reject { |b|
181
+ b.include?(__FILE__)
182
+ }.map { |b|
183
+ b.sub(/(Object#)?new_empty_binding/, 'test')
184
+ })
185
+ raise e
186
+ end
187
+
188
+ if check.call(actual, expected)
189
+ puts "PASS #{actual}"
190
+ else
191
+ puts "FAIL"
192
+ raise RuntimeError, error_message.call(actual, expected), caller(2)
193
+ end
194
+
195
+ wait_ms = 10
196
+ waited = 0
197
+ while (leaked = Ractor.count - 1) > 0 and waited < wait_ms
198
+ sleep 0.001
199
+ waited += 1
200
+ end
201
+ unless leaked == 0
202
+ raise "Test at line #{line} leaked #{leaked} Ractors"
203
+ end
204
+
205
+ # cleanup global state
206
+ Object.send(:remove_const, :A) if Object.const_defined?(:A)
207
+ end
208
+
209
+ def assert_equal(expected, code, frozen_string_literal: nil)
210
+ generic_assert(expected, code, -> a, e { e == a }) { |actual, expected|
211
+ "Expected #{expected.inspect} but got #{actual.inspect}"
212
+ }
213
+ end
214
+
215
+ def assert_match(expected, code, frozen_string_literal: nil)
216
+ generic_assert(expected, code, -> a, e { e.match?(a) }) { |actual, expected|
217
+ "Expected #{expected} =~ #{actual.inspect} but it was false"
218
+ }
219
+ end
220
+
221
+ def assert_normal_exit(code)
222
+ # Skip because such tests need to run in their own process
223
+ return
224
+ # generic_assert(:unused, code, -> a, e { true }) { |actual, expected|
225
+ # raise "unreachable"
226
+ # }
227
+ end
228
+
229
+ def yjit_enabled?
230
+ false
231
+ end
232
+
233
+ Warning[:experimental] = false
234
+
235
+ require_relative 'test_ractor'