diff --git a/lib/puppet/pops/loader/base_loader.rb b/lib/puppet/pops/loader/base_loader.rb index f7b34ece7..fd9bc53c5 100644 --- a/lib/puppet/pops/loader/base_loader.rb +++ b/lib/puppet/pops/loader/base_loader.rb @@ -1,102 +1,103 @@ # BaseLoader # === # An abstract implementation of Puppet::Pops::Loader::Loader # # A derived class should implement `find(typed_name)` and set entries, and possible handle "miss caching". # # @api private # class Puppet::Pops::Loader::BaseLoader < Puppet::Pops::Loader::Loader # The parent loader attr_reader :parent # An internal name used for debugging and error message purposes attr_reader :loader_name def initialize(parent_loader, loader_name) @parent = parent_loader # the higher priority loader to consult @named_values = {} # hash name => NamedEntry @last_name = nil # the last name asked for (optimization) @last_result = nil # the value of the last name (optimization) @loader_name = loader_name # the name of the loader (not the name-space it is a loader for) end # @api public # def load_typed(typed_name) # The check for "last queried name" is an optimization when a module searches. First it checks up its parent # chain, then itself, and then delegates to modules it depends on. # These modules are typically parented by the same # loader as the one initiating the search. It is inefficient to again try to search the same loader for # the same name. if typed_name == @last_name @last_result else @last_name = typed_name @last_result = internal_load(typed_name) end end # This method is final (subclasses should not override it) # # @api private # def get_entry(typed_name) @named_values[typed_name] end # @api private # def set_entry(typed_name, value, origin = nil) if entry = @named_values[typed_name] then fail_redefined(entry); end @named_values[typed_name] = Puppet::Pops::Loader::Loader::NamedEntry.new(typed_name, value, origin) end # @api private # def add_entry(type, name, value, origin) set_entry(Puppet::Pops::Loader::Loader::TypedName.new(type, name), value, origin) end # Promotes an already created entry (typically from another loader) to this loader # # @api private # def promote_entry(named_entry) typed_name = named_entry.typed_name - if entry = @named_values[typed_name] then fail_redefined(entry); end + if entry = @named_values[typed_name] then fail_redefine(entry); end @named_values[typed_name] = named_entry end private def fail_redefine(entry) + require 'debugger'; debugger origin_info = entry.origin ? " Originally set at #{origin_label(entry.origin)}." : "unknown location" - raise ArgumentError, "Attempt to redefine entity '#{entry.typed_name}' originally set at #{origin_label(origin)}.#{origin_info}" + raise ArgumentError, "Attempt to redefine entity '#{entry.typed_name}' originally set at #{origin_info}" end # TODO: Should not really be here?? - TODO: A Label provider ? semantics for the URI? # def origin_label(origin) if origin && origin.is_a?(URI) origin.to_s elsif origin.respond_to?(:uri) origin.uri.to_s else - nil + origin end end # loads in priority order: # 1. already loaded here # 2. load from parent # 3. find it here # 4. give up # def internal_load(typed_name) # avoid calling get_entry, by looking it up @named_values[typed_name] || parent.load_typed(typed_name) || find(typed_name) end end diff --git a/lib/puppet/pops/loader/loader.rb b/lib/puppet/pops/loader/loader.rb index 256fc373e..37c912c2d 100644 --- a/lib/puppet/pops/loader/loader.rb +++ b/lib/puppet/pops/loader/loader.rb @@ -1,180 +1,180 @@ # Loader # === # A Loader is responsible for loading "entities" ("instantiable and executable objects in the puppet language" which # are type, hostclass, definition, function, and bindings. # # The main method for users of a Loader is the `load` or `load_typed methods`, which returns a previously loaded entity # of a given type/name, and searches and loads the entity if not already loaded. # # private entities # --- # TODO: handle loading of entities that are private. Suggest that all calls pass an origin_loader (the loader # where request originated (or symbol :public). A module loader has one (or possibly a list) of what is # considered to represent private loader - i.e. the dependency loader for a module. If an entity is private # it should be stored with this status, and an error should be raised if the origin_loader is not on the list # of accepted "private" loaders. # The private loaders can not be given at creation time (they are parented by the loader in question). Another # alternative is to check if the origin_loader is a child loader, but this requires bidirectional links # between loaders or a search if loader with private entity is a parent of the origin_loader). # # @api public # class Puppet::Pops::Loader::Loader # Produces the value associated with the given name if already loaded, or available for loading # by this loader, one of its parents, or other loaders visible to this loader. # This is the method an external party should use to "get" the named element. # # An implementor of this method should first check if the given name is already loaded by self, or a parent # loader, and if so return that result. If not, it should call `find` to perform the loading. # # @param type [:Symbol] the type to load # @param name [String, Symbol] the name of the entity to load # @return [Object, nil] the value or nil if not found # # @api public # def load(type, name) if result = load_typed(TypedName.new(type, name.to_s)) result.value end end # Loads the given typed name, and returns a NamedEntry if found, else returns nil. # This the same a `load`, but returns a NamedEntry with origin/value information. # # @param typed_name [TypedName] - the type, name combination to lookup # @return [NamedEntry, nil] the entry containing the loaded value, or nil if not found # # @api public # def load_typed(typed_name) raise NotImplementedError.new end # Produces the value associated with the given name if defined **in this loader**, or nil if not defined. # This lookup does not trigger any loading, or search of the given name. # An implementor of this method may not search or look up in any other loader, and it may not # define the name. # # @param typed_name [TypedName] - the type, name combination to lookup # # @api private # def [] (typed_name) if found = get_entry(typed_name) found.value else nil end end # Searches for the given name in this loader's context (parents should already have searched their context(s) without # producing a result when this method is called). # An implementation of find typically caches the result. # # @param typed_name [TypedName] the type, name combination to lookup # @return [NamedEntry, nil] the entry for the loaded entry, or nil if not found # # @api private # def find(typed_name) raise NotImplementedError.new end # Returns the parent of the loader, or nil, if this is the top most loader. This implementation returns nil. def parent nil end # Produces the private loader for loaders that have a one (the visibility given to loaded entities). # For loaders that does not provide a private loader, self is returned. # # @api private def private_loader self end # Binds a value to a name. The name should not start with '::', but may contain multiple segments. # # @param type [:Symbol] the type of the entity being set # @param name [String, Symbol] the name of the entity being set # @param origin [URI, #uri, String] the origin of the set entity, a URI, or provider of URI, or URI in string form # @return [NamedEntry, nil] the created entry # # @api private # def set_entry(type, name, value, origin = nil) raise NotImplementedError.new end # Produces a NamedEntry if a value is bound to the given name, or nil if nothing is bound. # # @param typed_name [TypedName] the type, name combination to lookup # @return [NamedEntry, nil] the value bound in an entry # # @api private # def get_entry(typed_name) raise NotImplementedError.new end # An entry for one entity loaded by the loader. # class NamedEntry attr_reader :typed_name attr_reader :value attr_reader :origin def initialize(typed_name, value, origin) - @name = typed_name + @typed_name = typed_name @value = value @origin = origin freeze() end end # A name/type combination that can be used as a compound hash key # class TypedName attr_reader :type attr_reader :name attr_reader :name_parts # True if name is qualified (more than a single segment) attr_reader :qualified def initialize(type, name) @type = type # relativize the name (get rid of leading ::), and make the split string available @name_parts = name.to_s.split(/::/) @name_parts.shift if name_parts[0].empty? @name = name_parts.join('::') @qualified = name_parts.size > 1 # precompute hash - the name is frozen, so this is safe to do @hash = [self.class, type, @name].hash # Not allowed to have numeric names - 0, 010, 0x10, 1.2 etc if Puppet::Pops::Utils.is_numeric?(@name) raise ArgumentError, "Illegal attempt to use a numeric name '#{name}' at #{origin_label(origin)}." end freeze() end def hash @hash end def ==(o) o.class == self.class && type == o.type && name == o.name end alias eql? == def to_s "#{type}/#{name}" end end end diff --git a/spec/unit/pops/loaders/dependency_loader_spec.rb b/spec/unit/pops/loaders/dependency_loader_spec.rb index dbea5b208..cbdefe897 100644 --- a/spec/unit/pops/loaders/dependency_loader_spec.rb +++ b/spec/unit/pops/loaders/dependency_loader_spec.rb @@ -1,44 +1,61 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' describe 'dependency loader' do include PuppetSpec::Files let(:static_loader) { Puppet::Pops::Loader::StaticLoader.new() } let(:loaders) { Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, [])) } describe 'FileBased module loader' do it 'load something in global name space raises an error' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { 'foo.rb' => 'Puppet::Functions.create_function("foo") { def foo; end; }' }}}}}) module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') dep_loader = Puppet::Pops::Loader::DependencyLoader.new(static_loader, 'test-dep', [module_loader]) expect do dep_loader.load_typed(typed_name(:function, 'testmodule::foo')).value end.to raise_error(ArgumentError, /produced mis-matched name, expected 'testmodule::foo', got foo/) end it 'can load something in a qualified name space' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { 'foo.rb' => 'Puppet::Functions.create_function("testmodule::foo") { def foo; end; }' }}}}}) module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') dep_loader = Puppet::Pops::Loader::DependencyLoader.new(static_loader, 'test-dep', [module_loader]) function = dep_loader.load_typed(typed_name(:function, 'testmodule::foo')).value expect(function.class.name).to eq('testmodule::foo') expect(function.is_a?(Puppet::Functions::Function)).to eq(true) end + + it 'can load something in a qualified name space more than once' do + module_dir = dir_containing('testmodule', { + 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { + 'foo.rb' => 'Puppet::Functions.create_function("testmodule::foo") { def foo; end; }' + }}}}}) + module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') + dep_loader = Puppet::Pops::Loader::DependencyLoader.new(static_loader, 'test-dep', [module_loader]) + + function = dep_loader.load_typed(typed_name(:function, 'testmodule::foo')).value + expect(function.class.name).to eq('testmodule::foo') + expect(function.is_a?(Puppet::Functions::Function)).to eq(true) + + function = dep_loader.load_typed(typed_name(:function, 'testmodule::foo')).value + expect(function.class.name).to eq('testmodule::foo') + expect(function.is_a?(Puppet::Functions::Function)).to eq(true) + end end def typed_name(type, name) Puppet::Pops::Loader::Loader::TypedName.new(type, name) end end diff --git a/spec/unit/pops/loaders/loaders_spec.rb b/spec/unit/pops/loaders/loaders_spec.rb index daa9c716c..a9156ecd9 100644 --- a/spec/unit/pops/loaders/loaders_spec.rb +++ b/spec/unit/pops/loaders/loaders_spec.rb @@ -1,105 +1,151 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' +describe 'loader helper classes' do + it 'NamedEntry holds values and is frozen' do + ne = Puppet::Pops::Loader::Loader::NamedEntry.new('name', 'value', 'origin') + expect(ne.frozen?).to be_true + expect(ne.typed_name).to eql('name') + expect(ne.origin).to eq('origin') + expect(ne.value).to eq('value') + end + + it 'TypedName holds values and is frozen' do + tn = Puppet::Pops::Loader::Loader::TypedName.new(:function, '::foo::bar') + expect(tn.frozen?).to be_true + expect(tn.type).to eq(:function) + expect(tn.name_parts).to eq(['foo', 'bar']) + expect(tn.name).to eq('foo::bar') + expect(tn.qualified).to be_true + end +end + describe 'loaders' do include PuppetSpec::Files let(:module_without_metadata) { File.join(config_dir('wo_metadata_module'), 'modules') } let(:module_with_metadata) { File.join(config_dir('single_module'), 'modules') } let(:dependent_modules_with_metadata) { config_dir('dependent_modules_with_metadata') } let(:empty_test_env) { environment_for() } # Loaders caches the puppet_system_loader, must reset between tests before(:each) { Puppet::Pops::Loaders.clear() } it 'creates a puppet_system loader' do loaders = Puppet::Pops::Loaders.new(empty_test_env) expect(loaders.puppet_system_loader()).to be_a(Puppet::Pops::Loader::ModuleLoaders::FileBased) end it 'creates an environment loader' do loaders = Puppet::Pops::Loaders.new(empty_test_env) expect(loaders.public_environment_loader()).to be_a(Puppet::Pops::Loader::SimpleEnvironmentLoader) expect(loaders.public_environment_loader().to_s).to eql("(SimpleEnvironmentLoader 'environment:*test*')") expect(loaders.private_environment_loader()).to be_a(Puppet::Pops::Loader::DependencyLoader) expect(loaders.private_environment_loader().to_s).to eql("(DependencyLoader 'environment' [])") end it 'can load 3x system functions' do Puppet[:biff] = true loaders = Puppet::Pops::Loaders.new(empty_test_env) puppet_loader = loaders.puppet_system_loader() function = puppet_loader.load_typed(typed_name(:function, 'sprintf')).value expect(function.class.name).to eq('sprintf') expect(function).to be_a(Puppet::Functions::Function) end + it 'can load 3x system functions more than once' do + Puppet[:biff] = true + loaders = Puppet::Pops::Loaders.new(empty_test_env) + puppet_loader = loaders.puppet_system_loader() + + function = puppet_loader.load_typed(typed_name(:function, 'sprintf')).value + + expect(function.class.name).to eq('sprintf') + expect(function).to be_a(Puppet::Functions::Function) + + function = puppet_loader.load_typed(typed_name(:function, 'sprintf')).value + expect(function.class.name).to eq('sprintf') + expect(function).to be_a(Puppet::Functions::Function) + end + it 'can load a function using a qualified or unqualified name from a module with metadata' do loaders = Puppet::Pops::Loaders.new(environment_for(module_with_metadata)) modulea_loader = loaders.public_loader_for_module('modulea') unqualified_function = modulea_loader.load_typed(typed_name(:function, 'rb_func_a')).value qualified_function = modulea_loader.load_typed(typed_name(:function, 'modulea::rb_func_a')).value expect(unqualified_function).to be_a(Puppet::Functions::Function) expect(qualified_function).to be_a(Puppet::Functions::Function) expect(unqualified_function.class.name).to eq('rb_func_a') expect(qualified_function.class.name).to eq('modulea::rb_func_a') end it 'can load a function with a qualified name from module without metadata' do loaders = Puppet::Pops::Loaders.new(environment_for(module_without_metadata)) moduleb_loader = loaders.public_loader_for_module('moduleb') function = moduleb_loader.load_typed(typed_name(:function, 'moduleb::rb_func_b')).value expect(function).to be_a(Puppet::Functions::Function) expect(function.class.name).to eq('moduleb::rb_func_b') end it 'cannot load an unqualified function from a module without metadata' do loaders = Puppet::Pops::Loaders.new(environment_for(module_without_metadata)) moduleb_loader = loaders.public_loader_for_module('moduleb') expect(moduleb_loader.load_typed(typed_name(:function, 'rb_func_b'))).to be_nil end it 'makes all other modules visible to a module without metadata' do env = environment_for(module_with_metadata, module_without_metadata) loaders = Puppet::Pops::Loaders.new(env) moduleb_loader = loaders.private_loader_for_module('moduleb') function = moduleb_loader.load_typed(typed_name(:function, 'moduleb::rb_func_b')).value expect(function.call({})).to eql("I am modulea::rb_func_a() + I am moduleb::rb_func_b()") end it 'makes dependent modules visible to a module with metadata' do env = environment_for(dependent_modules_with_metadata) loaders = Puppet::Pops::Loaders.new(env) moduleb_loader = loaders.private_loader_for_module('user') function = moduleb_loader.load_typed(typed_name(:function, 'user::caller')).value expect(function.call({})).to eql("usee::callee() was told 'passed value' + I am user::caller()") end + it 'can load a function more than once from modules' do + env = environment_for(dependent_modules_with_metadata) + loaders = Puppet::Pops::Loaders.new(env) + + moduleb_loader = loaders.private_loader_for_module('user') + function = moduleb_loader.load_typed(typed_name(:function, 'user::caller')).value + expect(function.call({})).to eql("usee::callee() was told 'passed value' + I am user::caller()") + + function = moduleb_loader.load_typed(typed_name(:function, 'user::caller')).value + expect(function.call({})).to eql("usee::callee() was told 'passed value' + I am user::caller()") + end + def environment_for(*module_paths) Puppet::Node::Environment.create(:'*test*', module_paths, '') end def typed_name(type, name) Puppet::Pops::Loader::Loader::TypedName.new(type, name) end def config_dir(config_name) my_fixture(config_name) end end