Backport `Ractor`

Goal: if it works with `Ractor`, it works with backport
Non-goal: if it fails with `Ractor` (e.g. access class variable), it fails with backport. These checks are not present with backport.
Limitations: uses `Thread`s instead of actual actors, so won't have the actual performance of `Ractor`.
This commit is contained in:
Marc-Andre Lafortune 2020-10-23 22:32:18 -04:00
parent ef1e0422d1
commit 641d565bad
16 changed files with 2396 additions and 12 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ pkg
assets
rs
Gemfile.lock
.ruby-version

View File

@ -134,6 +134,8 @@ Style/HashEachMethods:
Style/HashSyntax:
EnforcedStyle: hash_rockets
Exclude:
- 'lib/backports/ractor/*.rb'
Style/Lambda:
EnforcedStyle: lambda
@ -150,8 +152,17 @@ Style/SoleNestedConditional:
Lint/ToEnumArguments:
Enabled: false
Style/OptionalBooleanParameter:
Enabled: false
Layout/CommentIndentation:
Enabled: false # buggy
Style/TrailingCommaInHashLiteral:
EnforcedStyleForMultiline: consistent_comma
Style/RedundantBegin:
Enabled: false # targetting older Ruby
Style/OptionalBooleanParameter:
Enabled: false # ok for private methods
Lint/DuplicateBranch:
Enabled: false

View File

@ -11,6 +11,6 @@ end
if RUBY_VERSION >= '2.4.0'
group :development do
gem 'rubocop', '~> 1.1.0'
gem 'rubocop', '~> 1.6.0'
end
end

View File

@ -1,6 +1,7 @@
# Backports Library [<img src="https://travis-ci.org/marcandre/backports.svg?branch=master">](https://travis-ci.org/marcandre/backports) [<img src="https://badge.fury.io/rb/backports.svg" alt="Gem Version" />](http://badge.fury.io/rb/backports) [![Tidelift](https://tidelift.com/badges/package/rubygems/backports)](https://tidelift.com/subscription/pkg/rubygems-backports?utm_source=rubygems-backports&utm_medium=referral&utm_campaign=readme)
Yearning to use some of the new cool features in Ruby 2.7 while using 2.3.x?
Yearning to use write a gem using some new cool features in Ruby 3.0 while
still supporting Ruby 2.5.x?
Have some legacy code in Ruby 1.8 but can't live without `flat_map`?
This gem is for you!
@ -16,13 +17,13 @@ for Ruby < 2.2.
### Explicitly (recommended)
For example, if you want to use transform_values and transform_keys, even in
For example, if you want to use `transform_values` and `transform_keys`, even in
Ruby implementations that don't include it:
require 'backports/2.4.0/hash/transform_values'
require 'backports/2.5.0/hash/transform_keys'
This will enable Hash#transform_values and Hash#transform_keys, using the
This will enable `Hash#transform_values` and `Hash#transform_keys`, using the
native versions if available or otherwise provide a pure Ruby version.
### By Module
@ -32,19 +33,17 @@ Class:
require 'backports/2.3.0/hash'
This will make sure that Hash responds to dig, fetch_values, <, <=, >, >= and
to_proc
This will make sure that Hash responds to `dig`, `fetch_values`, `to_proc` and comparisons.
### Up to a specific Ruby version (for quick coding)
You can load all backports up to a specific version.
For example, to bring any
version of Ruby mostly up to Ruby 2.7.0's standards:
For example, to bring any version of Ruby mostly up to Ruby 3.0.0's standards:
require 'backports/2.7.0'
require 'backports/3.0.0'
This will bring in all the features of 1.8.7 and many features of Ruby 1.9.x
all the way up to Ruby 2.7.0 (for all versions of Ruby)!
all the way up to Ruby 3.0.0 (for all versions of Ruby)!
You may `require 'backports/latest'` as a
shortcut to the latest Ruby version supported.
@ -118,6 +117,12 @@ itself, JRuby and Rubinius.
- `except`
- `transform_keys`, `transform_keys!` (with hash argument)
#### Ractor
- All methods, with the caveats:
- uses Ruby's `Thread` internally
- will not raise some errors when `Ractor` would (in particular `Ractor::IsolationError`)
- supported in Ruby 2.0+ only
#### Symbol
- `name`

View File

@ -164,6 +164,7 @@ IGNORE_IN_200 = %w[
IGNORE = %w[
3.0.0/env/except
3.0.0/symbol/name
3.0.0/ractor/ractor
]
CLASS_MAP = Hash.new{|k, v| k[v] = v}.merge!(

View File

@ -0,0 +1,5 @@
if RUBY_VERSION < '2'
warn 'Ractor not backported to Ruby 1.x'
else
require_relative '../ractor/ractor' unless defined?(Ractor.current)
end

View File

@ -0,0 +1,91 @@
require_relative '../2.4.0/hash/transform_values'
require_relative '../2.5.0/hash/transform_keys'
class Ractor
module Cloner
extend self
def deep_clone(obj)
return obj if Ractor.ractor_shareable_self?(obj, false) { false }
@processed = {}.compare_by_identity
@changed = nil
result = process(obj) do |r|
copy_contents(r)
end
return result if result
Ractor.ractor_mark_set_shareable(@processed)
obj
end
# Yields a deep copy.
# If no deep copy is needed, `obj` is returned and
# nothing is yielded
private def clone_deeper(obj)
return obj if Ractor.ractor_shareable_self?(obj, false) { false }
result = process(obj) do |r|
copy_contents(r)
end
return obj unless result
yield result if block_given?
result
end
# Yields if `obj` is a new structure
# Returns the deep copy, or `false` if no deep copy is needed
private def process(obj)
@processed.fetch(obj) do
# For recursive structures, assume that we'll need a duplicate.
# If that's not the case, we will have duplicated the whole structure
# for nothing...
@processed[obj] = result = obj.dup
changed = track_change { yield result }
return false if obj.frozen? && !changed
@changed = true
result.freeze if obj.frozen?
result
end
end
# returns if the block called `deep clone` and that the deep copy was needed
private def track_change
prev = @changed
@changed = false
yield
@changed
ensure
@changed = prev
end
# modifies in place `obj` by calling `deep clone` on its contents
private def copy_contents(obj)
case obj
when ::Hash
if obj.default
clone_deeper(obj.default) do |copy|
obj.default = copy
end
end
obj.transform_keys! { |key| clone_deeper(key) }
obj.transform_values! { |value| clone_deeper(value) }
when ::Array
obj.map! { |item| clone_deeper(item) }
when ::Struct
obj.each_pair do |key, item|
clone_deeper(item) { |copy| obj[key] = copy }
end
end
obj.instance_variables.each do |var|
clone_deeper(obj.instance_variable_get(var)) do |copy|
obj.instance_variable_set(var, copy)
end
end
end
end
private_constant :Cloner
end

View File

@ -0,0 +1,16 @@
class Ractor
class ClosedError < ::StopIteration
end
class Error < ::StandardError
end
class RemoteError < Error
attr_reader :ractor
def initialize(message = nil)
@ractor = Ractor.current
super
end
end
end

View File

@ -0,0 +1,62 @@
require_relative '../tools/filtered_queue'
class Ractor
# Standard ::Queue but raises if popping and closed
class BaseQueue < ::Backports::FilteredQueue
ClosedQueueError = ::Ractor::ClosedError
# yields message (if any)
def pop_non_blocking
yield pop(timeout: 0)
rescue TimeoutError
nil
end
end
class IncomingQueue < BaseQueue
TYPE = :incoming
protected def reenter
raise ::Ractor::Error, 'Can not reenter'
end
end
# * Wraps exception
# * Add `ack: ` to push (blocking)
class OutgoingQueue < BaseQueue
TYPE = :outgoing
WrappedException = ::Struct.new(:exception, :ractor)
def initialize
@ack_queue = ::Queue.new
super
end
def pop(timeout: nil, ack: true)
r = super(timeout: timeout)
@ack_queue << :done if ack
raise r.exception if WrappedException === r
r
end
def close(how = :hard)
super()
return if how == :soft
clear
@ack_queue.close
end
def push(obj, ack:)
super(obj)
if ack
r = @ack_queue.pop # block until popped
raise ClosedError, "The #{self.class::TYPE}-port is already closed" unless r == :done
end
self
end
end
private_constant :BaseQueue, :OutgoingQueue, :IncomingQueue
end

View File

@ -0,0 +1,238 @@
# Ruby 2.0+ backport of `Ractor` class
# Extra private methods and instance variables all start with `ractor_`
class Ractor
require_relative '../tools/arguments'
require_relative 'cloner'
require_relative 'errors'
require_relative 'queues'
require_relative 'sharing'
# Implementation notes
#
# Uses one `Thread` for each `Ractor`, as well as queues for communication
#
# The incoming queue is strict: contrary to standard queue, you can't pop from an empty closed queue.
# Since standard queues return `nil` is those conditions, we wrap/unwrap `nil` values and consider
# all `nil` values to be results of closed queues. `ClosedQueueError` are re-raised as `Ractor::ClosedError`
#
# The outgoing queue is strict and blocking. Same wrapping / raising as incoming,
# with an extra queue to acknowledge when a value has been read (or if the port is closed while waiting).
#
# The last result is a bit tricky as it needs to be pushed on the outgoing queue but can not be blocking.
# For this, we "soft close" the outgoing port.
def initialize(*args, &block)
@ractor_incoming_queue = IncomingQueue.new
@ractor_outgoing_queue = OutgoingQueue.new
raise ArgumentError, 'must be called with a block' unless block
kw = args.last
if kw.is_a?(Hash) && kw.size == 1 && kw.key?(:name)
args.pop
name = kw[:name]
end
@ractor_name = name && Backports.coerce_to_str(name)
if Ractor.main == nil # then initializing main Ractor
@ractor_thread = ::Thread.current
@ractor_origin = nil
@ractor_thread.thread_variable_set(:ractor, self)
else
@ractor_origin = caller(1, 1).first.split(':in `').first
args.map! { |a| Ractor.ractor_isolate(a, false) }
ractor_thread_start(args, block)
end
end
private def ractor_thread_start(args, block)
Thread.new do
@ractor_thread = Thread.current
@ractor_thread_group = ThreadGroup.new.add(@ractor_thread)
::Thread.current.thread_variable_set(:ractor, self)
result = nil
begin
result = instance_exec(*args, &block)
rescue ::Exception => err # rubocop:disable Lint/RescueException
begin
raise RemoteError, "thrown by remote Ractor: #{err.message}"
rescue RemoteError => e # Hack to create exception with `cause`
result = OutgoingQueue::WrappedException.new(e)
end
ensure
ractor_thread_terminate(result)
end
end
end
private def ractor_thread_terminate(result)
begin
ractor_outgoing_queue.push(result, ack: false) unless ractor_outgoing_queue.closed?
rescue ClosedQueueError
return # ignore
end
ractor_incoming_queue.close
ractor_outgoing_queue.close(:soft)
ensure
# TODO: synchronize?
@ractor_thread_group.list.each do |thread|
thread.kill unless thread == Thread.current
end
end
def send(obj, move: false)
ractor_incoming_queue << Ractor.ractor_isolate(obj, move)
self
rescue ::ClosedQueueError
raise ClosedError, 'The incoming-port is already closed'
end
alias_method :<<, :send
def take
ractor_outgoing_queue.pop(ack: true)
end
def name
@ractor_name
end
RACTOR_STATE = {
'sleep' => 'blocking',
'run' => 'running',
'aborting' => 'aborting',
false => 'terminated',
nil => 'terminated',
}.freeze
private_constant :RACTOR_STATE
def inspect
state = RACTOR_STATE[@ractor_thread ? @ractor_thread.status : 'run']
info = [
'Ractor:#1',
name,
@ractor_origin,
state
].compact.join(' ')
"#<#{info}>"
end
def close_incoming
r = ractor_incoming_queue.closed?
ractor_incoming_queue.close
r
end
def close_outgoing
r = ractor_outgoing_queue.closed?
ractor_outgoing_queue.close
r
end
private def receive
ractor_incoming_queue.pop
end
private def receive_if(&block)
raise ArgumentError, 'no block given' unless block
ractor_incoming_queue.pop(&block)
end
class << self
def yield(value, move: false)
value = ractor_isolate(value, move)
current.ractor_outgoing_queue.push(value, ack: true)
rescue ClosedQueueError
raise ClosedError, 'The outgoing-port is already closed'
end
def receive
current.__send__(:receive)
end
alias_method :recv, :receive
def receive_if(&block)
current.__send__(:receive_if, &block)
end
def select(*ractors, yield_value: not_given = true, move: false)
cur = Ractor.current
queues = ractors.map do |r|
r == cur ? r.ractor_incoming_queue : r.ractor_outgoing_queue
end
if !not_given
out = current.ractor_outgoing_queue
yield_value = ractor_isolate(yield_value, move)
elsif ractors.empty?
raise ArgumentError, 'specify at least one ractor or `yield_value`'
end
while true # rubocop:disable Style/InfiniteLoop
# Don't `loop`, in case of `ClosedError` (not that there should be any)
queues.each_with_index do |q, i|
q.pop_non_blocking do |val|
r = ractors[i]
return [r == cur ? :receive : r, val]
end
end
if out && out.num_waiting > 0
# Not quite atomic...
out.push(yield_value, ack: true)
return [:yield, nil]
end
sleep(0.001)
end
end
def make_shareable(obj)
return obj if ractor_check_shareability?(obj, true)
raise Ractor::Error, '#freeze does not freeze object correctly'
end
def shareable?(obj)
ractor_check_shareability?(obj, false)
end
def current
Thread.current.thread_variable_get(:ractor)
end
def count
ObjectSpace.each_object(Ractor).count(&:ractor_live?)
end
# @api private
def ractor_reset
ObjectSpace.each_object(Ractor).each do |r|
next if r == Ractor.current
next unless (th = r.ractor_thread)
th.kill
th.join
end
Ractor.current.ractor_incoming_queue.clear
end
attr_reader :main
private def ractor_init
@ractor_shareable = ::ObjectSpace::WeakMap.new
@main = Ractor.new { nil }
end
end
# @api private
def ractor_live?
!defined?(@ractor_thread) || # May happen if `count` is called from another thread before `initialize` has completed
@ractor_thread.status
end
# @api private
attr_reader :ractor_outgoing_queue, :ractor_incoming_queue, :ractor_thread
ractor_init
end

View File

@ -0,0 +1,93 @@
class Ractor
class << self
# @api private
def ractor_isolate(val, move = false)
return val if move
Cloner.deep_clone(val)
end
private def ractor_check_shareability?(obj, freeze_all)
ractor_shareable_self?(obj, freeze_all) do
visited = {}
return false unless ractor_shareable_parts?(obj, freeze_all, visited)
ractor_mark_set_shareable(visited)
true
end
end
# yield if shareability can't be determined without looking at its parts
def ractor_shareable_self?(obj, freeze_all)
return true if @ractor_shareable.key?(obj)
return true if ractor_shareable_by_nature?(obj, freeze_all)
if obj.frozen? || (freeze_all && obj.freeze)
yield
else
false
end
end
private def ractor_shareable_parts?(obj, freeze_all, visited)
return true if visited.key?(obj)
visited[obj] = true
ractor_traverse(obj) do |part|
return false unless ractor_shareable_self?(part, freeze_all) do
ractor_shareable_parts?(part, freeze_all, visited)
end
end
true
end
def ractor_mark_set_shareable(visited)
visited.each do |key|
@ractor_shareable[key] = Ractor
end
end
private def ractor_traverse(obj, &block)
case obj
when ::Hash
Hash obj.default
yield obj.default_proc
obj.each do |key, value|
yield key
yield value
end
when ::Range
yield obj.begin
yield obj.end
when ::Array, ::Struct
obj.each(&block)
when ::Complex
yield obj.real
yield obj.imaginary
when ::Rational
yield obj.numerator
yield obj.denominator
end
obj.instance_variables.each do |var|
yield obj.instance_variable_get(var)
end
end
private def ractor_shareable_by_nature?(obj, freeze_all)
case obj
when ::Module, ::Ractor
true
when ::Regexp, ::Range, ::Numeric
!freeze_all # Assume that these are literals that would have been frozen in 3.0
# unless we're making them shareable, in which case we might as well
# freeze them for real.
when ::Symbol, false, true, nil # Were only frozen in Ruby 2.3+
true
else
false
end
end
end
end

480
test/mri_runner.rb Normal file
View File

@ -0,0 +1,480 @@
# Adapted from MRI's bootstraptest/runner
def main
# @ruby = File.expand_path('miniruby')
@ruby = nil
@verbose = false
$VERBOSE = false
$stress = false
@color = nil
@tty = nil
@quiet = false
dir = nil
quiet = false
tests = nil
# ARGV.delete_if {|arg|
# case arg
# when /\A--ruby=(.*)/
# @ruby = $1
# @ruby.gsub!(/^([^ ]*)/){File.expand_path($1)}
# @ruby.gsub!(/(\s+-I\s*)((?!(?:\.\/)*-(?:\s|\z))\S+)/){$1+File.expand_path($2)}
# @ruby.gsub!(/(\s+-r\s*)(\.\.?\/\S+)/){$1+File.expand_path($2)}
# true
# when /\A--sets=(.*)/
# tests = Dir.glob("#{File.dirname($0)}/test_{#{$1}}*.rb").sort
# puts tests.map {|path| File.basename(path) }.inspect
# true
# when /\A--dir=(.*)/
# dir = $1
# true
# when /\A(--stress|-s)/
# $stress = true
# when /\A--color(?:=(?:always|(auto)|(never)|(.*)))?\z/
# warn "unknown --color argument: #$3" if $3
# @color = $1 ? nil : !$2
# true
# when /\A--tty(=(?:yes|(no)|(.*)))?\z/
# warn "unknown --tty argument: #$3" if $3
# @tty = !$1 || !$2
# true
# when /\A(-q|--q(uiet))\z/
# quiet = true
# @quiet = true
# true
# when /\A(-v|--v(erbose))\z/
# @verbose = true
# when /\A(-h|--h(elp)?)\z/
# puts(<<-End)
# Usage: #{File.basename($0, '.*')} --ruby=PATH [--sets=NAME,NAME,...]
# --sets=NAME,NAME,... Name of test sets.
# --dir=DIRECTORY Working directory.
# default: /tmp/bootstraptestXXXXX.tmpwd
# --color[=WHEN] Colorize the output. WHEN defaults to 'always'
# or can be 'never' or 'auto'.
# -s, --stress stress test.
# -v, --verbose Output test name before exec.
# -q, --quiet Don\'t print header message.
# -h, --help Print this message and quit.
# End
# exit true
# when /\A-j/
# true
# else
# false
# end
# }
# if tests and not ARGV.empty?
# $stderr.puts "--tests and arguments are exclusive"
# exit false
# end
# tests ||= ARGV
# tests = Dir.glob("#{File.dirname($0)}/test_*.rb").sort if tests.empty?
# pathes = tests.map {|path| File.expand_path(path) }
@progress = %w[- \\ | /]
@progress_bs = "\b" * @progress[0].size
@tty = $stderr.tty? if @tty.nil?
case @color
when nil
@color = @tty && /dumb/ !~ ENV["TERM"]
end
@tty &&= !@verbose
if @color
# dircolors-like style
colors = (colors = ENV['TEST_COLORS']) ? Hash[colors.scan(/(\w+)=([^:\n]*)/)] : {}
begin
File.read(File.join(__dir__, "../tool/colors")).scan(/(\w+)=([^:\n]*)/) do |n, c|
colors[n] ||= c
end
rescue
end
@passed = "\e[;#{colors["pass"] || "32"}m"
@failed = "\e[;#{colors["fail"] || "31"}m"
@reset = "\e[m"
else
@passed = @failed = @reset = ""
end
# unless quiet
# puts Time.now
# 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}]"
# else
# puts "Driver is ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}) [#{RUBY_PLATFORM}]"
# end
# puts "Target is #{`#{@ruby} -v`.chomp}"
# puts
# $stdout.flush
# end
# in_temporary_working_directory(dir) {
# exec_test pathes
# }
end
def erase(e = true)
if e and @columns > 0 and @tty and !@verbose
"\e[1K\r"
else
""
end
end
def exec_test(pathes)
@count = 0
@error = 0
@errbuf = []
@location = nil
@columns = 0
@width = pathes.map {|path| File.basename(path).size}.max + 2
pathes.each do |path|
@basename = File.basename(path)
$stderr.printf("%s%-*s ", erase(@quiet), @width, @basename)
$stderr.flush
@columns = @width + 1
$stderr.puts if @verbose
count = @count
error = @error
load File.expand_path(path)
if @tty
if @error == error
msg = "PASS #{@count-count}"
@columns += msg.size - 1
$stderr.print "#{@progress_bs}#{@passed}#{msg}#{@reset}"
else
msg = "FAIL #{@error-error}/#{@count-count}"
$stderr.print "#{@progress_bs}#{@failed}#{msg}#{@reset}"
@columns = 0
end
end
$stderr.puts unless @quiet and @tty and @error == error
end
$stderr.print(erase) if @quiet
@errbuf.each do |msg|
$stderr.puts msg
end
if @error == 0
if @count == 0
$stderr.puts "No tests, no problem"
else
$stderr.puts "#{@passed}PASS#{@reset} all #{@count} tests"
end
exit true
else
$stderr.puts "#{@failed}FAIL#{@reset} #{@error}/#{@count} tests failed"
exit false
end
end
def show_progress(message = '')
if @verbose
$stderr.print "\##{@count} #{@location} "
elsif @tty
$stderr.print "#{@progress_bs}#{@progress[@count % @progress.size]}"
end
t = Time.now if @verbose
faildesc, errout = with_stderr {yield}
t = Time.now - t if @verbose
if !faildesc
if @tty
$stderr.print "#{@progress_bs}#{@progress[@count % @progress.size]}"
elsif @verbose
$stderr.printf(". %.3f\n", t)
else
$stderr.print '.'
end
else
$stderr.print "#{@failed}F"
$stderr.printf(" %.3f", t) if @verbose
$stderr.print @reset
$stderr.puts if @verbose
error faildesc, message
unless errout.empty?
$stderr.print "#{@failed}stderr output is not empty#{@reset}\n", adjust_indent(errout)
end
if @tty and !@verbose
$stderr.printf("%-*s%s", @width, @basename, @progress[@count % @progress.size])
end
end
rescue Interrupt
$stderr.puts "\##{@count} #{@location}"
raise
rescue Exception => err
$stderr.print 'E'
$stderr.puts if @verbose
error err.message, message, err.backtrace
ensure
begin
check_coredump
rescue CoreDumpError => err
$stderr.print 'E'
$stderr.puts if @verbose
error err.message, message
end
end
def show_limit(testsrc, opt = '', **argh)
result = get_result_string(testsrc, opt, **argh)
if @tty and @verbose
$stderr.puts ".{#@reset}\n#{erase}#{result}"
else
@errbuf.push result
end
end
def assert_check(testsrc, message = '', opt = '', **argh)
show_progress(message) {
result = get_result_string(testsrc, opt, **argh)
yield(result)
}
end
def assert_equal(expected, testsrc, message = '', opt = '', **argh)
newtest
assert_check(testsrc, message, opt, **argh) {|result|
if expected == result
nil
else
desc = "#{result.inspect} (expected #{expected.inspect})"
pretty(testsrc, desc, result)
end
}
end
def assert_match(expected_pattern, testsrc, message = '')
newtest
assert_check(testsrc, message) {|result|
if expected_pattern =~ result
nil
else
desc = "#{expected_pattern.inspect} expected to be =~\n#{result.inspect}"
pretty(testsrc, desc, result)
end
}
end
def assert_not_match(unexpected_pattern, testsrc, message = '')
newtest
assert_check(testsrc, message) {|result|
if unexpected_pattern !~ result
nil
else
desc = "#{unexpected_pattern.inspect} expected to be !~\n#{result.inspect}"
pretty(testsrc, desc, result)
end
}
end
def assert_valid_syntax(testsrc, message = '')
newtest
assert_check(testsrc, message, '-c') {|result|
result if /Syntax OK/ !~ result
}
end
def assert_normal_exit(testsrc, *rest, timeout: nil, **opt)
newtest
message, ignore_signals = rest
message ||= ''
show_progress(message) {
faildesc = nil
filename = make_srcfile(testsrc)
old_stderr = $stderr.dup
timeout_signaled = false
begin
$stderr.reopen("assert_normal_exit.log", "w")
io = IO.popen("#{@ruby} -W0 #{filename}")
pid = io.pid
th = Thread.new {
io.read
io.close
$?
}
if !th.join(timeout)
Process.kill :KILL, pid
timeout_signaled = true
end
status = th.value
ensure
$stderr.reopen(old_stderr)
old_stderr.close
end
if status && status.signaled?
signo = status.termsig
signame = Signal.list.invert[signo]
unless ignore_signals and ignore_signals.include?(signame)
sigdesc = "signal #{signo}"
if signame
sigdesc = "SIG#{signame} (#{sigdesc})"
end
if timeout_signaled
sigdesc << " (timeout)"
end
faildesc = pretty(testsrc, "killed by #{sigdesc}", nil)
stderr_log = File.read("assert_normal_exit.log")
if !stderr_log.empty?
faildesc << "\n" if /\n\z/ !~ faildesc
stderr_log << "\n" if /\n\z/ !~ stderr_log
stderr_log.gsub!(/^.*\n/) { '| ' + $& }
faildesc << stderr_log
end
end
end
faildesc
}
end
def assert_finish(timeout_seconds, testsrc, message = '')
if RubyVM.const_defined? :MJIT
timeout_seconds *= 3 if RubyVM::MJIT.enabled? # for --jit-wait
end
newtest
show_progress(message) {
faildesc = nil
filename = make_srcfile(testsrc)
io = IO.popen("#{@ruby} -W0 #{filename}")
pid = io.pid
waited = false
tlimit = Time.now + timeout_seconds
diff = timeout_seconds
while diff > 0
if Process.waitpid pid, Process::WNOHANG
waited = true
break
end
if io.respond_to?(:read_nonblock)
if IO.select([io], nil, nil, diff)
begin
io.read_nonblock(1024)
rescue Errno::EAGAIN, IO::WaitReadable, EOFError
break
end while true
end
else
sleep 0.1
end
diff = tlimit - Time.now
end
if !waited
Process.kill(:KILL, pid)
Process.waitpid pid
faildesc = pretty(testsrc, "not finished in #{timeout_seconds} seconds", nil)
end
io.close
faildesc
}
end
def flunk(message = '')
newtest
show_progress('') { message }
end
def pretty(src, desc, result)
src = src.sub(/\A\s*\n/, '')
(/\n/ =~ src ? "\n#{adjust_indent(src)}" : src) + " #=> #{desc}"
end
INDENT = 27
def adjust_indent(src)
untabify(src).gsub(/^ {#{INDENT}}/o, '').gsub(/^/, ' ').sub(/\s*\z/, "\n")
end
def untabify(str)
str.gsub(/^\t+/) {' ' * (8 * $&.size) }
end
def make_srcfile(src, frozen_string_literal: nil)
filename = 'bootstraptest.tmp.rb'
File.open(filename, 'w') {|f|
f.puts "#frozen_string_literal:true" if frozen_string_literal
f.puts "GC.stress = true" if $stress
f.puts "print(begin; #{src}; end)"
}
filename
end
def get_result_string(src, opt = '', **argh)
if @ruby
filename = make_srcfile(src, **argh)
begin
`#{@ruby} -W0 #{opt} #{filename}`
ensure
raise Interrupt if $? and $?.signaled? && $?.termsig == Signal.list["INT"]
end
else
eval(src).to_s
end
end
def with_stderr
out = err = nil
begin
r, w = IO.pipe
stderr = $stderr.dup
$stderr.reopen(w)
w.close
reader = Thread.start {r.read}
begin
out = yield
ensure
$stderr.reopen(stderr)
err = reader.value
end
ensure
w.close rescue nil
r.close rescue nil
end
return out, err
end
def newtest
@location = File.basename(caller(2).first)
@count += 1
#cleanup_coredump
end
def error(msg, additional_message, backtrace = nil)
msg = "#{@failed}\##{@count} #{@location}#{@reset}: #{msg} #{additional_message}"
msg << backtrace.join("\n") if backtrace
if @tty
$stderr.puts "#{erase}#{msg}"
else
@errbuf.push msg
end
@error += 1
end
def in_temporary_working_directory(dir)
if dir
Dir.mkdir dir
Dir.chdir(dir) {
yield
}
else
Dir.mktmpdir(["bootstraptest", ".tmpwd"]) {|d|
Dir.chdir(d) {
yield
}
}
end
end
def cleanup_coredump
FileUtils.rm_f 'core'
FileUtils.rm_f Dir.glob('core.*')
FileUtils.rm_f @ruby+'.stackdump' if @ruby
end
class CoreDumpError < StandardError; end
def check_coredump
# if File.file?('core') or not Dir.glob('core.*').empty? or
# (@ruby and File.exist?(@ruby+'.stackdump'))
# raise CoreDumpError, "core dumped"
# end
end
main

View File

@ -0,0 +1,36 @@
require './test/mri_runner'
if Ractor.respond_to? :ractor_reset
SKIP = [
'touching moved object causes an error',
'move example2: Array',
'move with yield',
'Access to global-variables are prohibited',
'$stdin,out,err is Ractor local, but shared fds',
'given block Proc will be isolated, so can not access outer variables.',
'ivar in shareable-objects are not allowed to access from non-main Ractor',
'cvar in shareable-objects are not allowed to access from non-main Ractor',
'Getting non-shareable objects via constants by other Ractors is not allowed',
'Setting non-shareable objects into constants by other Ractors is not allowed',
'define_method is not allowed',
'ObjectSpace.each_object can not handle unshareable objects with Ractors',
'ObjectSpace._id2ref can not handle unshareable objects with Ractors',
'ivar in shareable-objects are not allowed to access from non-main Ractor, by @iv (set)',
'ivar in shareable-objects are not allowed to access from non-main Ractor, by @iv (get)',
'Can not trap with not isolated Proc on non-main ractor',
'Ractor.make_shareable(a_proc) makes a proc shareable',
"define_method() can invoke different Ractor's proc if the proc is shareable.", # :-(
'Ractor.make_shareable(a_proc) makes a proc shareable.',
#*('Ractor.count' if RUBY_VERSION < '2.2')
].freeze
alias show_progress_org show_progress
def show_progress(m='', &b)
_path, line = @location.split(':')
comment = File.read(PATH).lines[line.to_i-2][2...-1]
show_progress_org(m, &b) unless SKIP.include?(comment)
Ractor.ractor_reset
end
end

59
test/ractor_extra_test.rb Normal file
View File

@ -0,0 +1,59 @@
require './test/test_helper'
require './lib/backports/3.0.0/ractor.rb'
class ExtraRactorTest < Test::Unit::TestCase
def assert_shareable(*objects)
check_shareability(objects, true)
end
def assert_not_shareable(*objects, &block)
check_shareability(objects, false)
end
def check_shareability(objects, shareable)
objects.each do |obj|
assert_equal shareable, obj.object_id == Ractor.new(obj, &:object_id).take
assert_equal shareable, obj.object_id == Ractor.new([obj]) { |x, | x.object_id }.take
assert_equal obj.frozen?, Ractor.new(obj, &:frozen?).take
assert_equal obj.frozen?, Ractor.new([obj]) { |x, | x.frozen? } .take
assert_equal shareable, Ractor.shareable?(obj)
end
end
def test_copy_only_copies_when_needed
r = Ractor.new {}
assert_shareable(r, 42, 4.2, 2..4, false, true, nil, 'abc'.freeze)
assert_not_shareable(+'abc', [], {})
a = []; b = [a].freeze; c = [a].freeze; a << c
assert_not_shareable(a, b, c)
a.freeze
assert_shareable(a, b, c)
h = {a: 1, b: 2}
assert_not_shareable(h)
assert_shareable(h.freeze)
h = {a: [1, 2, 3], b: 2}
assert_not_shareable(h)
assert_not_shareable(h.freeze)
h[:a].freeze
assert_shareable(h)
end
def make_shareable_fail
o = Object.new
def o.freeze; self; end
assert_raise(Ractor::Error) { Ractor.make_shareable(o) }
end
def test_main
assert_same(Ractor.current, Ractor.main)
r = Ractor.new { [Ractor.main, Ractor.current] }
main, current = r.take
assert_same(r, current)
assert_same(main, Ractor.main)
end
end

14
test/ractor_test.rb Normal file
View File

@ -0,0 +1,14 @@
if RUBY_VERSION < '3'
require './lib/backports/3.0.0/ractor.rb'
require './lib/backports/2.3.0/string/uminus.rb'
PATH = './test/test_ractor.rb'
require './test/mri_runner_patched.rb'
nil.freeze
true.freeze
false.freeze
exec_test [PATH]
end

1272
test/test_ractor.rb Normal file

File diff suppressed because it is too large Load Diff