From e8883b55da69ec93bc9e291660e5168e96416c14 Mon Sep 17 00:00:00 2001 From: Amin Mansuri Date: Sat, 12 Sep 2026 07:44:53 -0300 Subject: [PATCH 1/4] Release the autoload claim when Autoload#load has nothing to load A direct require of the autoload's file defines the constant while the claim is still held, so the value stays on the Autoload and the constant table keeps UNDEF; every later lookup from another thread re-enters LoadService. --- core/src/main/java/org/jruby/RubyModule.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/jruby/RubyModule.java b/core/src/main/java/org/jruby/RubyModule.java index 7cce200eaa6..cc8406d585d 100644 --- a/core/src/main/java/org/jruby/RubyModule.java +++ b/core/src/main/java/org/jruby/RubyModule.java @@ -6689,11 +6689,14 @@ synchronized IRubyObject load(ThreadContext context) { // This method needs to be synchronized for removing Autoload // from autoloadMap when it's loaded. LoadService loadService = loadService(context); - if (!loadService.featureAlreadyLoaded(path.asJavaString())) { - if (loadService.autoloadRequire(path)) { - // Do not finish autoloading by cyclic autoload - finishAutoload(context, symbol); - } + if (loadService.featureAlreadyLoaded(path.asJavaString())) { + // Nothing to load here: the feature is loaded, or a direct require of it is in + // progress. Keeping the claim would make that require's definition of the + // constant look like this autoload's own and leave UNDEF in the constant table. + this.ctx = null; + } else if (loadService.autoloadRequire(path)) { + // Do not finish autoloading by cyclic autoload + finishAutoload(context, symbol); } } catch (LoadError | RuntimeError lre) { // reset ctx to null for a future attempt to load From 33fb67c06592d9ddb3dfc8dc0ef4a307ba62bedb Mon Sep 17 00:00:00 2001 From: Amin Mansuri Date: Sat, 12 Sep 2026 07:44:53 -0300 Subject: [PATCH 2/4] Specs for an autoload satisfied by a direct require of its file ruby/spec pins what Ruby can see (remove_const value, Marshal and $LOAD_PATH independence from another thread); test/jruby pins the constant-table slot. --- spec/ruby/core/kernel/autoload_spec.rb | 33 +++++ .../fixtures/autoload_satisfied_by_require.rb | 4 + spec/ruby/core/module/autoload_spec.rb | 102 ++++++++++++++ .../fixtures/autoload_satisfied_by_require.rb | 6 + .../autoload_satisfied_by_require_nested.rb | 7 + ...oload_satisfied_by_require_nested_inner.rb | 8 ++ .../autoload_satisfied_by_require_raise.rb | 8 ++ .../autoload_satisfied_by_require_relative.rb | 1 + test/jruby/autoload_required/assigned.rb | 3 + test/jruby/autoload_required/colon2.rb | 4 + test/jruby/autoload_required/concurrent.rb | 6 + test/jruby/autoload_required/eager/alpha.rb | 8 ++ test/jruby/autoload_required/eager/beta.rb | 8 ++ test/jruby/autoload_required/eager/gamma.rb | 8 ++ test/jruby/autoload_required/java_import.rb | 7 + test/jruby/autoload_required/loaded.rb | 6 + test/jruby/autoload_required/private.rb | 6 + test/jruby/autoload_required/table_slot.rb | 6 + test/jruby/test_autoload.rb | 129 ++++++++++++++++++ 19 files changed, 360 insertions(+) create mode 100644 spec/ruby/core/kernel/fixtures/autoload_satisfied_by_require.rb create mode 100644 spec/ruby/core/module/fixtures/autoload_satisfied_by_require.rb create mode 100644 spec/ruby/core/module/fixtures/autoload_satisfied_by_require_nested.rb create mode 100644 spec/ruby/core/module/fixtures/autoload_satisfied_by_require_nested_inner.rb create mode 100644 spec/ruby/core/module/fixtures/autoload_satisfied_by_require_raise.rb create mode 100644 spec/ruby/core/module/fixtures/autoload_satisfied_by_require_relative.rb create mode 100644 test/jruby/autoload_required/assigned.rb create mode 100644 test/jruby/autoload_required/colon2.rb create mode 100644 test/jruby/autoload_required/concurrent.rb create mode 100644 test/jruby/autoload_required/eager/alpha.rb create mode 100644 test/jruby/autoload_required/eager/beta.rb create mode 100644 test/jruby/autoload_required/eager/gamma.rb create mode 100644 test/jruby/autoload_required/java_import.rb create mode 100644 test/jruby/autoload_required/loaded.rb create mode 100644 test/jruby/autoload_required/private.rb create mode 100644 test/jruby/autoload_required/table_slot.rb diff --git a/spec/ruby/core/kernel/autoload_spec.rb b/spec/ruby/core/kernel/autoload_spec.rb index 5edb70541d1..b99d131977a 100644 --- a/spec/ruby/core/kernel/autoload_spec.rb +++ b/spec/ruby/core/kernel/autoload_spec.rb @@ -164,6 +164,39 @@ def go KernelSpecs::AutoloadMethod2::AutoloadFromIncludedModule2.loaded.should == :autoload_from_included_module2 end end + + describe "after the autoload was satisfied by requiring the file directly" do + before :each do + @path = fixture(__FILE__, "autoload_satisfied_by_require.rb") + Kernel.autoload :KSAutoloadSatisfiedByRequire, @path + ScratchPad.record [] + end + + after :each do + if Object.const_defined?(:KSAutoloadSatisfiedByRequire, false) + Object.send(:remove_const, :KSAutoloadSatisfiedByRequire) + end + end + + it "defines a regular constant that no longer depends on $LOADED_FEATURES" do + require @path + ScratchPad.recorded.should == [:loaded] + Kernel.autoload?(:KSAutoloadSatisfiedByRequire).should be_nil + klass = KSAutoloadSatisfiedByRequire + data = Marshal.dump(klass.new) + + $LOADED_FEATURES.replace(@loaded_features) + Thread.new { Marshal.load(data).class }.value.should equal(klass) + ScratchPad.recorded.should == [:loaded] + end + + it "returns the constant from remove_const" do + require @path + klass = KSAutoloadSatisfiedByRequire + + Object.send(:remove_const, :KSAutoloadSatisfiedByRequire).should equal(klass) + end + end end describe "Kernel.autoload?" do diff --git a/spec/ruby/core/kernel/fixtures/autoload_satisfied_by_require.rb b/spec/ruby/core/kernel/fixtures/autoload_satisfied_by_require.rb new file mode 100644 index 00000000000..028948dbae4 --- /dev/null +++ b/spec/ruby/core/kernel/fixtures/autoload_satisfied_by_require.rb @@ -0,0 +1,4 @@ +ScratchPad << :loaded + +class KSAutoloadSatisfiedByRequire +end diff --git a/spec/ruby/core/module/autoload_spec.rb b/spec/ruby/core/module/autoload_spec.rb index 625d945686a..a1d0b79f430 100644 --- a/spec/ruby/core/module/autoload_spec.rb +++ b/spec/ruby/core/module/autoload_spec.rb @@ -320,6 +320,108 @@ module ModuleSpecs::Autoload end end + describe "after the autoload was satisfied by requiring the file directly" do + before :each do + @path = fixture(__FILE__, "autoload_satisfied_by_require.rb") + ModuleSpecs::Autoload.autoload :SatisfiedByRequire, @path + @remove << :SatisfiedByRequire + ScratchPad.record [] + end + + it "defines a regular constant that no longer depends on $LOADED_FEATURES" do + require @path + ScratchPad.recorded.should == [:loaded] + ModuleSpecs::Autoload.autoload?(:SatisfiedByRequire).should be_nil + klass = ModuleSpecs::Autoload::SatisfiedByRequire + data = Marshal.dump(klass.new) + + $LOADED_FEATURES.replace(@loaded_features) + Thread.new { Marshal.load(data).class }.value.should equal(klass) + ScratchPad.recorded.should == [:loaded] + end + + it "defines a regular constant that no longer depends on $LOAD_PATH" do + ModuleSpecs::Autoload.autoload :SatisfiedByRequire, "autoload_satisfied_by_require" + $:.push File.dirname(@path) + begin + require "autoload_satisfied_by_require.rb" + ensure + $:.pop + end + ScratchPad.recorded.should == [:loaded] + klass = ModuleSpecs::Autoload::SatisfiedByRequire + data = Marshal.dump(klass.new) + + Thread.new { Marshal.load(data).class }.value.should equal(klass) + ScratchPad.recorded.should == [:loaded] + end + + it "returns the constant from remove_const" do + require @path + klass = ModuleSpecs::Autoload::SatisfiedByRequire + @remove.delete(:SatisfiedByRequire) + + ModuleSpecs::Autoload.send(:remove_const, :SatisfiedByRequire).should equal(klass) + ModuleSpecs::Autoload.const_defined?(:SatisfiedByRequire).should be_false + end + + it "can be registered as an autoload again after remove_const" do + require @path + klass = ModuleSpecs::Autoload::SatisfiedByRequire + ModuleSpecs::Autoload.send(:remove_const, :SatisfiedByRequire) + $LOADED_FEATURES.replace(@loaded_features) + + ModuleSpecs::Autoload.autoload :SatisfiedByRequire, @path + ModuleSpecs::Autoload.autoload?(:SatisfiedByRequire).should == @path + ModuleSpecs::Autoload::SatisfiedByRequire.should_not equal(klass) + ScratchPad.recorded.should == [:loaded, :loaded] + end + + it "keeps the constant defined by the file when the require raises afterwards" do + path = fixture(__FILE__, "autoload_satisfied_by_require_raise.rb") + ModuleSpecs::Autoload.autoload :SatisfiedByRequireRaise, path + @remove << :SatisfiedByRequireRaise + + -> { require path }.should raise_error(RuntimeError, "raised after defining the constant") + ScratchPad.recorded.should == [:loaded] + ModuleSpecs::Autoload.autoload?(:SatisfiedByRequireRaise).should be_nil + klass = ModuleSpecs::Autoload::SatisfiedByRequireRaise + data = Marshal.dump(klass.new) + + Thread.new { Marshal.load(data).class }.value.should equal(klass) + ScratchPad.recorded.should == [:loaded] + end + + it "applies to an autoload the required file declares for a nested constant" do + nested = fixture(__FILE__, "autoload_satisfied_by_require_nested.rb") + inner = fixture(__FILE__, "autoload_satisfied_by_require_nested_inner.rb") + ModuleSpecs::Autoload.autoload :SatisfiedByRequireNested, nested + @remove << :SatisfiedByRequireNested + + require nested + ModuleSpecs::Autoload::SatisfiedByRequireNested.autoload?(:Inner).should == inner + require inner + ModuleSpecs::Autoload::SatisfiedByRequireNested.autoload?(:Inner).should be_nil + klass = ModuleSpecs::Autoload::SatisfiedByRequireNested::Inner + data = Marshal.dump(klass.new) + + $LOADED_FEATURES.replace(@loaded_features) + Thread.new { Marshal.load(data).class }.value.should equal(klass) + ScratchPad.recorded.should == [:nested_loaded, :inner_loaded] + end + + it "applies when the file was loaded with require_relative" do + require fixture(__FILE__, "autoload_satisfied_by_require_relative.rb") + ScratchPad.recorded.should == [:loaded] + klass = ModuleSpecs::Autoload::SatisfiedByRequire + data = Marshal.dump(klass.new) + + $LOADED_FEATURES.replace(@loaded_features) + Thread.new { Marshal.load(data).class }.value.should equal(klass) + ScratchPad.recorded.should == [:loaded] + end + end + describe "after the autoload is triggered by require" do before :each do @path = tmp("autoload.rb") diff --git a/spec/ruby/core/module/fixtures/autoload_satisfied_by_require.rb b/spec/ruby/core/module/fixtures/autoload_satisfied_by_require.rb new file mode 100644 index 00000000000..e99b214e030 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_satisfied_by_require.rb @@ -0,0 +1,6 @@ +ScratchPad << :loaded + +module ModuleSpecs::Autoload + class SatisfiedByRequire + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_satisfied_by_require_nested.rb b/spec/ruby/core/module/fixtures/autoload_satisfied_by_require_nested.rb new file mode 100644 index 00000000000..95fe0e7c256 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_satisfied_by_require_nested.rb @@ -0,0 +1,7 @@ +ScratchPad << :nested_loaded + +module ModuleSpecs::Autoload + class SatisfiedByRequireNested + autoload :Inner, File.expand_path("autoload_satisfied_by_require_nested_inner.rb", __dir__) + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_satisfied_by_require_nested_inner.rb b/spec/ruby/core/module/fixtures/autoload_satisfied_by_require_nested_inner.rb new file mode 100644 index 00000000000..976043a8f7d --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_satisfied_by_require_nested_inner.rb @@ -0,0 +1,8 @@ +ScratchPad << :inner_loaded + +module ModuleSpecs::Autoload + class SatisfiedByRequireNested + class Inner + end + end +end diff --git a/spec/ruby/core/module/fixtures/autoload_satisfied_by_require_raise.rb b/spec/ruby/core/module/fixtures/autoload_satisfied_by_require_raise.rb new file mode 100644 index 00000000000..4321a1bb754 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_satisfied_by_require_raise.rb @@ -0,0 +1,8 @@ +ScratchPad << :loaded + +module ModuleSpecs::Autoload + class SatisfiedByRequireRaise + end +end + +raise "raised after defining the constant" diff --git a/spec/ruby/core/module/fixtures/autoload_satisfied_by_require_relative.rb b/spec/ruby/core/module/fixtures/autoload_satisfied_by_require_relative.rb new file mode 100644 index 00000000000..4b8df6f58e4 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_satisfied_by_require_relative.rb @@ -0,0 +1 @@ +require_relative "autoload_satisfied_by_require" diff --git a/test/jruby/autoload_required/assigned.rb b/test/jruby/autoload_required/assigned.rb new file mode 100644 index 00000000000..4ae0230c7af --- /dev/null +++ b/test/jruby/autoload_required/assigned.rb @@ -0,0 +1,3 @@ +$autoload_required_loads << __FILE__ + +TestAutoload::Assigned = Class.new diff --git a/test/jruby/autoload_required/colon2.rb b/test/jruby/autoload_required/colon2.rb new file mode 100644 index 00000000000..f63f2ef7e41 --- /dev/null +++ b/test/jruby/autoload_required/colon2.rb @@ -0,0 +1,4 @@ +$autoload_required_loads << __FILE__ + +class TestAutoload::Colon2 +end diff --git a/test/jruby/autoload_required/concurrent.rb b/test/jruby/autoload_required/concurrent.rb new file mode 100644 index 00000000000..16bff28c0b1 --- /dev/null +++ b/test/jruby/autoload_required/concurrent.rb @@ -0,0 +1,6 @@ +$autoload_required_loads << __FILE__ + +class TestAutoload + class Concurrent + end +end diff --git a/test/jruby/autoload_required/eager/alpha.rb b/test/jruby/autoload_required/eager/alpha.rb new file mode 100644 index 00000000000..6c5fe090361 --- /dev/null +++ b/test/jruby/autoload_required/eager/alpha.rb @@ -0,0 +1,8 @@ +$autoload_required_loads << __FILE__ + +class TestAutoload + module Eager + class Alpha + end + end +end diff --git a/test/jruby/autoload_required/eager/beta.rb b/test/jruby/autoload_required/eager/beta.rb new file mode 100644 index 00000000000..c2099e94099 --- /dev/null +++ b/test/jruby/autoload_required/eager/beta.rb @@ -0,0 +1,8 @@ +$autoload_required_loads << __FILE__ + +class TestAutoload + module Eager + class Beta < Alpha + end + end +end diff --git a/test/jruby/autoload_required/eager/gamma.rb b/test/jruby/autoload_required/eager/gamma.rb new file mode 100644 index 00000000000..859ca8a8216 --- /dev/null +++ b/test/jruby/autoload_required/eager/gamma.rb @@ -0,0 +1,8 @@ +$autoload_required_loads << __FILE__ + +class TestAutoload + module Eager + module Gamma + end + end +end diff --git a/test/jruby/autoload_required/java_import.rb b/test/jruby/autoload_required/java_import.rb new file mode 100644 index 00000000000..9f70ed56a1b --- /dev/null +++ b/test/jruby/autoload_required/java_import.rb @@ -0,0 +1,7 @@ +$autoload_required_loads << __FILE__ + +class TestAutoload + module Imported + java_import java.util.ArrayList + end +end diff --git a/test/jruby/autoload_required/loaded.rb b/test/jruby/autoload_required/loaded.rb new file mode 100644 index 00000000000..fb40ea6e2bd --- /dev/null +++ b/test/jruby/autoload_required/loaded.rb @@ -0,0 +1,6 @@ +$autoload_required_loads << __FILE__ + +class TestAutoload + class Loaded + end +end diff --git a/test/jruby/autoload_required/private.rb b/test/jruby/autoload_required/private.rb new file mode 100644 index 00000000000..d6d6f55c579 --- /dev/null +++ b/test/jruby/autoload_required/private.rb @@ -0,0 +1,6 @@ +$autoload_required_loads << __FILE__ + +class TestAutoload + class Private + end +end diff --git a/test/jruby/autoload_required/table_slot.rb b/test/jruby/autoload_required/table_slot.rb new file mode 100644 index 00000000000..7ee3e170df1 --- /dev/null +++ b/test/jruby/autoload_required/table_slot.rb @@ -0,0 +1,6 @@ +$autoload_required_loads << __FILE__ + +class TestAutoload + class TableSlot + end +end diff --git a/test/jruby/test_autoload.rb b/test/jruby/test_autoload.rb index 7167b081abe..65724836c03 100644 --- a/test/jruby/test_autoload.rb +++ b/test/jruby/test_autoload.rb @@ -44,4 +44,133 @@ class SomeClass; def foo; true; end; end end end + # A constant declared with autoload and then defined by a direct require of the same + # file must end up in the constant table itself; an UNDEF marker left there sends every + # later lookup from another thread back through the load service. + AUTOLOAD_REQUIRED = File.expand_path("autoload_required", __dir__) + + module Eager; end + module Imported; end + + def setup + $autoload_required_loads = [] + end + + # The value in the module's constant table, :undef while it still holds the autoload marker. + def table_slot(mod, name) + require 'jruby' + entry = JRuby.reference(mod).getConstantMap.get(name.to_s) + return :missing if entry.nil? + value = entry.value + value.class # UNDEF has no metaclass and fails here + value + rescue java.lang.NullPointerException + :undef + end + + def in_thread + Thread.new do + Thread.current.report_on_exception = false + yield + end.value + end + + def test_explicit_require_stores_constant_in_table + path = "#{AUTOLOAD_REQUIRED}/table_slot.rb" + TestAutoload.autoload(:TableSlot, path) + require path + assert_equal [path], $autoload_required_loads + assert_nil TestAutoload.autoload?(:TableSlot) + assert_same TestAutoload::TableSlot, table_slot(TestAutoload, :TableSlot) + assert_same TestAutoload::TableSlot, in_thread { TestAutoload::TableSlot } + assert_equal [path], $autoload_required_loads + end + + def test_explicit_require_colon2_definition_stores_constant_in_table + path = "#{AUTOLOAD_REQUIRED}/colon2.rb" + TestAutoload.autoload(:Colon2, path) + require path + assert_nil TestAutoload.autoload?(:Colon2) + assert_same TestAutoload::Colon2, table_slot(TestAutoload, :Colon2) + end + + # Assignment never went through the class-definition lookup; kept as a guard. + def test_explicit_require_assignment_stores_constant_in_table + path = "#{AUTOLOAD_REQUIRED}/assigned.rb" + TestAutoload.autoload(:Assigned, path) + require path + assert_nil TestAutoload.autoload?(:Assigned) + assert_same TestAutoload::Assigned, table_slot(TestAutoload, :Assigned) + end + + def test_explicit_require_keeps_private_constant + path = "#{AUTOLOAD_REQUIRED}/private.rb" + TestAutoload.autoload(:Private, path) + TestAutoload.send(:private_constant, :Private) + require path + assert_nil TestAutoload.autoload?(:Private) + assert_raise(NameError) { TestAutoload::Private } + assert_raise(NameError) { in_thread { TestAutoload::Private } } + assert_same TestAutoload.const_get(:Private), table_slot(TestAutoload, :Private) + end + + # Kernel#load re-runs the file through the autoload; the slot was already right. Guard. + def test_explicit_load_finishes_the_autoload + path = "#{AUTOLOAD_REQUIRED}/loaded.rb" + TestAutoload.autoload(:Loaded, path) + load path + assert_nil TestAutoload.autoload?(:Loaded) + assert_same TestAutoload::Loaded, table_slot(TestAutoload, :Loaded) + end + + # java_import defines the constant with const_set; the slot was already right. Guard. + def test_explicit_require_of_java_import + path = "#{AUTOLOAD_REQUIRED}/java_import.rb" + Imported.autoload(:ArrayList, path) + require path + assert_nil Imported.autoload?(:ArrayList) + assert_same java.util.ArrayList, table_slot(Imported, :ArrayList) + assert_same java.util.ArrayList, in_thread { TestAutoload::Imported::ArrayList } + end + + def test_explicit_require_then_concurrent_references + path = "#{AUTOLOAD_REQUIRED}/concurrent.rb" + TestAutoload.autoload(:Concurrent, path) + require path + klass = TestAutoload::Concurrent + features = $LOADED_FEATURES.size + threads = 8.times.map do + Thread.new { 1000.times.all? { TestAutoload::Concurrent.equal?(klass) } } + end + assert_equal [true] * 8, threads.map(&:value) + assert_equal features, $LOADED_FEATURES.size + assert_equal [path], $autoload_required_loads + assert_same klass, table_slot(TestAutoload, :Concurrent) + end + + # Zeitwerk's shape: the loader registers one autoload per file, eager loading requires each file. + def test_eager_loading_after_autoload_registration + files = Dir["#{AUTOLOAD_REQUIRED}/eager/*.rb"].sort + files.each { |f| Eager.autoload(File.basename(f, ".rb").capitalize.to_sym, f) } + features = $LOADED_FEATURES.dup + files.each { |f| require f } + assert_equal files, $autoload_required_loads + %i[Alpha Beta Gamma].each do |name| + assert_nil Eager.autoload?(name) + assert_same Eager.const_get(name), table_slot(Eager, name) + end + assert_same Eager::Alpha, Eager::Beta.superclass + + # a regular constant now: no feature bookkeeping is consulted, so nothing is loaded twice + loaded = $LOADED_FEATURES.dup + $LOADED_FEATURES.replace(features) + begin + assert_same Eager::Gamma, in_thread { TestAutoload::Eager::Gamma } + assert_same Eager::Beta, in_thread { TestAutoload::Eager::Beta } + ensure + $LOADED_FEATURES.replace(loaded) + end + assert_equal files, $autoload_required_loads + end + end From 50052c4d2926095cc54ae69454bf53be25186c3d Mon Sep 17 00:00:00 2001 From: Amin Mansuri Date: Sat, 12 Sep 2026 08:59:49 -0300 Subject: [PATCH 3/4] Specs for sibling constants and circular requires between autoloaded files --- spec/ruby/core/module/autoload_spec.rb | 30 +++++++++++++++++++ .../core/module/fixtures/autoload_siblings.rb | 9 ++++++ .../jruby/autoload_required/circular_first.rb | 9 ++++++ .../autoload_required/circular_second.rb | 9 ++++++ test/jruby/autoload_required/pair.rb | 11 +++++++ test/jruby/test_autoload.rb | 28 +++++++++++++++++ 6 files changed, 96 insertions(+) create mode 100644 spec/ruby/core/module/fixtures/autoload_siblings.rb create mode 100644 test/jruby/autoload_required/circular_first.rb create mode 100644 test/jruby/autoload_required/circular_second.rb create mode 100644 test/jruby/autoload_required/pair.rb diff --git a/spec/ruby/core/module/autoload_spec.rb b/spec/ruby/core/module/autoload_spec.rb index a1d0b79f430..2cf8b014278 100644 --- a/spec/ruby/core/module/autoload_spec.rb +++ b/spec/ruby/core/module/autoload_spec.rb @@ -422,6 +422,36 @@ module ModuleSpecs::Autoload end end + describe "when one file defines several autoloaded constants" do + before :each do + @path = fixture(__FILE__, "autoload_siblings.rb") + ModuleSpecs::Autoload.autoload :SiblingA, @path + ModuleSpecs::Autoload.autoload :SiblingB, @path + @remove << :SiblingA << :SiblingB + ScratchPad.record [] + end + + it "defines every constant the file declares as a regular constant" do + ModuleSpecs::Autoload::SiblingA + ScratchPad.recorded.should == [:loaded] + ModuleSpecs::Autoload.autoload?(:SiblingB).should be_nil + klass = ModuleSpecs::Autoload::SiblingB + data = Marshal.dump(klass.new) + + $LOADED_FEATURES.replace(@loaded_features) + Thread.new { Marshal.load(data).class }.value.should equal(klass) + ScratchPad.recorded.should == [:loaded] + end + + it "returns the sibling constant from remove_const" do + ModuleSpecs::Autoload::SiblingA + klass = ModuleSpecs::Autoload::SiblingB + @remove.delete(:SiblingB) + + ModuleSpecs::Autoload.send(:remove_const, :SiblingB).should equal(klass) + end + end + describe "after the autoload is triggered by require" do before :each do @path = tmp("autoload.rb") diff --git a/spec/ruby/core/module/fixtures/autoload_siblings.rb b/spec/ruby/core/module/fixtures/autoload_siblings.rb new file mode 100644 index 00000000000..862bab23a79 --- /dev/null +++ b/spec/ruby/core/module/fixtures/autoload_siblings.rb @@ -0,0 +1,9 @@ +ScratchPad << :loaded + +module ModuleSpecs::Autoload + class SiblingA + end + + class SiblingB + end +end diff --git a/test/jruby/autoload_required/circular_first.rb b/test/jruby/autoload_required/circular_first.rb new file mode 100644 index 00000000000..25d294885a7 --- /dev/null +++ b/test/jruby/autoload_required/circular_first.rb @@ -0,0 +1,9 @@ +$autoload_required_loads << __FILE__ +require File.expand_path("circular_second", __dir__) + +class TestAutoload + module Circular + class First + end + end +end diff --git a/test/jruby/autoload_required/circular_second.rb b/test/jruby/autoload_required/circular_second.rb new file mode 100644 index 00000000000..f910679232f --- /dev/null +++ b/test/jruby/autoload_required/circular_second.rb @@ -0,0 +1,9 @@ +$autoload_required_loads << __FILE__ +require File.expand_path("circular_first", __dir__) + +class TestAutoload + module Circular + class Second + end + end +end diff --git a/test/jruby/autoload_required/pair.rb b/test/jruby/autoload_required/pair.rb new file mode 100644 index 00000000000..70cf3430f15 --- /dev/null +++ b/test/jruby/autoload_required/pair.rb @@ -0,0 +1,11 @@ +$autoload_required_loads << __FILE__ + +class TestAutoload + module Siblings + class PairA + end + + class PairB + end + end +end diff --git a/test/jruby/test_autoload.rb b/test/jruby/test_autoload.rb index 65724836c03..e3629a90bed 100644 --- a/test/jruby/test_autoload.rb +++ b/test/jruby/test_autoload.rb @@ -51,6 +51,8 @@ class SomeClass; def foo; true; end; end module Eager; end module Imported; end + module Siblings; end + module Circular; end def setup $autoload_required_loads = [] @@ -173,4 +175,30 @@ def test_eager_loading_after_autoload_registration assert_equal files, $autoload_required_loads end + # One file defines two autoloaded constants (ActiveSupport's autoload_at shape); no direct require. + def test_sibling_constant_defined_by_the_same_autoload_stores_constant_in_table + path = "#{AUTOLOAD_REQUIRED}/pair.rb" + Siblings.autoload(:PairA, path) + Siblings.autoload(:PairB, path) + Siblings::PairA + assert_equal [path], $autoload_required_loads + assert_nil Siblings.autoload?(:PairB) + assert_same Siblings::PairA, table_slot(Siblings, :PairA) + assert_same Siblings::PairB, table_slot(Siblings, :PairB) + assert_same Siblings::PairB, in_thread { Marshal.load(Marshal.dump(TestAutoload::Siblings::PairB.new)).class } + assert_equal [path], $autoload_required_loads + end + + # The autoloaded file requires another autoloaded file, which requires the first one back. + def test_circular_require_between_autoloaded_files_stores_both_constants + Circular.autoload(:First, "#{AUTOLOAD_REQUIRED}/circular_first.rb") + Circular.autoload(:Second, "#{AUTOLOAD_REQUIRED}/circular_second.rb") + verbose, $VERBOSE = $VERBOSE, nil + Circular::First + $VERBOSE = verbose + assert_nil Circular.autoload?(:Second) + assert_same Circular::First, table_slot(Circular, :First) + assert_same Circular::Second, table_slot(Circular, :Second) + end + end From d47167c5c28dde2e38c8b20bb8ff836e54f9ffab Mon Sep 17 00:00:00 2001 From: Amin Mansuri Date: Sat, 12 Sep 2026 10:44:22 -0300 Subject: [PATCH 4/4] Avoid a void-context warning in test_autoload.rb --- test/jruby/test_autoload.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/jruby/test_autoload.rb b/test/jruby/test_autoload.rb index e3629a90bed..0fe0a028547 100644 --- a/test/jruby/test_autoload.rb +++ b/test/jruby/test_autoload.rb @@ -180,7 +180,7 @@ def test_sibling_constant_defined_by_the_same_autoload_stores_constant_in_table path = "#{AUTOLOAD_REQUIRED}/pair.rb" Siblings.autoload(:PairA, path) Siblings.autoload(:PairB, path) - Siblings::PairA + assert_kind_of Class, Siblings::PairA assert_equal [path], $autoload_required_loads assert_nil Siblings.autoload?(:PairB) assert_same Siblings::PairA, table_slot(Siblings, :PairA) @@ -194,7 +194,7 @@ def test_circular_require_between_autoloaded_files_stores_both_constants Circular.autoload(:First, "#{AUTOLOAD_REQUIRED}/circular_first.rb") Circular.autoload(:Second, "#{AUTOLOAD_REQUIRED}/circular_second.rb") verbose, $VERBOSE = $VERBOSE, nil - Circular::First + assert_kind_of Class, Circular::First $VERBOSE = verbose assert_nil Circular.autoload?(:Second) assert_same Circular::First, table_slot(Circular, :First)