require_relative '../../spec_helper' require_relative '../../fixtures/code_loading' require_relative 'fixtures/classes' require 'thread' describe "Module#autoload?" do it "returns the name of the file that will be autoloaded" do ModuleSpecs::Autoload.autoload :Autoload, "autoload.rb" ModuleSpecs::Autoload.autoload?(:Autoload).should == "autoload.rb" end it "returns nil if no file has been registered for a constant" do ModuleSpecs::Autoload.autoload?(:Manualload).should be_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 ruby_version_is "2.7" do 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 be_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 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 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 ModuleSpecs::Autoload.autoload :A, @non_existent ModuleSpecs::Autoload.autoload?(:A).should == @non_existent end it "sets the autoload constant in the constants table" do ModuleSpecs::Autoload.autoload :B, @non_existent ModuleSpecs::Autoload.should have_constant(:B) 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.autoload :X, fixture(__FILE__, "autoload_x.rb") @remove << :X ModuleSpecs::Autoload::X.should == :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 ModuleSpecs::Autoload::DynClass::C.new.loaded.should == :dynclass_c ScratchPad.recorded.should == :loaded end 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 ModuleSpecs::Autoload::DynModule::D.new.loaded.should == :dynmodule_d ScratchPad.recorded.should == :loaded end it "loads the registered constant when it is opened as a class" do ModuleSpecs::Autoload.autoload :E, fixture(__FILE__, "autoload_e.rb") class ModuleSpecs::Autoload::E end ModuleSpecs::Autoload::E.new.loaded.should == :autoload_e end it "loads the registered constant when it is opened as a module" do ModuleSpecs::Autoload.autoload :F, fixture(__FILE__, "autoload_f.rb") module ModuleSpecs::Autoload::F end ModuleSpecs::Autoload::F.loaded.should == :autoload_f end it "loads the registered constant when it is inherited from" do ModuleSpecs::Autoload.autoload :G, fixture(__FILE__, "autoload_g.rb") class ModuleSpecs::Autoload::Gsub < ModuleSpecs::Autoload::G end ModuleSpecs::Autoload::Gsub.new.loaded.should == :autoload_g end it "loads the registered constant when it is included" do ModuleSpecs::Autoload.autoload :H, fixture(__FILE__, "autoload_h.rb") class ModuleSpecs::Autoload::HClass include ModuleSpecs::Autoload::H end ModuleSpecs::Autoload::HClass.new.loaded.should == :autoload_h end 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 end it "loads a file with .rb extension when passed the name without the extension" do ModuleSpecs::Autoload.autoload :J, fixture(__FILE__, "autoload_j") 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_error(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::K.should == :autoload_k ScratchPad.recorded.should be_nil end it "ignores the autoload request if the file is already loaded" do filename = fixture(__FILE__, "autoload_s.rb") require filename ScratchPad.recorded.should == :loaded ScratchPad.clear ModuleSpecs::Autoload.autoload :S, filename @remove << :S ModuleSpecs::Autoload.autoload?(:S).should be_nil end it "retains the autoload even if the request to require fails" do filename = fixture(__FILE__, "a_path_that_should_not_exist.rb") ModuleSpecs::Autoload.autoload :NotThere, filename ModuleSpecs::Autoload.autoload?(:NotThere).should == filename -> { require filename }.should raise_error(LoadError) ModuleSpecs::Autoload.autoload?(:NotThere).should == filename end it "allows multiple autoload constants for a single file" do filename = fixture(__FILE__, "autoload_lm.rb") ModuleSpecs::Autoload.autoload :L, filename ModuleSpecs::Autoload.autoload :M, filename ModuleSpecs::Autoload::L.should == :autoload_l ModuleSpecs::Autoload::M.should == :autoload_m end it "runs for an exception condition class and doesn't trample the exception" do filename = fixture(__FILE__, "autoload_ex1.rb") ModuleSpecs::Autoload.autoload :EX1, filename ModuleSpecs::Autoload.use_ex1.should == :good end 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 be_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 be_nil ModuleSpecs::Autoload::Dog.should have_constant(:R) 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 be_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 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 def check_before_during_thread_after(&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::DuringAutoload ensure in_loading_thread, in_other_thread = t.value end after = check.call [before, in_loading_thread, in_other_thread, after] end it "returns nil in autoload thread and 'constant' otherwise for defined?" do results = check_before_during_thread_after { 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 { 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 { 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 { ModuleSpecs::Autoload.autoload?(:DuringAutoload) } results.should == [@path, nil, @path, nil] end end 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.const_defined?(:Fail).should == true ModuleSpecs::Autoload.should have_constant(:Fail) ModuleSpecs::Autoload.autoload?(:Fail).should == @non_existent -> { ModuleSpecs::Autoload::Fail }.should raise_error(LoadError) ModuleSpecs::Autoload.should have_constant(:Fail) ModuleSpecs::Autoload.const_defined?(:Fail).should == true ModuleSpecs::Autoload.autoload?(:Fail).should == @non_existent -> { ModuleSpecs::Autoload::Fail }.should raise_error(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 have_constant(:Raise) ModuleSpecs::Autoload.autoload?(:Raise).should == path -> { ModuleSpecs::Autoload::Raise }.should raise_error(RuntimeError) ScratchPad.recorded.should == [:raise] ModuleSpecs::Autoload.should have_constant(:Raise) ModuleSpecs::Autoload.const_defined?(:Raise).should == true ModuleSpecs::Autoload.autoload?(:Raise).should == path -> { ModuleSpecs::Autoload::Raise }.should raise_error(RuntimeError) ScratchPad.recorded.should == [:raise, :raise] end ruby_version_is "3.1" do 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 have_constant(:O) ModuleSpecs::Autoload.autoload?(:O).should == path -> { ModuleSpecs::Autoload::O }.should raise_error(NameError) ModuleSpecs::Autoload.const_defined?(:O).should == false ModuleSpecs::Autoload.should_not have_constant(:O) ModuleSpecs::Autoload.autoload?(:O).should == nil -> { ModuleSpecs::Autoload.const_get(:O) }.should raise_error(NameError) end end ruby_version_is ""..."3.1" do it "does not remove the constant from Module#constants if the loaded file does not define it, but leaves it as 'undefined'" do path = fixture(__FILE__, "autoload_o.rb") ScratchPad.record [] ModuleSpecs::Autoload.autoload :O, path ModuleSpecs::Autoload.const_defined?(:O).should == true ModuleSpecs::Autoload.should have_constant(:O) ModuleSpecs::Autoload.autoload?(:O).should == path -> { ModuleSpecs::Autoload::O }.should raise_error(NameError) ModuleSpecs::Autoload.const_defined?(:O).should == false ModuleSpecs::Autoload.should have_constant(:O) ModuleSpecs::Autoload.autoload?(:O).should == nil -> { ModuleSpecs::Autoload.const_get(:O) }.should raise_error(NameError) end 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_error(NameError) ScratchPad.recorded.should == [:loaded] -> { ModuleSpecs::Autoload::NotDefinedByFile }.should raise_error(NameError) ScratchPad.recorded.should == [:loaded] Thread.new { -> { ModuleSpecs::Autoload::NotDefinedByFile }.should raise_error(NameError) }.join ScratchPad.recorded.should == [:loaded] end it "returns 'constant' on referring the constant with defined?()" do module ModuleSpecs::Autoload::Q autoload :R, fixture(__FILE__, "autoload.rb") defined?(R).should == 'constant' end ModuleSpecs::Autoload::Q.should have_constant(:R) end it "does not load the file when removing an autoload constant" do module ModuleSpecs::Autoload::Q autoload :R, fixture(__FILE__, "autoload.rb") remove_const :R end ModuleSpecs::Autoload::Q.should_not have_constant(: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 ruby_bug "[Bug #15780]", ""..."2.7" do ModuleSpecs::Autoload.const_defined?("P").should be_true end end it "loads the file when opening a module that is the autoloaded constant" do module ModuleSpecs::Autoload::U autoload :V, fixture(__FILE__, "autoload_v.rb") class V X = get_value end end @remove << :U ModuleSpecs::Autoload::U::V::X.should == :autoload_uvx end 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 :CS_CONST_AUTOLOAD, fixture(__FILE__, "autoload_subclass.rb") end 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 have_constant(:DeclaredAndDefinedInParent) -> { const_get(:DeclaredAndDefinedInParent) }.should raise_error(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") 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_error(NameError) ModuleSpecs::Autoload::LexicalScope.send(:remove_const, :DeclaredInParentDefinedInCurrent) end end ruby_version_is "3.1" do 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_error(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_error(NameError) end DeclaredInCurrentDefinedInParent.should == :declared_in_current_defined_in_parent end end end ruby_version_is ""..."3.1" do it "and fails when finding the undefined autoload constant in the current scope when declared in current and defined in parent" 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 raise_error(NameError) # Basically, the autoload constant remains in a "undefined" state self.autoload?(:DeclaredInCurrentDefinedInParent).should == nil const_defined?(:DeclaredInCurrentDefinedInParent).should == false self.should have_constant(:DeclaredInCurrentDefinedInParent) -> { const_get(:DeclaredInCurrentDefinedInParent) }.should raise_error(NameError) end DeclaredInCurrentDefinedInParent.should == :declared_in_current_defined_in_parent end 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 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 MetaScope.new end end end ModuleSpecs::Autoload.r.should be_kind_of(ModuleSpecs::Autoload::MetaScope) end end # [ruby-core:19127] [ruby-core:29941] it "does NOT raise a NameError when the autoload file did not define the constant and a module is opened with the same name" do module ModuleSpecs::Autoload class W autoload :Y, fixture(__FILE__, "autoload_w.rb") class Y end end end @remove << :W ModuleSpecs::Autoload::W::Y.should be_kind_of(Class) ScratchPad.recorded.should == :loaded 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 be_kind_of(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 be_kind_of(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 be_kind_of(Class) ModuleSpecs::Autoload::Foo::Baz.should be_kind_of(Class) ensure $LOAD_PATH.shift end end it "calls #to_path on non-string filenames" do p = mock('path') p.should_receive(:to_path).and_return @non_existent ModuleSpecs.autoload :A, p end it "raises an ArgumentError when an empty filename is given" do -> { ModuleSpecs.autoload :A, "" }.should raise_error(ArgumentError) end it "raises a NameError when the constant name starts with a lower case letter" do -> { ModuleSpecs.autoload "a", @non_existent }.should raise_error(NameError) end it "raises a NameError when the constant name starts with a number" do -> { ModuleSpecs.autoload "1two", @non_existent }.should raise_error(NameError) end it "raises a NameError when the constant name has a space in it" do -> { ModuleSpecs.autoload "a name", @non_existent }.should raise_error(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 } -> { ModuleSpecs::Autoload::S = mod1 }.should complain(/already initialized constant/) mod2 = mod1.dup mod1.autoload?(:T).should == filename mod2.autoload?(:T).should == filename mod1::T.should == :autoload_t -> { mod2::T }.should raise_error(NameError) end it "raises a TypeError if opening a class with a different superclass than the class defined in the autoload file" do ModuleSpecs::Autoload.autoload :Z, fixture(__FILE__, "autoload_z.rb") class ModuleSpecs::Autoload::ZZ end -> do class ModuleSpecs::Autoload::Z < ModuleSpecs::Autoload::ZZ end end.should raise_error(TypeError) end it "raises a TypeError if not passed a String or object responding to #to_path for the filename" do name = mock("autoload_name.rb") -> { ModuleSpecs::Autoload.autoload :Str, name }.should raise_error(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") -> { ModuleSpecs::Autoload.autoload :Str, name }.should_not raise_error end describe "on a frozen module" do it "raises a FrozenError before setting the name" do frozen_module = Module.new.freeze -> { frozen_module.autoload :Foo, @non_existent }.should raise_error(FrozenError) frozen_module.should_not have_constant(:Foo) end end describe "when changing $LOAD_PATH" do before do $LOAD_PATH.unshift(File.expand_path('../fixtures/path1', __FILE__)) end after do $LOAD_PATH.shift $LOAD_PATH.shift end it "does not reload a file due to a different load path" do ModuleSpecs::Autoload.autoload :LoadPath, "load_path" ModuleSpecs::Autoload::LoadPath.loaded.should == :autoload_load_path end end 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 ScratchPad.record [] t1_val = nil t2_val = nil fin = false t1 = Thread.new do Thread.pass until start t1_val = ModuleSpecs::Autoload::Concur ScratchPad.recorded << :t1_post fin = true end t2_exc = nil t2 = Thread.new do Thread.pass until t1 and t1[:in_autoload_rb] begin t2_val = ModuleSpecs::Autoload::Concur rescue Exception => e t2_exc = e else Thread.pass until fin ScratchPad.recorded << :t2_post end end start = true t1.join t2.join ScratchPad.recorded.should == [:con_pre, :con_post, :t1_post, :t2_post] t1_val.should == 1 t2_val.should == t1_val t2_exc.should be_nil end # 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 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 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 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 end it "raises a NameError in each thread if the constant is not set" do file = fixture(__FILE__, "autoload_never_set.rb") start = false threads = Array.new(10) do Thread.new do Thread.pass until start begin ModuleSpecs::Autoload.autoload :NeverSetConstant, file Thread.pass ModuleSpecs::Autoload::NeverSetConstant rescue NameError => e e ensure Thread.pass end end end start = true threads.each { |t| t.value.should be_an_instance_of(NameError) } end it "raises a LoadError in each thread if the file does not exist" do file = fixture(__FILE__, "autoload_does_not_exist.rb") start = false threads = Array.new(10) do Thread.new do Thread.pass until start begin ModuleSpecs::Autoload.autoload :FileDoesNotExist, file Thread.pass ModuleSpecs::Autoload::FileDoesNotExist rescue LoadError => e e ensure Thread.pass end end end start = true threads.each { |t| t.value.should be_an_instance_of(LoadError) } end end it "loads the registered constant even if the constant was already loaded by another thread" do Thread.new { ModuleSpecs::Autoload::FromThread::D.foo }.value.should == :foo end end