diff options
Diffstat (limited to 'spec/ruby/core/module/autoload_spec.rb')
| -rw-r--r-- | spec/ruby/core/module/autoload_spec.rb | 740 |
1 files changed, 630 insertions, 110 deletions
diff --git a/spec/ruby/core/module/autoload_spec.rb b/spec/ruby/core/module/autoload_spec.rb index a72ae7735b..057237a92f 100644 --- a/spec/ruby/core/module/autoload_spec.rb +++ b/spec/ruby/core/module/autoload_spec.rb @@ -1,6 +1,6 @@ -require File.expand_path('../../../spec_helper', __FILE__) -require File.expand_path('../fixtures/classes', __FILE__) -require 'thread' +require_relative '../../spec_helper' +require_relative '../../fixtures/code_loading' +require_relative 'fixtures/classes' describe "Module#autoload?" do it "returns the name of the file that will be autoloaded" do @@ -9,24 +9,43 @@ describe "Module#autoload?" do end it "returns nil if no file has been registered for a constant" do - ModuleSpecs::Autoload.autoload?(:Manualload).should be_nil + ModuleSpecs::Autoload.autoload?(:Manualload).should == nil + end + + it "returns the name of the file that will be autoloaded if an ancestor defined that autoload" do + ModuleSpecs::Autoload::Parent.autoload :AnotherAutoload, "another_autoload.rb" + ModuleSpecs::Autoload::Child.autoload?(:AnotherAutoload).should == "another_autoload.rb" + end + + it "returns nil if an ancestor defined that autoload but recursion is disabled" do + ModuleSpecs::Autoload::Parent.autoload :InheritedAutoload, "inherited_autoload.rb" + ModuleSpecs::Autoload::Child.autoload?(:InheritedAutoload, false).should == nil + end + + it "returns the name of the file that will be loaded if recursion is disabled but the autoload is defined on the class itself" do + ModuleSpecs::Autoload::Child.autoload :ChildAutoload, "child_autoload.rb" + ModuleSpecs::Autoload::Child.autoload?(:ChildAutoload, false).should == "child_autoload.rb" end end describe "Module#autoload" do before :all do @non_existent = fixture __FILE__, "no_autoload.rb" + CodeLoadingSpecs.preload_rubygems end before :each do @loaded_features = $".dup - @frozen_module = Module.new.freeze ScratchPad.clear + @remove = [] end after :each do $".replace @loaded_features + @remove.each { |const| + ModuleSpecs::Autoload.send :remove_const, const + } end it "registers a file to load the first time the named constant is accessed" do @@ -36,21 +55,34 @@ describe "Module#autoload" do it "sets the autoload constant in the constants table" do ModuleSpecs::Autoload.autoload :B, @non_existent - ModuleSpecs::Autoload.should have_constant(:B) + ModuleSpecs::Autoload.should.const_defined?(:B, false) + end + + it "can be overridden with a second autoload on the same constant" do + ModuleSpecs::Autoload.autoload :Overridden, @non_existent + @remove << :Overridden + ModuleSpecs::Autoload.autoload?(:Overridden).should == @non_existent + + path = fixture(__FILE__, "autoload_overridden.rb") + ModuleSpecs::Autoload.autoload :Overridden, path + ModuleSpecs::Autoload.autoload?(:Overridden).should == path + + ModuleSpecs::Autoload::Overridden.should == :overridden end it "loads the registered constant when it is accessed" do - ModuleSpecs::Autoload.should_not have_constant(:X) + ModuleSpecs::Autoload.should_not.const_defined?(:X) ModuleSpecs::Autoload.autoload :X, fixture(__FILE__, "autoload_x.rb") + @remove << :X ModuleSpecs::Autoload::X.should == :x - ModuleSpecs::Autoload.send(:remove_const, :X) end it "loads the registered constant into a dynamically created class" do cls = Class.new { autoload :C, fixture(__FILE__, "autoload_c.rb") } ModuleSpecs::Autoload::DynClass = cls + @remove << :DynClass - ScratchPad.recorded.should be_nil + ScratchPad.recorded.should == nil ModuleSpecs::Autoload::DynClass::C.new.loaded.should == :dynclass_c ScratchPad.recorded.should == :loaded end @@ -58,8 +90,9 @@ describe "Module#autoload" do it "loads the registered constant into a dynamically created module" do mod = Module.new { autoload :D, fixture(__FILE__, "autoload_d.rb") } ModuleSpecs::Autoload::DynModule = mod + @remove << :DynModule - ScratchPad.recorded.should be_nil + ScratchPad.recorded.should == nil ModuleSpecs::Autoload::DynModule::D.new.loaded.should == :dynmodule_d ScratchPad.recorded.should == :loaded end @@ -95,9 +128,10 @@ describe "Module#autoload" do it "does not load the file when the constant is already set" do ModuleSpecs::Autoload.autoload :I, fixture(__FILE__, "autoload_i.rb") + @remove << :I ModuleSpecs::Autoload.const_set :I, 3 ModuleSpecs::Autoload::I.should == 3 - ScratchPad.recorded.should be_nil + ScratchPad.recorded.should == nil end it "loads a file with .rb extension when passed the name without the extension" do @@ -105,17 +139,26 @@ describe "Module#autoload" do ModuleSpecs::Autoload::J.should == :autoload_j end + it "calls main.require(path) to load the file" do + ModuleSpecs::Autoload.autoload :ModuleAutoloadCallsRequire, "module_autoload_not_exist.rb" + main = TOPLEVEL_BINDING.eval("self") + main.should_receive(:require).with("module_autoload_not_exist.rb") + # The constant won't be defined since require is mocked to do nothing + -> { ModuleSpecs::Autoload::ModuleAutoloadCallsRequire }.should.raise(NameError) + end + it "does not load the file if the file is manually required" do filename = fixture(__FILE__, "autoload_k.rb") ModuleSpecs::Autoload.autoload :KHash, filename + @remove << :KHash require filename ScratchPad.recorded.should == :loaded ScratchPad.clear - ModuleSpecs::Autoload::KHash.should be_kind_of(Class) + ModuleSpecs::Autoload::KHash.should.is_a?(Class) ModuleSpecs::Autoload::KHash::K.should == :autoload_k - ScratchPad.recorded.should be_nil + ScratchPad.recorded.should == nil end it "ignores the autoload request if the file is already loaded" do @@ -127,8 +170,8 @@ describe "Module#autoload" do ScratchPad.clear ModuleSpecs::Autoload.autoload :S, filename - ModuleSpecs::Autoload.autoload?(:S).should be_nil - ModuleSpecs::Autoload.send(:remove_const, :S) + @remove << :S + ModuleSpecs::Autoload.autoload?(:S).should == nil end it "retains the autoload even if the request to require fails" do @@ -137,9 +180,9 @@ describe "Module#autoload" do ModuleSpecs::Autoload.autoload :NotThere, filename ModuleSpecs::Autoload.autoload?(:NotThere).should == filename - lambda { + -> { require filename - }.should raise_error(LoadError) + }.should.raise(LoadError) ModuleSpecs::Autoload.autoload?(:NotThere).should == filename end @@ -158,28 +201,322 @@ describe "Module#autoload" do ModuleSpecs::Autoload.use_ex1.should == :good end - it "does not load the file when referring to the constant in defined?" do - module ModuleSpecs::Autoload::Q - autoload :R, fixture(__FILE__, "autoload.rb") - defined?(R).should == "constant" + it "considers an autoload constant as loaded when autoload is called for/from the current file" do + filename = fixture(__FILE__, "autoload_during_require_current_file.rb") + require filename + + ScratchPad.recorded.should == nil + end + + describe "interacting with defined?" do + it "does not load the file when referring to the constant in defined?" do + module ModuleSpecs::Autoload::Dog + autoload :R, fixture(__FILE__, "autoload_exception.rb") + end + + defined?(ModuleSpecs::Autoload::Dog::R).should == "constant" + ScratchPad.recorded.should == nil + + ModuleSpecs::Autoload::Dog.should.const_defined?(:R, false) + end + + it "loads an autoloaded parent when referencing a nested constant" do + module ModuleSpecs::Autoload + autoload :GoodParent, fixture(__FILE__, "autoload_nested.rb") + end + @remove << :GoodParent + + defined?(ModuleSpecs::Autoload::GoodParent::Nested).should == 'constant' + ScratchPad.recorded.should == :loaded + end + + it "returns nil when it fails to load an autoloaded parent when referencing a nested constant" do + module ModuleSpecs::Autoload + autoload :BadParent, fixture(__FILE__, "autoload_exception.rb") + end + + defined?(ModuleSpecs::Autoload::BadParent::Nested).should == nil + ScratchPad.recorded.should == :exception + end + end + + describe "the autoload is triggered when the same file is required directly" do + before :each do + module ModuleSpecs::Autoload + autoload :RequiredDirectly, fixture(__FILE__, "autoload_required_directly.rb") + end + @remove << :RequiredDirectly + @path = fixture(__FILE__, "autoload_required_directly.rb") + @check = -> { + [ + defined?(ModuleSpecs::Autoload::RequiredDirectly), + ModuleSpecs::Autoload.autoload?(:RequiredDirectly) + ] + } + ScratchPad.record @check + end + + it "with a full path" do + @check.call.should == ["constant", @path] + require @path + ScratchPad.recorded.should == [nil, nil] + @check.call.should == ["constant", nil] + end + + it "with a relative path" do + @check.call.should == ["constant", @path] + $:.push File.dirname(@path) + begin + require "autoload_required_directly.rb" + ensure + $:.pop + end + ScratchPad.recorded.should == [nil, nil] + @check.call.should == ["constant", nil] + end + + it "in a nested require" do + nested = fixture(__FILE__, "autoload_required_directly_nested.rb") + nested_require = -> { + result = nil + ScratchPad.record -> { + result = @check.call + } + require nested + result + } + ScratchPad.record nested_require + + @check.call.should == ["constant", @path] + require @path + ScratchPad.recorded.should == [nil, nil] + @check.call.should == ["constant", nil] + end + + it "does not raise an error if the autoload constant was not defined" do + module ModuleSpecs::Autoload + autoload :RequiredDirectlyNoConstant, fixture(__FILE__, "autoload_required_directly_no_constant.rb") + end + @path = fixture(__FILE__, "autoload_required_directly_no_constant.rb") + @remove << :RequiredDirectlyNoConstant + @check = -> { + [ + defined?(ModuleSpecs::Autoload::RequiredDirectlyNoConstant), + ModuleSpecs::Autoload.constants(false).include?(:RequiredDirectlyNoConstant), + ModuleSpecs::Autoload.const_defined?(:RequiredDirectlyNoConstant), + ModuleSpecs::Autoload.autoload?(:RequiredDirectlyNoConstant) + ] + } + ScratchPad.record @check + @check.call.should == ["constant", true, true, @path] + $:.push File.dirname(@path) + begin + require "autoload_required_directly_no_constant.rb" + ensure + $:.pop + end + ScratchPad.recorded.should == [nil, true, false, nil] + @check.call.should == [nil, true, false, nil] + end + end + + describe "after the autoload is triggered by require" do + before :each do + @path = tmp("autoload.rb") + end + + after :each do + rm_r @path + end + + it "the mapping feature to autoload is removed, and a new autoload with the same path is considered" do + ModuleSpecs::Autoload.autoload :RequireMapping1, @path + touch(@path) { |f| f.puts "ModuleSpecs::Autoload::RequireMapping1 = 1" } + ModuleSpecs::Autoload::RequireMapping1.should == 1 + + $LOADED_FEATURES.delete(@path) + ModuleSpecs::Autoload.autoload :RequireMapping2, @path[0...-3] + @remove << :RequireMapping2 + touch(@path) { |f| f.puts "ModuleSpecs::Autoload::RequireMapping2 = 2" } + ModuleSpecs::Autoload::RequireMapping2.should == 2 + end + end + + def check_before_during_thread_after(const, &check) + before = check.call + to_autoload_thread, from_autoload_thread = Queue.new, Queue.new + ScratchPad.record -> { + from_autoload_thread.push check.call + to_autoload_thread.pop + } + t = Thread.new { + in_loading_thread = from_autoload_thread.pop + in_other_thread = check.call + to_autoload_thread.push :done + [in_loading_thread, in_other_thread] + } + in_loading_thread, in_other_thread = nil + begin + ModuleSpecs::Autoload.const_get(const) + ensure + in_loading_thread, in_other_thread = t.value + end + after = check.call + [before, in_loading_thread, in_other_thread, after] + end + + describe "during the autoload before the constant is assigned" do + before :each do + @path = fixture(__FILE__, "autoload_during_autoload.rb") + ModuleSpecs::Autoload.autoload :DuringAutoload, @path + @remove << :DuringAutoload + raise unless ModuleSpecs::Autoload.autoload?(:DuringAutoload) == @path + end + + it "returns nil in autoload thread and 'constant' otherwise for defined?" do + results = check_before_during_thread_after(:DuringAutoload) { + defined?(ModuleSpecs::Autoload::DuringAutoload) + } + results.should == ['constant', nil, 'constant', 'constant'] + end + + it "keeps the constant in Module#constants" do + results = check_before_during_thread_after(:DuringAutoload) { + ModuleSpecs::Autoload.constants(false).include?(:DuringAutoload) + } + results.should == [true, true, true, true] + end + + it "returns false in autoload thread and true otherwise for Module#const_defined?" do + results = check_before_during_thread_after(:DuringAutoload) { + ModuleSpecs::Autoload.const_defined?(:DuringAutoload, false) + } + results.should == [true, false, true, true] + end + + it "returns nil in autoload thread and returns the path in other threads for Module#autoload?" do + results = check_before_during_thread_after(:DuringAutoload) { + ModuleSpecs::Autoload.autoload?(:DuringAutoload) + } + results.should == [@path, nil, @path, nil] + end + end + + describe "during the autoload after the constant is assigned" do + before :each do + @path = fixture(__FILE__, "autoload_during_autoload_after_define.rb") + ModuleSpecs::Autoload.autoload :DuringAutoloadAfterDefine, @path + @autoload_location = [__FILE__, __LINE__ - 1] + @const_location = [@path, 2] + @remove << :DuringAutoloadAfterDefine + raise unless ModuleSpecs::Autoload.autoload?(:DuringAutoloadAfterDefine) == @path + end + + it "returns 'constant' in both threads" do + results = check_before_during_thread_after(:DuringAutoloadAfterDefine) { + defined?(ModuleSpecs::Autoload::DuringAutoloadAfterDefine) + } + results.should == ['constant', 'constant', 'constant', 'constant'] + end + + it "Module#constants include the autoloaded in both threads" do + results = check_before_during_thread_after(:DuringAutoloadAfterDefine) { + ModuleSpecs::Autoload.constants(false).include?(:DuringAutoloadAfterDefine) + } + results.should == [true, true, true, true] + end + + it "Module#const_defined? returns true in both threads" do + results = check_before_during_thread_after(:DuringAutoloadAfterDefine) { + ModuleSpecs::Autoload.const_defined?(:DuringAutoloadAfterDefine, false) + } + results.should == [true, true, true, true] + end + + it "returns nil in autoload thread and returns the path in other threads for Module#autoload?" do + results = check_before_during_thread_after(:DuringAutoloadAfterDefine) { + ModuleSpecs::Autoload.autoload?(:DuringAutoloadAfterDefine) + } + results.should == [@path, nil, @path, nil] + end + + ruby_bug("#20188", ""..."3.4") do + it "returns the real constant location in autoload thread and returns the autoload location in other threads for Module#const_source_location" do + results = check_before_during_thread_after(:DuringAutoloadAfterDefine) { + ModuleSpecs::Autoload.const_source_location(:DuringAutoloadAfterDefine) + } + results.should == [@autoload_location, @const_location, @autoload_location, @const_location] + end end - ModuleSpecs::Autoload::Q.should have_constant(:R) end - it "does not remove the constant from the constant table if load fails" do + it "does not remove the constant from Module#constants if load fails and keeps it as an autoload" do ModuleSpecs::Autoload.autoload :Fail, @non_existent - ModuleSpecs::Autoload.should have_constant(:Fail) - lambda { ModuleSpecs::Autoload::Fail }.should raise_error(LoadError) - ModuleSpecs::Autoload.should have_constant(:Fail) + ModuleSpecs::Autoload.const_defined?(:Fail).should == true + ModuleSpecs::Autoload.should.const_defined?(:Fail, false) + ModuleSpecs::Autoload.autoload?(:Fail).should == @non_existent + + -> { ModuleSpecs::Autoload::Fail }.should.raise(LoadError) + + ModuleSpecs::Autoload.should.const_defined?(:Fail, false) + ModuleSpecs::Autoload.const_defined?(:Fail).should == true + ModuleSpecs::Autoload.autoload?(:Fail).should == @non_existent + + -> { ModuleSpecs::Autoload::Fail }.should.raise(LoadError) + end + + it "does not remove the constant from Module#constants if load raises a RuntimeError and keeps it as an autoload" do + path = fixture(__FILE__, "autoload_raise.rb") + ScratchPad.record [] + ModuleSpecs::Autoload.autoload :Raise, path + + ModuleSpecs::Autoload.const_defined?(:Raise).should == true + ModuleSpecs::Autoload.should.const_defined?(:Raise, false) + ModuleSpecs::Autoload.autoload?(:Raise).should == path + + -> { ModuleSpecs::Autoload::Raise }.should.raise(RuntimeError) + ScratchPad.recorded.should == [:raise] + + ModuleSpecs::Autoload.should.const_defined?(:Raise, false) + ModuleSpecs::Autoload.const_defined?(:Raise).should == true + ModuleSpecs::Autoload.autoload?(:Raise).should == path + + -> { ModuleSpecs::Autoload::Raise }.should.raise(RuntimeError) + ScratchPad.recorded.should == [:raise, :raise] end - it "does not remove the constant from the constant table if the loaded files does not define it" do - ModuleSpecs::Autoload.autoload :O, fixture(__FILE__, "autoload_o.rb") - ModuleSpecs::Autoload.should have_constant(:O) + it "removes the constant from Module#constants if the loaded file does not define it" do + path = fixture(__FILE__, "autoload_o.rb") + ScratchPad.record [] + ModuleSpecs::Autoload.autoload :O, path + + ModuleSpecs::Autoload.const_defined?(:O).should == true + ModuleSpecs::Autoload.should.const_defined?(:O, false) + ModuleSpecs::Autoload.autoload?(:O).should == path + + -> { ModuleSpecs::Autoload::O }.should.raise(NameError) - lambda { ModuleSpecs::Autoload::O }.should raise_error(NameError) - ModuleSpecs::Autoload.should have_constant(:O) + ModuleSpecs::Autoload.const_defined?(:O).should == false + ModuleSpecs::Autoload.should_not.const_defined?(:O) + ModuleSpecs::Autoload.autoload?(:O).should == nil + -> { ModuleSpecs::Autoload.const_get(:O) }.should.raise(NameError) + end + + it "does not try to load the file again if the loaded file did not define the constant" do + path = fixture(__FILE__, "autoload_o.rb") + ScratchPad.record [] + ModuleSpecs::Autoload.autoload :NotDefinedByFile, path + + -> { ModuleSpecs::Autoload::NotDefinedByFile }.should.raise(NameError) + ScratchPad.recorded.should == [:loaded] + -> { ModuleSpecs::Autoload::NotDefinedByFile }.should.raise(NameError) + ScratchPad.recorded.should == [:loaded] + + Thread.new { + -> { ModuleSpecs::Autoload::NotDefinedByFile }.should.raise(NameError) + }.join + ScratchPad.recorded.should == [:loaded] end it "returns 'constant' on referring the constant with defined?()" do @@ -187,7 +524,7 @@ describe "Module#autoload" do autoload :R, fixture(__FILE__, "autoload.rb") defined?(R).should == 'constant' end - ModuleSpecs::Autoload::Q.should have_constant(:R) + ModuleSpecs::Autoload::Q.should.const_defined?(:R, false) end it "does not load the file when removing an autoload constant" do @@ -195,12 +532,13 @@ describe "Module#autoload" do autoload :R, fixture(__FILE__, "autoload.rb") remove_const :R end - ModuleSpecs::Autoload::Q.should_not have_constant(:R) + ModuleSpecs::Autoload::Q.should_not.const_defined?(:R) end it "does not load the file when accessing the constants table of the module" do ModuleSpecs::Autoload.autoload :P, @non_existent - ModuleSpecs::Autoload.const_defined?(:P).should be_true + ModuleSpecs::Autoload.const_defined?(:P).should == true + ModuleSpecs::Autoload.const_defined?("P").should == true end it "loads the file when opening a module that is the autoloaded constant" do @@ -211,43 +549,188 @@ describe "Module#autoload" do X = get_value end end + @remove << :U ModuleSpecs::Autoload::U::V::X.should == :autoload_uvx end - it "loads the file that defines subclass XX::YY < YY and YY is a top level constant" do - + it "loads the file that defines subclass XX::CS_CONST_AUTOLOAD < CS_CONST_AUTOLOAD and CS_CONST_AUTOLOAD is a top level constant" do module ModuleSpecs::Autoload::XX - autoload :YY, fixture(__FILE__, "autoload_subclass.rb") + autoload :CS_CONST_AUTOLOAD, fixture(__FILE__, "autoload_subclass.rb") end - ModuleSpecs::Autoload::XX::YY.superclass.should == YY + ModuleSpecs::Autoload::XX::CS_CONST_AUTOLOAD.superclass.should == CS_CONST_AUTOLOAD end + describe "after autoloading searches for the constant like the original lookup" do + it "in lexical scopes if both declared and defined in parent" do + module ModuleSpecs::Autoload + ScratchPad.record -> { + DeclaredAndDefinedInParent = :declared_and_defined_in_parent + } + autoload :DeclaredAndDefinedInParent, fixture(__FILE__, "autoload_callback.rb") + class LexicalScope + DeclaredAndDefinedInParent.should == :declared_and_defined_in_parent + + # The constant is really in Autoload, not Autoload::LexicalScope + self.should_not.const_defined?(:DeclaredAndDefinedInParent) + -> { const_get(:DeclaredAndDefinedInParent) }.should.raise(NameError) + end + DeclaredAndDefinedInParent.should == :declared_and_defined_in_parent + end + end + + it "in lexical scopes if declared in parent and defined in current" do + module ModuleSpecs::Autoload + ScratchPad.record -> { + class LexicalScope + DeclaredInParentDefinedInCurrent = :declared_in_parent_defined_in_current + end + } + autoload :DeclaredInParentDefinedInCurrent, fixture(__FILE__, "autoload_callback.rb") - it "looks up the constant in the scope where it is referred" do - module ModuleSpecs - module Autoload - autoload :QQ, fixture(__FILE__, "autoload_scope.rb") - class PP - QQ.new.should be_kind_of(ModuleSpecs::Autoload::PP::QQ) + class LexicalScope + DeclaredInParentDefinedInCurrent.should == :declared_in_parent_defined_in_current + LexicalScope::DeclaredInParentDefinedInCurrent.should == :declared_in_parent_defined_in_current end + + # Basically, the parent autoload constant remains in a "undefined" state + self.autoload?(:DeclaredInParentDefinedInCurrent).should == nil + const_defined?(:DeclaredInParentDefinedInCurrent).should == false + -> { DeclaredInParentDefinedInCurrent }.should.raise(NameError) + + ModuleSpecs::Autoload::LexicalScope.send(:remove_const, :DeclaredInParentDefinedInCurrent) + end + end + + it "warns once in verbose mode if the constant was defined in a parent scope" do + ScratchPad.record -> { + ModuleSpecs::DeclaredInCurrentDefinedInParent = :declared_in_current_defined_in_parent + } + + module ModuleSpecs + module Autoload + autoload :DeclaredInCurrentDefinedInParent, fixture(__FILE__, "autoload_callback.rb") + self.autoload?(:DeclaredInCurrentDefinedInParent).should == fixture(__FILE__, "autoload_callback.rb") + const_defined?(:DeclaredInCurrentDefinedInParent).should == true + + -> { + DeclaredInCurrentDefinedInParent + }.should complain( + /Expected .*autoload_callback.rb to define ModuleSpecs::Autoload::DeclaredInCurrentDefinedInParent but it didn't/, + verbose: true, + ) + + -> { + DeclaredInCurrentDefinedInParent + }.should_not complain(/.*/, verbose: true) + self.autoload?(:DeclaredInCurrentDefinedInParent).should == nil + const_defined?(:DeclaredInCurrentDefinedInParent).should == false + ModuleSpecs.const_defined?(:DeclaredInCurrentDefinedInParent).should == true + end + end + end + + it "looks up in parent scope after failed autoload" do + @remove << :DeclaredInCurrentDefinedInParent + module ModuleSpecs::Autoload + ScratchPad.record -> { + DeclaredInCurrentDefinedInParent = :declared_in_current_defined_in_parent + } + + class LexicalScope + autoload :DeclaredInCurrentDefinedInParent, fixture(__FILE__, "autoload_callback.rb") + -> { DeclaredInCurrentDefinedInParent }.should_not.raise(NameError) + # Basically, the autoload constant remains in a "undefined" state + self.autoload?(:DeclaredInCurrentDefinedInParent).should == nil + const_defined?(:DeclaredInCurrentDefinedInParent).should == false + -> { const_get(:DeclaredInCurrentDefinedInParent) }.should.raise(NameError) + end + + DeclaredInCurrentDefinedInParent.should == :declared_in_current_defined_in_parent + end + end + + it "in the included modules" do + @remove << :DefinedInIncludedModule + module ModuleSpecs::Autoload + ScratchPad.record -> { + module DefinedInIncludedModule + Incl = :defined_in_included_module + end + include DefinedInIncludedModule + } + autoload :Incl, fixture(__FILE__, "autoload_callback.rb") + Incl.should == :defined_in_included_module + end + end + + it "in the included modules of the superclass" do + @remove << :DefinedInSuperclassIncludedModule + module ModuleSpecs::Autoload + class LookupAfterAutoloadSuper + end + class LookupAfterAutoloadChild < LookupAfterAutoloadSuper + end + + ScratchPad.record -> { + module DefinedInSuperclassIncludedModule + InclS = :defined_in_superclass_included_module + end + LookupAfterAutoloadSuper.include DefinedInSuperclassIncludedModule + } + + class LookupAfterAutoloadChild + autoload :InclS, fixture(__FILE__, "autoload_callback.rb") + InclS.should == :defined_in_superclass_included_module + end + end + end + + it "in the prepended modules" do + @remove << :DefinedInPrependedModule + module ModuleSpecs::Autoload + ScratchPad.record -> { + module DefinedInPrependedModule + Prep = :defined_in_prepended_module + end + include DefinedInPrependedModule + } + autoload :Prep, fixture(__FILE__, "autoload_callback.rb") + Prep.should == :defined_in_prepended_module end end - end - it "looks up the constant when in a meta class scope" do - module ModuleSpecs - module Autoload - autoload :R, fixture(__FILE__, "autoload_r.rb") + it "in a meta class scope" do + module ModuleSpecs::Autoload + ScratchPad.record -> { + class MetaScope + end + } + autoload :MetaScope, fixture(__FILE__, "autoload_callback.rb") class << self def r - R.new + MetaScope.new end end end + ModuleSpecs::Autoload.r.should.is_a?(ModuleSpecs::Autoload::MetaScope) end - ModuleSpecs::Autoload.r.should be_kind_of(ModuleSpecs::Autoload::R) + end + + it "should trigger the autoload when using `private_constant`" do + @remove << :DynClass + module ModuleSpecs::Autoload + autoload :DynClass, fixture(__FILE__, "autoload_c.rb") + private_constant :DynClass + + ScratchPad.recorded.should == nil + + DynClass::C.new.loaded.should == :dynclass_c + ScratchPad.recorded.should == :loaded + end + + -> { ModuleSpecs::Autoload::DynClass }.should.raise(NameError, /private constant/) end # [ruby-core:19127] [ruby-core:29941] @@ -260,10 +743,45 @@ describe "Module#autoload" do end end end + @remove << :W - ModuleSpecs::Autoload::W::Y.should be_kind_of(Class) + ModuleSpecs::Autoload::W::Y.should.is_a?(Class) ScratchPad.recorded.should == :loaded - ModuleSpecs::Autoload::W.send(:remove_const, :Y) + end + + it "does not call #require a second time and does not warn if already loading the same feature with #require" do + main = TOPLEVEL_BINDING.eval("self") + main.should_not_receive(:require) + + module ModuleSpecs::Autoload + autoload :AutoloadDuringRequire, fixture(__FILE__, "autoload_during_require.rb") + end + + -> { + Kernel.require fixture(__FILE__, "autoload_during_require.rb") + }.should_not complain(verbose: true) + ModuleSpecs::Autoload::AutoloadDuringRequire.should.is_a?(Class) + end + + it "does not call #require a second time and does not warn if feature sets and trigger autoload on itself" do + main = TOPLEVEL_BINDING.eval("self") + main.should_not_receive(:require) + + -> { + Kernel.require fixture(__FILE__, "autoload_self_during_require.rb") + }.should_not complain(verbose: true) + ModuleSpecs::Autoload::AutoloadSelfDuringRequire.should.is_a?(Class) + end + + it "handles multiple autoloads in the same file" do + $LOAD_PATH.unshift(File.expand_path('../fixtures/multi', __FILE__)) + begin + require 'foo/bar_baz' + ModuleSpecs::Autoload::Foo::Bar.should.is_a?(Class) + ModuleSpecs::Autoload::Foo::Baz.should.is_a?(Class) + ensure + $LOAD_PATH.shift + end end it "calls #to_path on non-string filenames" do @@ -273,26 +791,27 @@ describe "Module#autoload" do end it "raises an ArgumentError when an empty filename is given" do - lambda { ModuleSpecs.autoload :A, "" }.should raise_error(ArgumentError) + -> { ModuleSpecs.autoload :A, "" }.should.raise(ArgumentError) end it "raises a NameError when the constant name starts with a lower case letter" do - lambda { ModuleSpecs.autoload "a", @non_existent }.should raise_error(NameError) + -> { ModuleSpecs.autoload "a", @non_existent }.should.raise(NameError) end it "raises a NameError when the constant name starts with a number" do - lambda { ModuleSpecs.autoload "1two", @non_existent }.should raise_error(NameError) + -> { ModuleSpecs.autoload "1two", @non_existent }.should.raise(NameError) end it "raises a NameError when the constant name has a space in it" do - lambda { ModuleSpecs.autoload "a name", @non_existent }.should raise_error(NameError) + -> { ModuleSpecs.autoload "a name", @non_existent }.should.raise(NameError) end it "shares the autoload request across dup'ed copies of modules" do require fixture(__FILE__, "autoload_s.rb") + @remove << :S filename = fixture(__FILE__, "autoload_t.rb") mod1 = Module.new { autoload :T, filename } - lambda { + -> { ModuleSpecs::Autoload::S = mod1 }.should complain(/already initialized constant/) mod2 = mod1.dup @@ -301,7 +820,7 @@ describe "Module#autoload" do mod2.autoload?(:T).should == filename mod1::T.should == :autoload_t - lambda { mod2::T }.should raise_error(NameError) + -> { mod2::T }.should.raise(NameError) end it "raises a TypeError if opening a class with a different superclass than the class defined in the autoload file" do @@ -309,29 +828,30 @@ describe "Module#autoload" do class ModuleSpecs::Autoload::ZZ end - lambda do + -> do class ModuleSpecs::Autoload::Z < ModuleSpecs::Autoload::ZZ end - end.should raise_error(TypeError) + end.should.raise(TypeError) end - it "raises a TypeError if not passed a String or object respodning to #to_path for the filename" do + it "raises a TypeError if not passed a String or object responding to #to_path for the filename" do name = mock("autoload_name.rb") - lambda { ModuleSpecs::Autoload.autoload :Str, name }.should raise_error(TypeError) + -> { ModuleSpecs::Autoload.autoload :Str, name }.should.raise(TypeError) end it "calls #to_path on non-String filename arguments" do name = mock("autoload_name.rb") name.should_receive(:to_path).and_return("autoload_name.rb") - lambda { ModuleSpecs::Autoload.autoload :Str, name }.should_not raise_error + -> { ModuleSpecs::Autoload.autoload :Str, name }.should_not.raise end describe "on a frozen module" do - it "raises a RuntimeError before setting the name" do - lambda { @frozen_module.autoload :Foo, @non_existent }.should raise_error(RuntimeError) - @frozen_module.should_not have_constant(:Foo) + it "raises a FrozenError before setting the name" do + frozen_module = Module.new.freeze + -> { frozen_module.autoload :Foo, @non_existent }.should.raise(FrozenError) + frozen_module.should_not.const_defined?(:Foo) end end @@ -354,6 +874,7 @@ describe "Module#autoload" do describe "(concurrently)" do it "blocks a second thread while a first is doing the autoload" do ModuleSpecs::Autoload.autoload :Concur, fixture(__FILE__, "autoload_concur.rb") + @remove << :Concur start = false @@ -395,58 +916,57 @@ describe "Module#autoload" do t1_val.should == 1 t2_val.should == t1_val - t2_exc.should be_nil - - ModuleSpecs::Autoload.send(:remove_const, :Concur) + t2_exc.should == nil end - ruby_bug "#10892", ""..."2.3" do - it "blocks others threads while doing an autoload" do - file_path = fixture(__FILE__, "repeated_concurrent_autoload.rb") - autoload_path = file_path.sub(/\.rb\Z/, '') - mod_count = 30 - thread_count = 16 + # https://bugs.ruby-lang.org/issues/10892 + it "blocks others threads while doing an autoload" do + file_path = fixture(__FILE__, "repeated_concurrent_autoload.rb") + autoload_path = file_path.sub(/\.rb\Z/, '') + mod_count = 30 + thread_count = 16 + + mod_names = [] + mod_count.times do |i| + mod_name = :"Mod#{i}" + Object.autoload mod_name, autoload_path + mod_names << mod_name + end - mod_names = [] - mod_count.times do |i| - mod_name = :"Mod#{i}" - Object.autoload mod_name, autoload_path - mod_names << mod_name - end + barrier = ModuleSpecs::CyclicBarrier.new thread_count + ScratchPad.record ModuleSpecs::ThreadSafeCounter.new - barrier = ModuleSpecs::CyclicBarrier.new thread_count - ScratchPad.record ModuleSpecs::ThreadSafeCounter.new - - threads = (1..thread_count).map do - Thread.new do - mod_names.each do |mod_name| - break false unless barrier.enabled? - - was_last_one_in = barrier.await # wait for all threads to finish the iteration - # clean up so we can autoload the same file again - $LOADED_FEATURES.delete(file_path) if was_last_one_in && $LOADED_FEATURES.include?(file_path) - barrier.await # get ready for race - - begin - Object.const_get(mod_name).foo - rescue NoMethodError - barrier.disable! - break false - end + threads = (1..thread_count).map do + Thread.new do + mod_names.each do |mod_name| + break false unless barrier.enabled? + + was_last_one_in = barrier.await # wait for all threads to finish the iteration + # clean up so we can autoload the same file again + $LOADED_FEATURES.delete(file_path) if was_last_one_in && $LOADED_FEATURES.include?(file_path) + barrier.await # get ready for race + + begin + Object.const_get(mod_name).foo + rescue NameError, NoMethodError # rubocop:disable Lint/ShadowedException + barrier.disable! + break false end end end + end - # check that no thread got a NoMethodError because of partially loaded module - threads.all? {|t| t.value}.should be_true + # check that no thread got a NameError or NoMethodError because of partially loaded module + threads.all? {|t| t.value}.should == true - # check that the autoloaded file was evaled exactly once - ScratchPad.recorded.get.should == mod_count + # check that the autoloaded file was evaled exactly once + ScratchPad.recorded.get.should == mod_count - mod_names.each do |mod_name| - Object.send(:remove_const, mod_name) - end + mod_names.each do |mod_name| + Object.send(:remove_const, mod_name) end + ensure + threads.each(&:join) if threads end it "raises a NameError in each thread if the constant is not set" do @@ -470,7 +990,7 @@ describe "Module#autoload" do start = true threads.each { |t| - t.value.should be_an_instance_of(NameError) + t.value.should.instance_of?(NameError) } end @@ -495,7 +1015,7 @@ describe "Module#autoload" do start = true threads.each { |t| - t.value.should be_an_instance_of(LoadError) + t.value.should.instance_of?(LoadError) } end end |
