diff --git a/NEWS.md b/NEWS.md index 1e65ff9bcba20e..318a7e37ad6475 100644 --- a/NEWS.md +++ b/NEWS.md @@ -273,6 +273,21 @@ Ruby 4.0 bundled RubyGems and Bundler version 4. see the following links for det ## Compatibility issues +* A class or module can now be modified only by the Ractor which created it, + its *owner*. Defining, removing or undefining methods, `alias`, changing + visibility, `include`/`prepend`, `Module#refine`, defining or removing + constants, registering an `autoload`, writing the class's own instance + variables and class variables, `Module#freeze` and + `Module#set_temporary_name` raise `Ractor::IsolationError` in any other + Ractor. Reading is unchanged. Everything defined at boot or by the main + Ractor, `require`d libraries included, is owned by the main Ractor, so a + non-main Ractor can no longer monkey-patch it; and since defining a constant + in a foreign class is prohibited, it can not define a top-level class or + module name either. In exchange a Ractor has full use of the classes it + creates itself, including unshareable constant, instance variable and class + variable values, which not even the main Ractor could do before. See + doc/language/ractor.md. [[Feature #22226]] + * `Kernel#at_exit` and `END {}` now raise `Ractor::IsolationError` when called in a non-main Ractor. Previously the registered handler ran in the main Ractor at process exit, which was confusing. [[Feature #22139]] @@ -428,6 +443,7 @@ A lot of work has gone into making Ractors more stable, performant, and usable. [Feature #22175]: https://bugs.ruby-lang.org/issues/22175 [Feature #22185]: https://bugs.ruby-lang.org/issues/22185 [Feature #22205]: https://bugs.ruby-lang.org/issues/22205 +[Feature #22226]: https://bugs.ruby-lang.org/issues/22226 [Feature #22238]: https://bugs.ruby-lang.org/issues/22238 [PR #17201]: https://github.com/ruby/ruby/pull/17201 [GH-psych #805]: https://github.com/ruby/psych/pull/805 diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb index 3fb76118aea3c4..4d1b8d924ba8d2 100644 --- a/bootstraptest/test_ractor.rb +++ b/bootstraptest/test_ractor.rb @@ -869,8 +869,8 @@ def ractor_local_globals 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 +# ivar in shareable-objects are not allowed to access from non-owner Ractor +assert_equal "can not get unshareable values from instance variables of classes/modules created by another Ractor (@iv from C)", <<~'RUBY', frozen_string_literal: false class C @iv = 'str' end @@ -1045,8 +1045,8 @@ def initialize 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{ +# Reading non-shareable cvar of a class created by another Ractor is not allowed +assert_equal 'can not read non-shareable class variable @@cv of C, which was created by another Ractor', %q{ class C @@cv = 'str' end @@ -1064,8 +1064,8 @@ class C 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{ +# also cached non-shareable cvar read of a foreign class is not allowed +assert_equal 'can not read non-shareable class variable @@cv of C, which was created by another Ractor', %q{ class C @@cv = 'str' def self.cv @@ -1129,8 +1129,8 @@ def self.cv 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{ +# Writing a cvar of a class created by another Ractor is not allowed +assert_equal 'can not set class variable @@cv of C, which was created by another Ractor', %q{ class C @@cv = 'str' def self.cv=(v) @@ -1175,8 +1175,301 @@ def self.cv? r.value } +# A shareable object belongs to no single Ractor, so its singleton class is the +# main Ractor's rather than the one which materialized it +assert_equal 'true', %q{ + r = Ractor.new do + begin + Ractor.main.define_singleton_method(:zzz) { :sub } + :not_raised + rescue Ractor::IsolationError + :raised + end + end + raised = r.value + Ractor.main.define_singleton_method(:zzz) { :main } + (raised == :raised && Ractor.main.zzz == :main).to_s +} + +# The constant inline cache is keyed on the Ractor which filled it, so a non-owner +# can not be handed an unshareable constant through a cache the owner primed +assert_equal 'Ractor::IsolationError', %q{ + port = Ractor::Port.new + r = Ractor.new(port) do |port| + k = Class.new + k.const_set(:X, [1, 2, 3]) + m = Module.new + m.const_set(:K, k) + m.module_eval("def self.rd = K::X") + m.rd # the owner fills the cache + port << m + Ractor.receive + end + m = port.receive + res = begin + m.rd + 'no error' + rescue Ractor::IsolationError + 'Ractor::IsolationError' + end + r << nil + r.join + res +} + +# Class#initialize writes the superclass of an uninitialized class, so it is +# owner-only like any other modification +assert_equal 'can not modify K because it is created by another Ractor', %q{ + K = Class.allocate + + r = Ractor.new { K.send(:initialize, Struct) } + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# Module#refine writes its refinement tables into the receiver, so the receiver +# must be owned too +assert_equal 'can not modify M because it is created by another Ractor', %q{ + module M; end + + r = Ractor.new do + M.send(:refine, Class.new) { def z = 1 } + end + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# ... and so does Module#ruby2_keywords, which rewrites a method definition +assert_equal 'can not modify M because it is created by another Ractor', %q{ + module M + def m(*a) = a + end + + r = Ractor.new do + M.send(:ruby2_keywords, :m) + end + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# Changing method visibility on a class created by another Ractor is not allowed +assert_equal 'can not modify C because it is created by another Ractor', %q{ + class C + def m; end + end + + r = Ractor.new do + C.send(:private, :m) + end + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# ... and neither is module_function with a method name +assert_equal 'can not modify M because it is created by another Ractor', %q{ + module M + def m; end + end + + r = Ractor.new do + M.send(:module_function, :m) + end + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# Visibility changes on a class the Ractor created itself are allowed +assert_equal 'true', %q{ + Ractor.new do + k = Class.new { def m; end } + k.send(:private, :m) + k.private_method_defined?(:m).to_s + end.value +} + +# A moved object takes its singleton class's ownership with it, so the receiver +# can keep defining singleton methods on it +assert_equal '[:from_main, :from_ractor]', %q{ + o = Object.new + def o.foo = :from_main # materializes the singleton class here, in the main Ractor + + r = Ractor.new do + x = Ractor.receive + def x.bar = :from_ractor + [x.foo, x.bar] + end + r.send(o, move: true) + r.value.inspect +} + +# ... the whole eigenclass chain goes with it, so `class << obj.singleton_class` +# keeps working for the receiver +assert_equal 'true', %q{ + o = Object.new + def o.foo = :main + o.singleton_class.singleton_class # materialize it here, in the main Ractor + + port = Ractor::Port.new + r = Ractor.new(port) do |port| + x = Ractor.receive + x.singleton_class.singleton_class.define_method(:mm) { :sub } + port << x.singleton_class.mm + end + r.send(o, move: true) + res = port.receive + r.join + (res == :sub).to_s +} + +# ... but the move is refused when that singleton class holds unshareable values, +# which the sender would keep while the receiver became their owner +assert_equal 'can not move an object whose singleton class has variable @iv referring to an unshareable object', <<~'RUBY', frozen_string_literal: false + o = Object.new + o.singleton_class.instance_variable_set(:@iv, 'sender') + + r = Ractor.new { Ractor.receive } + msg = begin + r.send(o, move: true) + 'no error' + rescue Ractor::IsolationError => e + e.message + end + r.send(1) + r.join + msg + RUBY + +# ... and it comes back when the object is moved back +assert_equal '[:from_main, :from_ractor, :from_main_again]', %q{ + o = Object.new + def o.foo = :from_main + + port = Ractor::Port.new + r = Ractor.new(port) do |port| + x = Ractor.receive + def x.bar = :from_ractor + port.send(x, move: true) + end + r.send(o, move: true) + + back = port.receive + def back.baz = :from_main_again + [back.foo, back.bar, back.baz].inspect +} + +# A Ractor has full use of the cvars of a class it created, unshareable values included +assert_equal 'not shareable', %q{ + Ractor.new do + k = Class.new + k.class_variable_set(:@@cv, +'not shareable') + k.class_variable_get(:@@cv) + end.value +} + +# The owner is the owner of the class the cvar is stored in, not of the receiver +assert_equal 'can not set class variable @@cv of C, which was created by another Ractor', %q{ + class C + @@cv = 1 + end + + r = Ractor.new do + # a subclass created by this Ractor, but @@cv lives in C + Class.new(C).class_variable_set(:@@cv, 2) + end + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# Removing a cvar of a class created by another Ractor is not allowed +assert_equal 'can not set class variable @@cv of C, which was created by another Ractor', %q{ + class C + @@cv = 1 + end + + r = Ractor.new do + C.send(:remove_class_variable, :@@cv) + end + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# Freezing a class created by another Ractor is not allowed +assert_equal 'can not modify String because it is created by another Ractor', %q{ + r = Ractor.new do + String.freeze + end + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# Freezing a class the Ractor created itself is allowed, and freezing an +# already frozen class stays a no-op for everybody +assert_equal 'true true', %q{ + FROZEN = Class.new.freeze + + Ractor.new do + own = Class.new + own.freeze + "#{own.frozen?} #{FROZEN.freeze.frozen?}" + end.value +} + +# set_temporary_name on a class created by another Ractor is not allowed +assert_equal 'can not modify C because it is created by another Ractor', %q{ + class C; end + + r = Ractor.new do + C.set_temporary_name('other') + end + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end +} + +# set_temporary_name on a module the Ractor created itself is allowed +assert_equal 'mine', %q{ + Ractor.new do + Module.new.set_temporary_name('mine').name + end.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 +assert_equal 'can not access non-shareable objects in constant C::CONST of a class/module created by another Ractor.', <<~'RUBY', frozen_string_literal: false class C CONST = 'str' end @@ -1191,7 +1484,7 @@ class C 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 +assert_equal "can not access non-shareable objects in constant Object::STR of a class/module created by another Ractor.", <<~'RUBY', frozen_string_literal: false STR = "hello" def str; STR; end s = str() # fill const cache @@ -1203,7 +1496,7 @@ def str; STR; 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 +assert_equal "can not access non-shareable objects in constant Object::STR of a class/module created by another Ractor.", <<~'RUBY', frozen_string_literal: false STR = "hello" module M def self.str; STR; end @@ -1216,8 +1509,67 @@ def self.str; STR; end 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 +# Copying a class/module created by another Ractor raises if its constants or +# fields refer to unshareable objects +assert_equal 'can not copy a class/module created by another Ractor because constant CONST refers to an unshareable object', <<~'RUBY', frozen_string_literal: false + class C + CONST = 'str' + end + + r = Ractor.new { C.dup } + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end + RUBY + +assert_equal 'can not copy a class/module created by another Ractor because variable @iv refers to an unshareable object', <<~'RUBY', frozen_string_literal: false + class C + @iv = 'str' + end + + r = Ractor.new { C.dup } + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end + RUBY + +# ... the metaclass is checked too, because the copy carries it over +assert_equal 'can not copy a class/module created by another Ractor because variable @secret refers to an unshareable object', <<~'RUBY', frozen_string_literal: false + class C; end + C.singleton_class.instance_variable_set(:@secret, 'str') + + r = Ractor.new { C.dup } + + begin + r.join + rescue Ractor::RemoteError => e + e.cause.message + end + RUBY + +# ... and the copy of a class which refers to nothing unshareable belongs to the +# copying Ractor, which is a way to modify a foreign class without mutating it +assert_equal 'copy orig', %q{ + class C + CONST = 1 + def m = 'orig' + end + + Ractor.new do + k = C.dup + k.class_eval { def m = 'copy' } + "#{k.new.m} #{C.new.m}" + end.value +} + +# Setting constants of classes created by other Ractors is not allowed +assert_equal 'can not set constants of classes/modules created by another Ractor', <<~'RUBY', frozen_string_literal: false class C end r = Ractor.new do @@ -1733,19 +2085,12 @@ class C8; def self.foo = 17; end } # check method cache invalidation +# (the owner Ractor redefines methods while another Ractor calls them) 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 @@ -1753,7 +2098,12 @@ def hello = nil end end - r1.value + 1000.times do + class Foo + def hello = nil + end + end + r2.value true @@ -2603,21 +2953,23 @@ def call_test(obj) assert_equal 'ok', <<~'RUBY' begin - CLASSES = 1000.times.map { Class.new }.freeze - + # Each Ractor creates its own class (it can only define bmethods on classes + # it owns) and returns it after defining the bmethod. # 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| + ractors = 1000.times.map do + Ractor.new do + klass = Class.new Ractor.receive klass.define_method(:foo) {} + klass end end - ractors.each do |ractor| + CLASSES = ractors.map do |ractor| ractor << nil - ractor.join - end + ractor.value + end.freeze ractors.clear GC.start diff --git a/class.c b/class.c index 344048c0310514..7c6d9e3be49ea8 100644 --- a/class.c +++ b/class.c @@ -33,6 +33,7 @@ #include "ruby/st.h" #include "vm_core.h" #include "ruby/ractor.h" +#include "ractor_core.h" #include "yjit.h" #include "zjit.h" @@ -601,6 +602,12 @@ class_alloc0(enum ruby_value_type type, VALUE klass, bool boxable) memset(RCLASS_EXT_PRIME(obj), 0, sizeof(rb_classext_t)); + // The creating Ractor owns the new class/module; an iclass has no owner. + // Singleton classes of classes/modules override this below. + if (type != T_ICLASS && UNLIKELY(!rb_ractor_main_p())) { + RCLASS_SET_OWNER_RACTOR_ID((VALUE)obj, rb_ractor_id(GET_RACTOR())); + } + /* ZALLOC RCLASS_CONST_TBL(obj) = 0; RCLASS_M_TBL(obj) = 0; @@ -630,6 +637,56 @@ class_alloc(enum ruby_value_type type, VALUE klass) return class_alloc0(type, klass, boxable); } +bool +rb_class_owned_by_ractor_p(rb_serial_t owner_id) +{ + return owner_id == rb_ractor_id(GET_RACTOR()); +} + +/* A singleton class has no class path of its own, so name it by the object it + * belongs to, as rb_class_modify_check does for a frozen one. No dispatch: the + * object belongs to another Ractor. */ +static VALUE +class_owner_name(VALUE klass) +{ + if (!RCLASS_SINGLETON_P(klass)) return rb_class_path(klass); + + VALUE obj = RCLASS_ATTACHED_OBJECT(klass); + return (RB_TYPE_P(obj, T_CLASS) || RB_TYPE_P(obj, T_MODULE)) ? rb_class_path(obj) : rb_any_to_s(obj); +} + +/* The eigenclass of klass, if it has one of its own, else 0. */ +static VALUE +class_own_metaclass(VALUE klass) +{ + VALUE meta = METACLASS_OF(klass); + return (RCLASS_SINGLETON_P(meta) && RCLASS_ATTACHED_OBJECT(meta) == klass) ? meta : 0; +} + +/* Only for a singleton class whose attached object has just changed hands through + * Ractor#send(move: true). Deliberately unreachable from Ruby. */ +void +rb_class_take_ownership(VALUE klass) +{ + // keep class_alloc0's "0 means main" encoding + rb_serial_t id = rb_ractor_main_p() ? 0 : rb_ractor_id(GET_RACTOR()); + + // and up the eigenclass chain: each one belongs to the class below it + do { + RCLASS_SET_OWNER_RACTOR_ID(klass, id); + } while ((klass = class_own_metaclass(klass)) != 0); +} + +void +rb_class_owner_check(VALUE klass) +{ + if (UNLIKELY(!rb_class_owned_p(klass))) { + rb_raise(rb_eRactorIsolationError, + "can not modify %"PRIsVALUE" because it is created by another Ractor", + class_owner_name(klass)); + } +} + static VALUE class_associate_super(VALUE klass, VALUE super, bool init) { @@ -942,6 +999,90 @@ rb_module_check_initializable(VALUE mod) } } +static enum rb_id_table_iterator_result +init_copy_check_const_i(ID id, VALUE v, void *data) +{ + const rb_const_entry_t *ce = (const rb_const_entry_t *)v; + if (!UNDEF_P(ce->value) && !rb_ractor_shareable_p(ce->value)) { + rb_raise(rb_eRactorIsolationError, + "can not copy a class/module created by another Ractor because " + "constant %"PRIsVALUE" refers to an unshareable object", rb_id2str(id)); + } + return ID_TABLE_CONTINUE; +} + +static int +init_copy_check_field_i(ID id, VALUE val, st_data_t arg) +{ + if ((rb_is_instance_id(id) || rb_is_class_id(id)) && !rb_ractor_shareable_p(val)) { + rb_raise(rb_eRactorIsolationError, + "can not copy a class/module created by another Ractor because " + "variable %"PRIsVALUE" refers to an unshareable object", rb_id2str(id)); + } + return ST_CONTINUE; +} + +// The copy belongs to the copying Ractor, so it must not carry over unshareable +// objects owned by the source's Ractor. +static enum rb_id_table_iterator_result +move_check_const_i(ID id, VALUE v, void *data) +{ + const rb_const_entry_t *ce = (const rb_const_entry_t *)v; + if (!UNDEF_P(ce->value) && !rb_ractor_shareable_p(ce->value)) { + rb_raise(rb_eRactorIsolationError, + "can not move an object whose singleton class has constant %"PRIsVALUE + " referring to an unshareable object", rb_id2str(id)); + } + return ID_TABLE_CONTINUE; +} + +static int +move_check_field_i(ID id, VALUE val, st_data_t arg) +{ + if ((rb_is_instance_id(id) || rb_is_class_id(id)) && !rb_ractor_shareable_p(val)) { + rb_raise(rb_eRactorIsolationError, + "can not move an object whose singleton class has variable %"PRIsVALUE + " referring to an unshareable object", rb_id2str(id)); + } + return ST_CONTINUE; +} + +/* The receiver of a moved object becomes the owner of its singleton class + * (rb_class_take_ownership), so nothing the sender keeps may stay readable there. */ +void +rb_class_check_singleton_movable(VALUE klass) +{ + do { + if (RCLASS_CONST_TBL(klass)) { + rb_id_table_foreach(RCLASS_CONST_TBL(klass), move_check_const_i, NULL); + } + rb_ivar_foreach_buffered(klass, move_check_field_i, 0); + } while ((klass = class_own_metaclass(klass)) != 0); +} + +static void +init_copy_check_tables(VALUE klass) +{ + if (RCLASS_CONST_TBL(klass)) { + rb_id_table_foreach(RCLASS_CONST_TBL(klass), init_copy_check_const_i, NULL); + } + rb_ivar_foreach_buffered(klass, init_copy_check_field_i, 0); +} + +static void +init_copy_owner_check(VALUE orig) +{ + if (!rb_class_owned_p(orig)) { + init_copy_check_tables(orig); + + // rb_singleton_class_clone_and_attach copies the metaclass's tables too + VALUE meta = METACLASS_OF(orig); + if (RCLASS_SINGLETON_P(meta)) { + init_copy_check_tables(meta); + } + } +} + /* :nodoc: */ VALUE rb_mod_init_copy(VALUE clone, VALUE orig) @@ -964,6 +1105,8 @@ rb_mod_init_copy(VALUE clone, VALUE orig) RUBY_ASSERT(RB_TYPE_P(orig, T_CLASS) || RB_TYPE_P(orig, T_MODULE)); RUBY_ASSERT(BUILTIN_TYPE(clone) == BUILTIN_TYPE(orig)); + init_copy_owner_check(orig); + rb_class_set_initialized(clone); if (!RCLASS_SINGLETON_P(CLASS_OF(clone))) { @@ -1189,6 +1332,8 @@ make_metaclass(VALUE klass) VALUE metaclass = class_boot_boxable(Qundef, FL_TEST_RAW(klass, RCLASS_BOXABLE)); FL_SET(metaclass, FL_SINGLETON); + // owned by the attached class's owner, not by whoever triggered the lazy creation + RCLASS_SET_OWNER_RACTOR_ID(metaclass, RCLASS_OWNER_RACTOR_ID(klass)); rb_singleton_class_attached(metaclass, klass); if (META_CLASS_OF_CLASS_CLASS_P(klass)) { @@ -1224,6 +1369,15 @@ make_singleton_class(VALUE obj) VALUE orig_class = METACLASS_OF(obj); VALUE klass = class_alloc0(T_CLASS, rb_cClass, FL_TEST_RAW(orig_class, RCLASS_BOXABLE)); FL_SET(klass, FL_SINGLETON); + if (RB_TYPE_P(obj, T_MODULE)) { + // as in make_metaclass: the module's owner, not the lazy creator + RCLASS_SET_OWNER_RACTOR_ID(klass, RCLASS_OWNER_RACTOR_ID(obj)); + } + else if (rb_ractor_shareable_p(obj)) { + // A shareable object is no single Ractor's, so its singleton class stays the + // main Ractor's rather than being claimed by whoever materialized it. + RCLASS_SET_OWNER_RACTOR_ID(klass, 0); + } class_initialize_method_table(klass); class_associate_super(klass, orig_class, true); if (orig_class && !UNDEF_P(orig_class)) { diff --git a/doc/language/ractor.md b/doc/language/ractor.md index 059d9df76827b7..572bad8c3c80eb 100644 --- a/doc/language/ractor.md +++ b/doc/language/ractor.md @@ -345,6 +345,60 @@ To isolate unshareable objects across ractors, we introduced additional language Note that when not using ractors, these additional semantics are not needed (100% compatible with Ruby 2). +### Class and module ownership + +Every class/module records the Ractor that created it as its *owner*. Only the owner Ractor can modify the class/module: + +* defining, removing or undefining methods, `alias`, and changing method visibility +* `include`/`prepend` into the class/module, and refining it with `Module#refine` +* defining or removing constants, and registering `autoload` +* setting instance variables of the class/module object +* setting and removing class variables stored in the class/module +* `Module#freeze` and `Module#set_temporary_name` + +Reading (calling methods, instantiating, reading constants and instance variables, subclassing, and so on) is allowed from any Ractor as before. + +Ownership bounds *who may write* a class/module, not *what readers may see*. Reads are not synchronized and a class modification is not atomic, so a non-owner Ractor can still observe a class/module while its owner is modifying it: after the first `def` of a `class ... end` body but before the second, or while `include`/`prepend` is rewiring the ancestor chain. What ownership guarantees is a single writer per class/module, not a consistent view for readers. + +That single writer is not always the class's own owner. `M.include(N)` and `M.prepend(N)` rewire the ancestor chain of every class that already includes `M`, whoever owns those classes: modifying `M` is `M`'s owner's right, and a class which includes a module created by another Ractor accepts that. So a class is written by its own owner and by the owners of the modules it includes. + +All classes/modules defined at boot or by code run by the main Ractor (including `require`d libraries) are owned by the main Ractor, so non-main ractors can not monkey-patch them: + +```ruby +r = Ractor.new do + class String # reopening itself is harmless, but... + def foo; end # ...defining a method on a class created by + end # another Ractor raises +end +begin + r.join +rescue Ractor::RemoteError => e + e.cause.message #=> "can not modify String because it is created by another Ractor" +end +``` + +In exchange, a Ractor can fully use the classes/modules it created itself, including things which were previously allowed only on the main Ractor: + +```ruby +Ractor.new do + k = Class.new do + def hello = "hello" + end + k.const_set(:CONST, [1, 2, 3]) # even unshareable constant values + k.instance_variable_set(:@iv, [4, 5]) # even unshareable ivar values + k.new.hello +end.value #=> "hello" +``` + +Notes: + +* A singleton class (and a metaclass) is owned by the owner of the object it is attached to, not by the Ractor which happened to trigger its lazy creation. So `def C.foo` is allowed exactly for the owner of `C`. A shareable object belongs to no single Ractor, so the singleton class of one is owned by the main Ractor. +* Classes/modules whose owner Ractor has terminated become permanently read-only for every Ractor. +* Since defining a constant in a class/module created by another Ractor is prohibited, a non-main Ractor can not define a top-level class name (it would write a constant into `Object`). Define classes under your own namespace instead: `m = Module.new; m.const_set(:Foo, Class.new)`. +* Copying a class/module created by another Ractor with `Class#dup`/`Object#clone` creates a copy owned by the copying Ractor; it raises `Ractor::IsolationError` if the source's constants or instance variables refer to unshareable objects. +* `Ractor#send(obj, move: true)` hands a materialized singleton class over with its object, so the receiver can go on defining singleton methods on it. The move is refused if that singleton class holds unshareable constants or variables, which would stay reachable by the sender. +* `require` runs on the main Ractor whichever Ractor calls it, so a library defines its classes as the main Ractor's. `load` does not: loading a file which defines a top-level name from a non-main Ractor raises `Ractor::IsolationError`, like writing that constant directly. + ### Global variables Only the main Ractor can access global variables. @@ -366,7 +420,7 @@ Note that some special global variables, such as `$stdin`, `$stdout` and `$stder ### Instance variables of shareable objects -Instance variables of classes/modules can be accessed from non-main ractors only if their values are shareable objects. +Instance variables of classes/modules can be accessed from non-owner ractors only if their values are shareable objects. ```ruby class C @@ -380,7 +434,7 @@ p Ractor.new do end.value #=> 1 ``` -Otherwise, only the main Ractor can access instance variables of shareable objects. +Otherwise, only the owner Ractor can access instance variables of classes/modules. Setting them is prohibited for non-owner ractors regardless of the value. ```ruby class C @@ -393,14 +447,14 @@ Ractor.new do p @iv rescue Ractor::IsolationError p $!.message - #=> "can not get unshareable values from instance variables of classes/modules from non-main Ractors" + #=> "can not get unshareable values from instance variables of classes/modules created by another Ractor (@iv from C)" end begin @iv = 42 rescue Ractor::IsolationError p $!.message - #=> "can not set instance variables of classes/modules by non-main Ractors" + #=> "can not set instance variables of classes/modules created by another Ractor" end end end.join @@ -423,30 +477,38 @@ end ### Class variables -Only the main Ractor can access class variables. +A class variable is shared across the whole inheritance chain, and the class it is actually stored in can change over time (a subclass's definition can be taken over by an ancestor). The Ractor that decides access is therefore the owner of the class the variable is *stored in*, not of the receiver it was looked up through. Only that Ractor can write the variable, and only that Ractor can read a value which is not shareable. ```ruby class C - @@cv = 'str' + @@cv = 'str' # unshareable object end -r = Ractor.new do +Ractor.new do class C - p @@cv + begin + p @@cv # stored in C, which is owned by the main Ractor + rescue Ractor::IsolationError + p $!.message + #=> "can not read non-shareable class variable @@cv of C, which was created by another Ractor" + end end -end +end.join +``` +A Ractor has full use of the class variables of the classes it created itself: -begin - r.join -rescue => e - e.class #=> Ractor::IsolationError -end +```ruby +Ractor.new do + k = Class.new + k.class_variable_set(:@@count, 'not shareable') + k.class_variable_get(:@@count) +end.value #=> "not shareable" ``` ### Constants -Only the main Ractor can read constants which refer to an unshareable object. +Only the owner Ractor of the class/module the constant is defined in can read constants which refer to an unshareable object. ```ruby class C @@ -462,7 +524,7 @@ rescue => e end ``` -Only the main Ractor can define constants which refer to an unshareable object. +Defining constants in a class/module created by another Ractor is prohibited, regardless of the value. The owner can define constants with any values, but constants which refer to unshareable objects can only be read back by the owner. ```ruby class C diff --git a/eval.c b/eval.c index ddefe300f5da29..15a8cad2f4e193 100644 --- a/eval.c +++ b/eval.c @@ -456,6 +456,7 @@ rb_class_modify_check(VALUE klass) } rb_error_frozen_object(klass); } + rb_class_owner_check(klass); } NORETURN(static void rb_longjmp(rb_execution_context_t *, enum ruby_tag_type, volatile VALUE, VALUE)); @@ -1636,6 +1637,11 @@ rb_mod_refine(VALUE module, VALUE klass) ensure_class_or_module(klass); + // refine installs refined method entries into the target's method table, and + // rb_refinement_setup writes the refinement tables into the receiver + rb_class_owner_check(module); + rb_class_owner_check(klass); + rb_refinement_setup(&data, module, klass); rb_yield_refine_block(data.refinement, data.refinements); diff --git a/ext/erb/escape/escape.c b/ext/erb/escape/escape.c index 903e832f038749..e905bb54bf6d27 100644 --- a/ext/erb/escape/escape.c +++ b/ext/erb/escape/escape.c @@ -100,10 +100,7 @@ find_next_match_sse2(search_state *search) int next_match_offset = trailing_zeros(search->matches_bitmap); search->matches_bitmap >>= (next_match_offset + 1); search->cstr += next_match_offset; - if (search->cstr > search->end) { - search->cstr = search->end; - return false; - } + RUBY_ASSERT(search->cstr <= search->end); return true; } @@ -170,12 +167,16 @@ static inline uint32_t trailing_zeros64(uint64_t input) static inline bool find_next_match_neon(search_state *search) { - size_t next_match_offset = trailing_zeros64(search->matches_bitmap) / 4; - search->matches_bitmap >>= (next_match_offset + 1) * 4; - search->cstr += next_match_offset; - if (search->cstr > search->end) { - search->cstr = search->end; - return false; + uint32_t trailing_zeros = trailing_zeros64(search->matches_bitmap); + + // uint64_t >>= 64 is undefined behaviour + if (trailing_zeros >= 63) { + search->matches_bitmap = 0; + search->cstr += 15; + } + else { + search->matches_bitmap >>= (trailing_zeros + 1); + search->cstr += trailing_zeros / 4; } return true; } @@ -207,10 +208,10 @@ find_next_neon(search_state *search) const uint8x16_t matches = vorrq_u8(mask2, mask3); const uint8x8_t res = vshrn_n_u16(vreinterpretq_u16_u8(matches), 4); - const uint64_t bitmap = vget_lane_u64(vreinterpret_u64_u8(res), 0) & 0x8888888888888888ull; + const uint64_t bitmap = vget_lane_u64(vreinterpret_u64_u8(res), 0); if (bitmap) { - search->matches_bitmap = bitmap; + search->matches_bitmap = bitmap & 0x8888888888888888ull; return find_next_match_neon(search); } search->cstr += sizeof(uint8x16_t); diff --git a/gc.c b/gc.c index 71c5c163f0b0de..5187c748ee8483 100644 --- a/gc.c +++ b/gc.c @@ -5800,7 +5800,7 @@ rb_raw_obj_info_buitin_type(char *const buff, const size_t buff_size, const VALU else if (rb_ractor_p(obj)) { rb_ractor_t *r = (void *)DATA_PTR(obj); if (r) { - APPEND_F("r:%d", r->pub.id); + APPEND_F("r:%"PRI_SERIALT_PREFIX"u", r->pub.id); } } break; diff --git a/gc/default/default.c b/gc/default/default.c index df1e914f14ea07..ff2b353616ac11 100644 --- a/gc/default/default.c +++ b/gc/default/default.c @@ -2540,10 +2540,10 @@ static struct heap_page_body * page_pool_acquire(struct page_arena **arena_out) { struct heap_page_body *body = NULL; - bool need_reuse = false; if (HEAP_PAGE_ALLOC_USE_MMAP) { #ifdef HAVE_MMAP + bool need_reuse = false; rb_global_objspace_t *g = global_objspace; rb_native_mutex_lock(&g->page_pool.lock); @@ -8597,11 +8597,13 @@ current_thread_time(struct timespec *ts) #if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_THREAD_CPUTIME_ID) { static int try_clock_gettime = 1; - if (try_clock_gettime && clock_gettime(CLOCK_THREAD_CPUTIME_ID, ts) == 0) { - return true; - } - else { - try_clock_gettime = 0; + if (try_clock_gettime) { + if (clock_gettime(CLOCK_THREAD_CPUTIME_ID, ts) == 0) { + return true; + } + else { + try_clock_gettime = 0; + } } } #endif @@ -11292,11 +11294,13 @@ current_process_time(struct timespec *ts) #if defined(HAVE_CLOCK_GETTIME) && defined(CLOCK_PROCESS_CPUTIME_ID) { static int try_clock_gettime = 1; - if (try_clock_gettime && clock_gettime(CLOCK_PROCESS_CPUTIME_ID, ts) == 0) { - return true; - } - else { - try_clock_gettime = 0; + if (try_clock_gettime) { + if (clock_gettime(CLOCK_PROCESS_CPUTIME_ID, ts) == 0) { + return true; + } + else { + try_clock_gettime = 0; + } } } #endif diff --git a/internal/class.h b/internal/class.h index 0773dbad95a1af..1d608e38db2966 100644 --- a/internal/class.h +++ b/internal/class.h @@ -12,6 +12,7 @@ #include "id_table.h" /* for struct rb_id_table */ #include "internal/box.h" #include "internal/serial.h" /* for rb_serial_t */ +#include "ractor_core.h" /* for rb_ractor_main_p */ #include "internal/static_assert.h" #include "internal/variable.h" /* for rb_class_ivar_set */ #include "ruby/internal/stdbool.h" /* for bool */ @@ -65,6 +66,11 @@ struct rb_classext_struct { const VALUE includer; } iclass; } as; + /* Id of the Ractor that created this class/module; only that Ractor may modify + * it. 0 is the main Ractor, so a program that never leaves it stores nothing. + * Ids are never reused, so a terminated owner leaves the class read-only for + * everybody. Prime classext only; always 0 for T_ICLASS. */ + rb_serial_t owner_ractor_id; uint16_t superclass_depth; attr_index_t max_iv_count; uint8_t variation_count; @@ -115,6 +121,37 @@ static inline void RCLASS_SET_PRIME_CLASSEXT_WRITABLE(VALUE obj, bool writable); #define RCLASS_EXT_PRIME(c) (&((struct RClass_and_rb_classext_t*)(c))->classext) #define RCLASS_EXT_PRIME_P(ext, c) (&((struct RClass_and_rb_classext_t*)(c))->classext == ext) +// Class ownership. See rb_classext_struct::owner_ractor_id. +#define RCLASSEXT_OWNER_RACTOR_ID(ext) (ext->owner_ractor_id) + +static inline rb_serial_t +RCLASS_OWNER_RACTOR_ID(VALUE klass) +{ + return RCLASS_EXT_PRIME(klass)->owner_ractor_id; +} + +static inline void +RCLASS_SET_OWNER_RACTOR_ID(VALUE klass, rb_serial_t ractor_id) +{ + // not a VALUE: no write barrier, nothing for the GC to mark or move + RCLASS_EXT_PRIME(klass)->owner_ractor_id = ractor_id; +} + +bool rb_class_owned_by_ractor_p(rb_serial_t owner_id); // rb_class_owned_p's slow half +void rb_class_owner_check(VALUE klass); // raise Ractor::IsolationError unless rb_class_owned_p(klass) +void rb_class_take_ownership(VALUE klass); // for a moved object's singleton class only +void rb_class_check_singleton_movable(VALUE klass); // raise if it holds unshareable values + +// true if the current Ractor created klass. Inline because the class ivar and +// constant read paths take it on every access. +static inline bool +rb_class_owned_p(VALUE klass) +{ + rb_serial_t owner_id = RCLASS_OWNER_RACTOR_ID(klass); + if (LIKELY(!owner_id)) return rb_ractor_main_p(); // the only case single-Ractor programs take + return rb_class_owned_by_ractor_p(owner_id); +} + static inline rb_classext_t * RCLASS_EXT_READABLE_IN_BOX(VALUE obj, const rb_box_t *box); static inline rb_classext_t * RCLASS_EXT_READABLE(VALUE obj); static inline rb_classext_t * RCLASS_EXT_WRITABLE_IN_BOX(VALUE obj, const rb_box_t *box); diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index d7c61b3066e69e..15b92f3e18d2f6 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -319,7 +319,7 @@ def install method_option "source", type: :array, banner: "Update a specific source (and all gems associated with it)" method_option "force", type: :boolean, aliases: "--redownload", banner: "Force reinstalling every gem, even if already installed" method_option "ruby", type: :boolean, banner: "Update ruby specified in Gemfile.lock" - method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler" + method_option "bundler", type: :string, lazy_default: ">= #{Bundler::VERSION}", banner: "Update the locked version of bundler" method_option "patch", type: :boolean, banner: "Prefer updating only to next patch version" method_option "minor", type: :boolean, banner: "Prefer updating only to next minor version" method_option "major", type: :boolean, banner: "Prefer updating to next major version (default)" @@ -647,7 +647,7 @@ def inject(*) method_option "pre", type: :boolean, banner: "If updating, always choose the highest allowed version, regardless of prerelease status" method_option "strict", type: :boolean, banner: "If updating, do not allow any gem to be updated past latest --patch | --minor | --major" method_option "conservative", type: :boolean, banner: "If updating, use bundle install conservative update behavior and do not allow shared dependencies to be updated" - method_option "bundler", type: :string, lazy_default: "> 0.a", banner: "Update the locked version of bundler" + method_option "bundler", type: :string, lazy_default: ">= #{Bundler::VERSION}", banner: "Update the locked version of bundler" method_option "cooldown", type: :numeric, banner: "Only consider gem versions published at least N days ago. Use 0 to disable." def lock require_relative "cli/lock" diff --git a/lib/bundler/cli/update.rb b/lib/bundler/cli/update.rb index c722474f64d7fe..fe740be07076b8 100644 --- a/lib/bundler/cli/update.rb +++ b/lib/bundler/cli/update.rb @@ -13,7 +13,7 @@ def run update_bundler = options[:bundler] - Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler) if update_bundler + Bundler.self_manager.update_bundler_and_restart_with_it_if_needed(update_bundler, pre: options[:pre]) if update_bundler sources = Array(options[:source]) groups = Array(options[:group]).map(&:to_sym) diff --git a/lib/bundler/man/bundle-lock.1 b/lib/bundler/man/bundle-lock.1 index f060fb1a3ad275..3f0f623bce963b 100644 --- a/lib/bundler/man/bundle-lock.1 +++ b/lib/bundler/man/bundle-lock.1 @@ -13,7 +13,7 @@ Lock the gems specified in Gemfile\. Ignores the existing lockfile\. Resolve then updates lockfile\. Taking a list of gems or updating all gems if no list is given\. .TP \fB\-\-bundler[=BUNDLER]\fR -Update the locked version of bundler to the given version or the latest version if no version is given\. +Update the locked version of bundler\. BUNDLER can be a version such as \fB4\.0\.20\fR, or a requirement such as \fB"> 0\.a"\fR\. With no argument, update to the latest released version, which never selects a prerelease\. .TP \fB\-\-local\fR Do not attempt to connect to \fBrubygems\.org\fR\. Instead, Bundler will use the gems already present in Rubygems' cache or in \fBvendor/cache\fR\. Note that if a appropriate platform\-specific gem exists on \fBrubygems\.org\fR it will not be found\. diff --git a/lib/bundler/man/bundle-lock.1.ronn b/lib/bundler/man/bundle-lock.1.ronn index df46ac39fc51ab..ecf477f475f6c6 100644 --- a/lib/bundler/man/bundle-lock.1.ronn +++ b/lib/bundler/man/bundle-lock.1.ronn @@ -33,8 +33,9 @@ Lock the gems specified in Gemfile. of gems or updating all gems if no list is given. * `--bundler[=BUNDLER]`: - Update the locked version of bundler to the given version or the latest - version if no version is given. + Update the locked version of bundler. BUNDLER can be a version such as + `4.0.20`, or a requirement such as `"> 0.a"`. With no argument, update to the + latest released version, which never selects a prerelease. * `--local`: Do not attempt to connect to `rubygems.org`. Instead, Bundler will use the diff --git a/lib/bundler/man/bundle-update.1 b/lib/bundler/man/bundle-update.1 index 9d5ea89c4d7111..2ff8401be300f7 100644 --- a/lib/bundler/man/bundle-update.1 +++ b/lib/bundler/man/bundle-update.1 @@ -27,7 +27,7 @@ Do not attempt to fetch gems remotely and use the gem cache instead\. Update the locked version of Ruby to the current version of Ruby\. .TP \fB\-\-bundler[=BUNDLER]\fR -Update the locked version of bundler to the invoked bundler version\. +Update the locked version of bundler\. BUNDLER can be a version such as \fB4\.0\.20\fR, or a requirement such as \fB"> 0\.a"\fR\. With no argument, update to the latest released version, which never selects a prerelease\. .TP \fB\-\-force\fR, \fB\-\-redownload\fR Force reinstalling every gem, even if already installed\. diff --git a/lib/bundler/man/bundle-update.1.ronn b/lib/bundler/man/bundle-update.1.ronn index 3ca4dc730a2f74..bb1913622c7153 100644 --- a/lib/bundler/man/bundle-update.1.ronn +++ b/lib/bundler/man/bundle-update.1.ronn @@ -53,7 +53,9 @@ gem. Update the locked version of Ruby to the current version of Ruby. * `--bundler[=BUNDLER]`: - Update the locked version of bundler to the invoked bundler version. + Update the locked version of bundler. BUNDLER can be a version such as + `4.0.20`, or a requirement such as `"> 0.a"`. With no argument, update to the + latest released version, which never selects a prerelease. * `--force`, `--redownload`: Force reinstalling every gem, even if already installed. diff --git a/lib/bundler/self_manager.rb b/lib/bundler/self_manager.rb index 4e6156ffa2f56d..aa948841ba5a8e 100644 --- a/lib/bundler/self_manager.rb +++ b/lib/bundler/self_manager.rb @@ -30,8 +30,8 @@ def install_locked_bundler_and_restart_with_it_if_needed install_and_restart_with(restart_version) end - def update_bundler_and_restart_with_it_if_needed(target) - spec = resolve_update_version_from(target) + def update_bundler_and_restart_with_it_if_needed(target, pre: false) + spec = resolve_update_version_from(target, pre: pre) return unless spec version = spec.version @@ -108,9 +108,9 @@ def autoswitching_applies? lockfile_version end - def resolve_update_version_from(target) + def resolve_update_version_from(target, pre: false) requirement = Gem::Requirement.new(target) - update_candidate = find_latest_matching_spec(requirement) + update_candidate = find_latest_matching_spec(requirement, pre: pre) if update_candidate.nil? raise InvalidOption, "The `bundle update --bundler` target version (#{target}) does not exist" @@ -137,18 +137,24 @@ def remote_specs end end - def find_latest_matching_spec(requirement) + def find_latest_matching_spec(requirement, pre: false) Bundler.configure - local_result = find_latest_matching_spec_from_collection(local_specs, requirement) + # A bare `bundle update --bundler` must stay on releases, like `gem update + # --system`, so only `--pre` or a prerelease requirement opts into one. + allow_prerelease = pre || requirement.prerelease? + + local_result = find_latest_matching_spec_from_collection(local_specs, requirement, allow_prerelease) return local_result if local_result && requirement.specific? - remote_result = find_latest_matching_spec_from_collection(remote_specs, requirement) + remote_result = find_latest_matching_spec_from_collection(remote_specs, requirement, allow_prerelease) return remote_result if local_result.nil? - [local_result, remote_result].max + [local_result, remote_result].compact.max end - def find_latest_matching_spec_from_collection(specs, requirement) + def find_latest_matching_spec_from_collection(specs, requirement, allow_prerelease) + specs = specs.reject {|spec| spec.version.prerelease? } unless allow_prerelease + specs.sort.reverse_each.find {|spec| requirement.satisfied_by?(spec.version) } end diff --git a/object.c b/object.c index 6327f4aadc420e..11b10b169566c3 100644 --- a/object.c +++ b/object.c @@ -1856,6 +1856,10 @@ rb_mod_to_s(VALUE klass) static VALUE rb_mod_freeze(VALUE mod) { + // a permanent modification; already frozen changes no state, so stays a no-op + if (!OBJ_FROZEN(mod)) { + rb_class_owner_check(mod); + } rb_class_name(mod); return rb_obj_freeze(mod); } @@ -2205,6 +2209,8 @@ rb_class_initialize(int argc, VALUE *argv, VALUE klass) { VALUE super; + // an uninitialized class is still somebody's, and this writes its superclass + rb_class_modify_check(klass); if (RCLASS_SUPER(klass) != 0 || klass == rb_cBasicObject) { rb_raise(rb_eTypeError, "already initialized class"); } diff --git a/ractor.c b/ractor.c index 3e610ccaaaa97d..4714ad94a2f16a 100644 --- a/ractor.c +++ b/ractor.c @@ -9,6 +9,7 @@ #include "vm_sync.h" #include "ractor_core.h" #include "internal/array.h" +#include "internal/class.h" #include "internal/complex.h" #include "internal/cont.h" #include "internal/error.h" @@ -79,7 +80,7 @@ ASSERT_ractor_locking(rb_ractor_t *r) static void ractor_lock(rb_ractor_t *r, const char *file, int line) { - RUBY_DEBUG_LOG2(file, line, "locking r:%u%s", r->pub.id, rb_current_ractor_raw(false) == r ? " (self)" : ""); + RUBY_DEBUG_LOG2(file, line, "locking r:%"PRI_SERIALT_PREFIX"u%s", r->pub.id, rb_current_ractor_raw(false) == r ? " (self)" : ""); ASSERT_ractor_unlocking(r); rb_native_mutex_lock(&r->sync.lock); @@ -98,7 +99,7 @@ ractor_lock(rb_ractor_t *r, const char *file, int line) } #endif - RUBY_DEBUG_LOG2(file, line, "locked r:%u%s", r->pub.id, rb_current_ractor_raw(false) == r ? " (self)" : ""); + RUBY_DEBUG_LOG2(file, line, "locked r:%"PRI_SERIALT_PREFIX"u%s", r->pub.id, rb_current_ractor_raw(false) == r ? " (self)" : ""); } static void @@ -128,7 +129,7 @@ ractor_unlock(rb_ractor_t *r, const char *file, int line) rb_native_mutex_unlock(&r->sync.lock); - RUBY_DEBUG_LOG2(file, line, "r:%u%s", r->pub.id, rb_current_ractor_raw(false) == r ? " (self)" : ""); + RUBY_DEBUG_LOG2(file, line, "r:%"PRI_SERIALT_PREFIX"u%s", r->pub.id, rb_current_ractor_raw(false) == r ? " (self)" : ""); } static void @@ -175,7 +176,7 @@ ractor_status_str(enum ractor_status status) static void ractor_status_set(rb_ractor_t *r, enum ractor_status status) { - RUBY_DEBUG_LOG("r:%u [%s]->[%s]", r->pub.id, ractor_status_str(r->status_), ractor_status_str(status)); + RUBY_DEBUG_LOG("r:%"PRI_SERIALT_PREFIX"u [%s]->[%s]", r->pub.id, ractor_status_str(r->status_), ractor_status_str(status)); // check 1 if (r->status_ != ractor_created) { @@ -424,7 +425,7 @@ static void ractor_free(void *ptr) { rb_ractor_t *r = (rb_ractor_t *)ptr; - RUBY_DEBUG_LOG("free r:%d", rb_ractor_id(r)); + RUBY_DEBUG_LOG("free r:%"PRI_SERIALT_PREFIX"u", rb_ractor_id(r)); free_targeted_hooks(&r->pub.targeted_hooks); rb_thread_sched_destroy(&r->threads.sched); @@ -520,26 +521,27 @@ RACTOR_PTR(VALUE self) } #define MAIN_RACTOR_ID 1 -static rb_atomic_t ractor_last_id = MAIN_RACTOR_ID; +static rb_serial_t ractor_last_id = MAIN_RACTOR_ID; #include "ractor_sync.c" // creation/termination -static uint32_t +/* Ids are never reused, so they must not wrap either: 64 bits, which rules out an + * atomic (there is no portable 64-bit one). Serialized by the VM lock, or by the + * GVL before there is a second Ractor to take it against -- the same condition + * vm_insert_ractor0 asserts. */ +static rb_serial_t ractor_next_id(void) { - uint32_t id; - - id = (uint32_t)(RUBY_ATOMIC_FETCH_ADD(ractor_last_id, 1) + 1); - - return id; + VM_ASSERT(RB_VM_LOCKED_P() || !rb_multi_ractor_p()); + return ++ractor_last_id; } static void vm_insert_ractor0(rb_vm_t *vm, rb_ractor_t *r, bool single_ractor_mode) { - RUBY_DEBUG_LOG("r:%u ractor.cnt:%u++", r->pub.id, vm->ractor.cnt); + RUBY_DEBUG_LOG("r:%"PRI_SERIALT_PREFIX"u ractor.cnt:%u++", r->pub.id, vm->ractor.cnt); VM_ASSERT(single_ractor_mode || RB_VM_LOCKED_P()); /* Incremental marking only runs in a single-objspace world, and nothing later can @@ -846,8 +848,10 @@ ractor_create(rb_execution_context_t *ec, VALUE self, VALUE loc, VALUE name, VAL rb_ractor_t *r = RACTOR_PTR(rv); ractor_init(r, name, loc); - r->pub.id = ractor_next_id(); - RUBY_DEBUG_LOG("r:%u", r->pub.id); + RB_VM_LOCKING() { + r->pub.id = ractor_next_id(); + } + RUBY_DEBUG_LOG("r:%"PRI_SERIALT_PREFIX"u", r->pub.id); rb_ractor_t *cr = rb_ec_ractor_ptr(ec); r->verbose = cr->verbose; @@ -973,7 +977,7 @@ rb_ractor_living_threads_insert(rb_ractor_t *r, rb_thread_t *th) RACTOR_LOCK(r); { - RUBY_DEBUG_LOG("r(%d)->threads.cnt:%d++", r->pub.id, r->threads.cnt); + RUBY_DEBUG_LOG("r(%"PRI_SERIALT_PREFIX"u)->threads.cnt:%d++", r->pub.id, r->threads.cnt); ccan_list_add_tail(&r->threads.set, &th->lt_node); r->threads.cnt++; } @@ -1145,7 +1149,7 @@ ractor_terminal_interrupt_all(rb_vm_t *vm) rb_ractor_t *r = 0; ccan_list_for_each(&vm->ractor.set, r, vmlr_node) { if (r != vm->ractor.main_ractor) { - RUBY_DEBUG_LOG("r:%d", rb_ractor_id(r)); + RUBY_DEBUG_LOG("r:%"PRI_SERIALT_PREFIX"u", rb_ractor_id(r)); rb_ractor_terminate_interrupt_main_thread(r); } } @@ -1362,7 +1366,7 @@ rb_ractor_dump(void) ccan_list_for_each(&vm->ractor.set, r, vmlr_node) { if (r != vm->ractor.main_ractor) { - fprintf(stderr, "r:%u (%s)\n", r->pub.id, ractor_status_str(r->status_)); + fprintf(stderr, "r:%"PRI_SERIALT_PREFIX"u (%s)\n", r->pub.id, ractor_status_str(r->status_)); } } } @@ -2884,6 +2888,13 @@ move_preflight(VALUE obj, struct move_preflight_ctx *ctx) st_insert(seen, (st_data_t)obj, 0); ctx->nodes++; + /* The receiver takes over a materialized singleton class, so its contents have to + * be movable too. */ + VALUE klass = RBASIC_CLASS(obj); + if (RB_UNLIKELY(klass && FL_TEST_RAW(klass, FL_SINGLETON))) { + rb_class_check_singleton_movable(klass); + } + switch (BUILTIN_TYPE(obj)) { case T_STRING: case T_OBJECT: @@ -3120,6 +3131,8 @@ courier_apply_klass(VALUE shell, VALUE klass) } if (RB_UNLIKELY(FL_TEST_RAW(klass, FL_SINGLETON))) { rb_singleton_class_attached(klass, shell); + /* the singleton class follows its object, which is now this Ractor's */ + rb_class_take_ownership(klass); } } diff --git a/ractor.rb b/ractor.rb index 67d96cbbd112ee..48afd71a56fba0 100644 --- a/ractor.rb +++ b/ractor.rb @@ -383,7 +383,7 @@ def send(...) def inspect loc = __builtin_cexpr! %q{ RACTOR_PTR(self)->loc } name = __builtin_cexpr! %q{ RACTOR_PTR(self)->name } - id = __builtin_cexpr! %q{ UINT2NUM(rb_ractor_id(RACTOR_PTR(self))) } + id = __builtin_cexpr! %q{ ULL2NUM(rb_ractor_id(RACTOR_PTR(self))) } status = __builtin_cexpr! %q{ rb_str_new2(RACTOR_PTR(self)->status_ == ractor_terminated ? "terminated" : "running") } @@ -866,7 +866,7 @@ def closed? # port.inspect -> string def inspect "#r))" + __builtin_cexpr! "ULL2NUM(rb_ractor_id(ractor_port_ptr_check(self)->r))" } id:#{ __builtin_cexpr! "SIZET2NUM(ractor_port_id(RACTOR_PORT_PTR(self)))" }>" diff --git a/ractor_core.h b/ractor_core.h index a3e2ce2f54af95..5829dc9aca7cbe 100644 --- a/ractor_core.h +++ b/ractor_core.h @@ -1,3 +1,5 @@ +#ifndef RUBY_RACTOR_CORE_H +#define RUBY_RACTOR_CORE_H #include "internal/gc.h" #include "ruby/ruby.h" #include "ruby/ractor.h" @@ -334,7 +336,7 @@ rb_ractor_set_current_ec_(rb_ractor_t *cr, rb_execution_context_t *ec, const cha void rb_vm_ractor_blocking_cnt_inc(rb_vm_t *vm, rb_ractor_t *cr, const char *file, int line); void rb_vm_ractor_blocking_cnt_dec(rb_vm_t *vm, rb_ractor_t *cr, const char *file, int line); -static inline uint32_t +static inline rb_serial_t rb_ractor_id(const rb_ractor_t *r) { return r->pub.id; @@ -389,3 +391,5 @@ rb_ractor_ignore_belonging(bool flag) #define rb_ractor_confirm_belonging(obj) obj #define rb_ractor_ignore_belonging(flag) (0) #endif + +#endif /* RUBY_RACTOR_CORE_H */ diff --git a/ractor_sync.c b/ractor_sync.c index eda4754236a7f4..fd19b6b3f39cd4 100644 --- a/ractor_sync.c +++ b/ractor_sync.c @@ -1359,7 +1359,7 @@ ractor_wakeup_all(rb_ractor_t *r, enum ractor_wakeup_status wakeup_status) { ASSERT_ractor_unlocking(r); - RUBY_DEBUG_LOG("r:%u wakeup:%s", rb_ractor_id(r), wakeup_status_str(wakeup_status)); + RUBY_DEBUG_LOG("r:%"PRI_SERIALT_PREFIX"u wakeup:%s", rb_ractor_id(r), wakeup_status_str(wakeup_status)); bool wakeup_p = false; @@ -1622,7 +1622,7 @@ ractor_send_basket(rb_execution_context_t *ec, const struct ractor_port *rp, str { bool closed = false; - RUBY_DEBUG_LOG("port:%u@r%u b:%s v:%p", (unsigned int)ractor_port_id(rp), rb_ractor_id(rp->r), basket_type_name(b->type), (void *)b->p.v); + RUBY_DEBUG_LOG("port:%u@r%"PRI_SERIALT_PREFIX"u b:%s v:%p", (unsigned int)ractor_port_id(rp), rb_ractor_id(rp->r), basket_type_name(b->type), (void *)b->p.v); RACTOR_LOCK(rp->r); { @@ -1644,7 +1644,7 @@ ractor_send_basket(rb_execution_context_t *ec, const struct ractor_port *rp, str ractor_wakeup_all(rp->r, wakeup_by_send); } else { - RUBY_DEBUG_LOG("closed:%u@r%u", (unsigned int)ractor_port_id(rp), rb_ractor_id(rp->r)); + RUBY_DEBUG_LOG("closed:%u@r%"PRI_SERIALT_PREFIX"u", (unsigned int)ractor_port_id(rp), rb_ractor_id(rp->r)); /* Nothing took the basket: it was not enqueued, so free it whether or not the * caller wants the error raised. */ diff --git a/spec/bundler/commands/lock_spec.rb b/spec/bundler/commands/lock_spec.rb index 97a3989d4b831c..c02ab1ef205bd3 100644 --- a/spec/bundler/commands/lock_spec.rb +++ b/spec/bundler/commands/lock_spec.rb @@ -886,6 +886,26 @@ expect(lockfile).to end_with("BUNDLED WITH\n 99\n") end + it "does not update the bundler version in the lockfile to a prerelease version, unless the target version allows prereleases" do + build_repo4 do + build_gem "bundler", "55" + build_gem "bundler", "56.0.0.beta1" + end + + system_gems "bundler-55", gem_repo: gem_repo4 + + install_gemfile <<-G, artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + source "https://gem.repo4" + G + lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2') + + bundle "lock --update --bundler --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + expect(lockfile).to end_with("BUNDLED WITH\n 55\n") + + bundle "lock --update --bundler '> 0.a' --verbose", artifice: "compact_index", env: { "BUNDLER_SPEC_GEM_REPO" => gem_repo4.to_s } + expect(lockfile).to end_with("BUNDLED WITH\n 56.0.0.beta1\n") + end + it "supports adding new platforms when there's no previous lockfile" do gemfile_with_rails_weakling_and_foo_from_repo4 diff --git a/spec/bundler/commands/update_scenarios_spec.rb b/spec/bundler/commands/update_scenarios_spec.rb index f1d9635bb22b48..5428b6169ee1a6 100644 --- a/spec/bundler/commands/update_scenarios_spec.rb +++ b/spec/bundler/commands/update_scenarios_spec.rb @@ -466,7 +466,7 @@ bundle :update, bundler: true, verbose: true expect(out).to include("Updating bundler to 999.0.0") - expect(out).to include("Running `bundle update --bundler \"> 0.a\" --verbose` with bundler 999.0.0") + expect(out).to include("Running `bundle update --bundler \">= 999.0.0\" --verbose` with bundler 999.0.0") expect(out).not_to include("Installing Bundler 2.99.9 and restarting using that version.") expect(lockfile).to eq <<~L @@ -578,6 +578,180 @@ expect(out).to include("myrack (1.0)") end + it "does not update the bundler version in the lockfile to a prerelease version", :ruby_repo do + pristine_system_gems "bundler-9.9.9" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "9.9.9" + build_bundler "999.0.0.beta1" + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "9.9.9") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: true, verbose: true + + expect(out).to include("Using bundler 9.9.9") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 9.9.9 + L + end + + it "updates the bundler version in the lockfile to a prerelease version when the target version allows prereleases", :ruby_repo do + bundle_config "path.system true" + + pristine_system_gems "bundler-9.0.0" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "999.0.0.beta1" + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "999.0.0.beta1") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle "update --bundler '> 0.a' --verbose" + + expect(out).to include("Updating bundler to 999.0.0.beta1") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 999.0.0.beta1 + L + end + + it "goes back to a released version given explicitly when the lockfile is locked to a prerelease", :ruby_repo do + bundle_config "path.system true" + + pristine_system_gems "bundler-9.0.0.beta1" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "9.0.0" + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "9.0.0") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + # Auto switching puts the beta back in charge on every command, so an + # explicit target is the only way out of a lockfile that names one. + expect(lockfile).to match(/BUNDLED WITH\n\s+9\.0\.0\.beta1\n/) + + bundle "update --bundler 9.0.0 --verbose" + + expect(out).to include("Updating bundler to 9.0.0") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 9.0.0 + L + + bundle "--version" + expect(out).to include("9.0.0") + end + + it "updates the bundler version in the lockfile to a prerelease version when --pre is given", :ruby_repo do + bundle_config "path.system true" + + pristine_system_gems "bundler-9.0.0" + + build_repo4 do + build_gem "myrack", "1.0" + + build_bundler "999.0.0.beta1" + end + + checksums = checksums_section do |c| + c.checksum(gem_repo4, "myrack", "1.0") + c.checksum(gem_repo4, "bundler", "999.0.0.beta1") + end + + install_gemfile <<-G + source "https://gem.repo4" + gem "myrack" + G + + bundle :update, bundler: true, pre: true, verbose: true + + expect(out).to include("Updating bundler to 999.0.0.beta1") + + expect(lockfile).to eq <<~L + GEM + remote: https://gem.repo4/ + specs: + myrack (1.0) + + PLATFORMS + #{lockfile_platforms} + + DEPENDENCIES + myrack + #{checksums} + BUNDLED WITH + 999.0.0.beta1 + L + end + it "errors if the explicit target version does not exist" do pristine_system_gems "bundler-9.9.9" diff --git a/spec/default.mspec b/spec/default.mspec index d756dc31ffd566..1fe83850919f85 100644 --- a/spec/default.mspec +++ b/spec/default.mspec @@ -3,6 +3,11 @@ $VERBOSE = false if (opt = ENV["RUBYOPT"]) and (opt = opt.dup).sub!(/(?:\A|\s)-w(?=\z|\s)/, '') ENV["RUBYOPT"] = opt end +# The specs assert the output of the ruby processes they spawn verbatim. See +# tool/test/init.rb for why the switch goes where it does. +rubyopt = ENV["RUBYOPT"].to_s.split - ["-W:no-experimental"] +rubyopt.insert(rubyopt.index("-") || rubyopt.size, "-W:no-experimental") +ENV["RUBYOPT"] = rubyopt.join(" ") # Enable constant leak checks by ruby/mspec ENV["CHECK_CONSTANT_LEAKS"] ||= "true" @@ -64,6 +69,38 @@ class MSpecScript prepend JobServer end +if ENV["RUBY_BOX"] == "1" + # Ruby::Box prints this at startup of every child process when RUBY_BOX=1 + # is inherited from the environment. + module MSpecScript::StripBoxExperimentalWarning + WARNING = /^.*: warning: Ruby::Box is experimental, and the behavior may change in the future!\nSee https:\/\/docs\.ruby-lang\.org\/\S+ for known issues, etc\.\n/ + + # At load time this would resolve RUBY_EXE before mspec exports the flags. + def setup_env + super + require "mspec/helpers/ruby_exe" + Object.class_eval do + # Not a prepend, which the Module ancestor specs would see. + alias_method :ruby_exe_with_box_warning, :ruby_exe + private :ruby_exe_with_box_warning + + private def ruby_exe(code = :not_given, opts = {}) + output = ruby_exe_with_box_warning(code, opts) + if code != :not_given and !opts[:env]&.any? {|k,| k.to_s == "RUBY_BOX"} + # on the bytes, since the output is not always valid in its encoding + output = output.b.gsub(WARNING, "").force_encoding(output.encoding) + end + output + end + end + end + end + + class MSpecScript + prepend StripBoxExperimentalWarning + end +end + require 'mspec/runner/formatters/dotted' class DottedFormatter diff --git a/spec/ruby/core/warning/element_reference_spec.rb b/spec/ruby/core/warning/element_reference_spec.rb index 5f977759ecc622..f2744dc90383a0 100644 --- a/spec/ruby/core/warning/element_reference_spec.rb +++ b/spec/ruby/core/warning/element_reference_spec.rb @@ -5,9 +5,11 @@ # If any warning options were set on the Ruby that will be executed, then # it's possible this test will fail. In this case we will skip this test. skip if ruby_exe.any? { |opt| opt.start_with?("-W") } + # RUBYOPT can carry -W too, and the defaults are what this example is about. + no_rubyopt = { "RUBYOPT" => nil } - ruby_exe('p [Warning[:deprecated], Warning[:experimental]]').chomp.should == "[false, true]" - ruby_exe('p [Warning[:deprecated], Warning[:experimental]]', options: "-w").chomp.should == "[true, true]" + ruby_exe('p [Warning[:deprecated], Warning[:experimental]]', env: no_rubyopt).chomp.should == "[false, true]" + ruby_exe('p [Warning[:deprecated], Warning[:experimental]]', options: "-w", env: no_rubyopt).chomp.should == "[true, true]" end it "returns default values for :performance category" do diff --git a/string.c b/string.c index 7226e7287f700f..61a18b62b2fa96 100644 --- a/string.c +++ b/string.c @@ -9385,9 +9385,18 @@ tr_trans_pairs_search_sse2(struct tr_trans_pairs_search *search) static inline VALUE tr_trans_pairs_next_match_neon(struct tr_trans_pairs_search *search) { - size_t next_match_offset = ntz_int64(search->matches_bitmap) / 4; - search->matches_bitmap >>= (next_match_offset + 1) * 4; - search->s += next_match_offset; + int trailing_zeros = ntz_int64(search->matches_bitmap); + + // uint64_t >>= 64 would be undefined behaviour + if (trailing_zeros >= 63) { + search->matches_bitmap = 0; + search->s += 15; + } + else { + search->matches_bitmap >>= (trailing_zeros + 1); + search->s += trailing_zeros / 4; + } + RUBY_ASSERT(search->s <= search->send); return search->trans_table[*search->s]; } @@ -9423,10 +9432,10 @@ tr_trans_pairs_search_neon(struct tr_trans_pairs_search *search) } const uint8x8_t res = vshrn_n_u16(vreinterpretq_u16_u8(matches[0]), 4); - const uint64_t bitmap = vget_lane_u64(vreinterpret_u64_u8(res), 0) & 0x8888888888888888ull; + const uint64_t bitmap = vget_lane_u64(vreinterpret_u64_u8(res), 0); if (bitmap) { - search->matches_bitmap = bitmap; + search->matches_bitmap = bitmap & 0x8888888888888888ull; return tr_trans_pairs_next_match_neon(search); } search->s += sizeof(uint8x16_t); diff --git a/test/-ext-/test_abi.rb b/test/-ext-/test_abi.rb index 7f30feb9449187..f6f0e6afbf4545 100644 --- a/test/-ext-/test_abi.rb +++ b/test/-ext-/test_abi.rb @@ -6,11 +6,11 @@ class TestABI < Test::Unit::TestCase def test_require_lib_with_incorrect_abi_on_dev_ruby omit "ABI is not checked" unless abi_checking_supported? - assert_separately [], <<~RUBY + assert_separately [], <<~'RUBY' err = assert_raise(LoadError) { require "-test-/abi" } assert_match(/incompatible ABI version/, err.message) if Ruby::Box.enabled? - assert_include err.message, "_-test-+abi." + assert_match(%r{/_ruby_box_[^/]+/\d+_\d+_abi\.}, err.message) else assert_include err.message, "/-test-/abi." end @@ -20,7 +20,7 @@ def test_require_lib_with_incorrect_abi_on_dev_ruby def test_disable_abi_check_using_environment_variable omit "ABI is not checked" unless abi_checking_supported? - assert_separately [{ "RUBY_ABI_CHECK" => "0" }], <<~RUBY + assert_separately [{ "RUBY_ABI_CHECK" => "0" }], <<~'RUBY' assert_nothing_raised { require "-test-/abi" } RUBY end @@ -28,11 +28,11 @@ def test_disable_abi_check_using_environment_variable def test_enable_abi_check_using_environment_variable omit "ABI is not checked" unless abi_checking_supported? - assert_separately [{ "RUBY_ABI_CHECK" => "1" }], <<~RUBY + assert_separately [{ "RUBY_ABI_CHECK" => "1" }], <<~'RUBY' err = assert_raise(LoadError) { require "-test-/abi" } assert_match(/incompatible ABI version/, err.message) if Ruby::Box.enabled? - assert_include err.message, "_-test-+abi." + assert_match(%r{/_ruby_box_[^/]+/\d+_\d+_abi\.}, err.message) else assert_include err.message, "/-test-/abi." end @@ -42,7 +42,7 @@ def test_enable_abi_check_using_environment_variable def test_require_lib_with_incorrect_abi_on_release_ruby omit "ABI is enforced" if abi_checking_supported? - assert_separately [], <<~RUBY + assert_separately [], <<~'RUBY' assert_nothing_raised { require "-test-/abi" } RUBY end diff --git a/test/.excludes-mmtk/TestObjSpace.rb b/test/.excludes-mmtk/TestObjSpace.rb index feb05063df63bf..8fc0a43349b21f 100644 --- a/test/.excludes-mmtk/TestObjSpace.rb +++ b/test/.excludes-mmtk/TestObjSpace.rb @@ -1,5 +1,6 @@ exclude(:test_dump_all_full, "testing behaviour specific to default GC") exclude(:test_dump_flag_age, "testing behaviour specific to default GC") exclude(:test_dump_flags, "testing behaviour specific to default GC") +exclude(:test_dump_flags_wb_protected, "testing behaviour specific to default GC") exclude(:test_dump_objects_dumps_page_slot_sizes, "testing behaviour specific to default GC") exclude(:test_trace_object_allocations_does_not_reuse_freed_allocation_info, "hang up") diff --git a/test/.excludes/JSONGenericObjectTest.rb b/test/.excludes/JSONGenericObjectTest.rb deleted file mode 100644 index 820a6a01200182..00000000000000 --- a/test/.excludes/JSONGenericObjectTest.rb +++ /dev/null @@ -1,4 +0,0 @@ -# ostruct will be loaded when JSON::GenericObject is autoloaded. By -# removing all test methods, the autoload in `setup` is not triggered. - -exclude /test_/, 'JSON::GenericObject needs ostruct gem' diff --git a/test/.excludes/TestArray.rb b/test/.excludes/TestArray.rb deleted file mode 100644 index 73da272007ca33..00000000000000 --- a/test/.excludes/TestArray.rb +++ /dev/null @@ -1 +0,0 @@ -exclude(:test_shared_marking, "The target code has already been changed") diff --git a/test/.excludes/TestArraySubclass.rb b/test/.excludes/TestArraySubclass.rb deleted file mode 100644 index 73da272007ca33..00000000000000 --- a/test/.excludes/TestArraySubclass.rb +++ /dev/null @@ -1 +0,0 @@ -exclude(:test_shared_marking, "The target code has already been changed") diff --git a/test/erb/test_erb.rb b/test/erb/test_erb.rb index c789e074510df5..b96c0cb3031b9a 100644 --- a/test/erb/test_erb.rb +++ b/test/erb/test_erb.rb @@ -43,34 +43,6 @@ def test_with_location assert_match(/\Atest filename:201\b/, e.backtrace[0]) end - def test_html_escape - assert_equal(" !"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", - ERB::Util.html_escape(" !\"\#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~")) - - assert_equal("", ERB::Util.html_escape("")) - assert_equal("abc", ERB::Util.html_escape("abc")) - assert_equal("<<", ERB::Util.html_escape("<\<")) - assert_equal("'&"><" * 10, ERB::Util.html_escape("'&\"><" * 10)) - - assert_equal("", ERB::Util.html_escape(nil)) - assert_equal("123", ERB::Util.html_escape(123)) - - assert_equal(65536+5, ERB::Util.html_escape("x"*65536 + "&").size) - assert_equal(65536+5, ERB::Util.html_escape("&" + "x"*65536).size) - end - - def test_html_escape_to_s - object = Object.new - def object.to_s - "object" - end - assert_equal("object", ERB::Util.html_escape(object)) - end - - def test_html_escape_extension - assert_nil(ERB::Util.method(:html_escape).source_location) - end if RUBY_ENGINE == 'ruby' - def test_concurrent_default_binding # This test randomly fails with JRuby -- NameError: undefined local variable or method `template2' pend if RUBY_ENGINE == 'jruby' @@ -770,13 +742,4 @@ def test_frozen_erb_instance_reused_across_ractors assert_equal(["2", "2"], rs.map(&:value)) RUBY end - - def test_util_html_escape_in_ractor - assert_ractor(<<~RUBY, require: 'erb') - r = Ractor.new do - ERB::Util.html_escape("