diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb index c7387f225..651b2ce53 100644 --- a/lib/puppet/parser/compiler.rb +++ b/lib/puppet/parser/compiler.rb @@ -1,598 +1,598 @@ require 'forwardable' require 'puppet/node' require 'puppet/resource/catalog' require 'puppet/util/errors' require 'puppet/resource/type_collection_helper' # Maintain a graph of scopes, along with a bunch of data # about the individual catalog we're compiling. class Puppet::Parser::Compiler extend Forwardable include Puppet::Util include Puppet::Util::Errors include Puppet::Util::MethodHelper include Puppet::Resource::TypeCollectionHelper def self.compile(node) $env_module_directories = nil node.environment.check_for_reparse new(node).compile.to_resource rescue => detail message = "#{detail} on node #{node.name}" Puppet.log_exception(detail, message) raise Puppet::Error, message, detail.backtrace end attr_reader :node, :facts, :collections, :catalog, :resources, :relationships, :topscope # The injector that provides lookup services, or nil if accessed before the compiler has started compiling and # bootstrapped. The injector is initialized and available before any manifests are evaluated. # # @return [Puppet::Pops::Binder::Injector, nil] The injector that provides lookup services for this compiler/environment # @api public # attr_accessor :injector # Access to the configured loaders for 4x # @return [Puppet::Pops::Loader::Loaders] the configured loaders # @api private attr_reader :loaders # The injector that provides lookup services during the creation of the {#injector}. # @return [Puppet::Pops::Binder::Injector, nil] The injector that provides lookup services during injector creation # for this compiler/environment # # @api private # attr_accessor :boot_injector # Add a collection to the global list. def_delegator :@collections, :<<, :add_collection def_delegator :@relationships, :<<, :add_relationship # Store a resource override. def add_override(override) # If possible, merge the override in immediately. if resource = @catalog.resource(override.ref) resource.merge(override) else # Otherwise, store the override for later; these # get evaluated in Resource#finish. @resource_overrides[override.ref] << override end end def add_resource(scope, resource) @resources << resource # Note that this will fail if the resource is not unique. @catalog.add_resource(resource) if not resource.class? and resource[:stage] raise ArgumentError, "Only classes can set 'stage'; normal resources like #{resource} cannot change run stage" end # Stages should not be inside of classes. They are always a # top-level container, regardless of where they appear in the # manifest. return if resource.stage? # This adds a resource to the class it lexically appears in in the # manifest. unless resource.class? return @catalog.add_edge(scope.resource, resource) end end # Do we use nodes found in the code, vs. the external node sources? def_delegator :known_resource_types, :nodes?, :ast_nodes? # Store the fact that we've evaluated a class def add_class(name) @catalog.add_class(name) unless name == "" end # Return a list of all of the defined classes. def_delegator :@catalog, :classes, :classlist # Compiler our catalog. This mostly revolves around finding and evaluating classes. # This is the main entry into our catalog. def compile Puppet.override( @context_overrides , "For compiling #{node.name}") do # Set the client's parameters into the top scope. Puppet::Util::Profiler.profile("Compile: Set node parameters") { set_node_parameters } Puppet::Util::Profiler.profile("Compile: Created settings scope") { create_settings_scope } if is_binder_active? # create injector, if not already created - this is for 3x that does not trigger # lazy loading of injector via context Puppet::Util::Profiler.profile("Compile: Created injector") { injector } end Puppet::Util::Profiler.profile("Compile: Evaluated main") { evaluate_main } Puppet::Util::Profiler.profile("Compile: Evaluated AST node") { evaluate_ast_node } Puppet::Util::Profiler.profile("Compile: Evaluated node classes") { evaluate_node_classes } Puppet::Util::Profiler.profile("Compile: Evaluated generators") { evaluate_generators } Puppet::Util::Profiler.profile("Compile: Finished catalog") { finish } fail_on_unevaluated @catalog end end # Constructs the overrides for the context def context_overrides() if Puppet[:parser] == 'future' require 'puppet/loaders' { :current_environment => environment, :global_scope => @topscope, # 4x placeholder for new global scope :loaders => lambda {|| loaders() }, # 4x loaders :injector => lambda {|| injector() } # 4x API - via context instead of via compiler } else { :current_environment => environment, } end end def_delegator :@collections, :delete, :delete_collection # Return the node's environment. def environment unless node.environment.is_a? Puppet::Node::Environment raise Puppet::DevError, "node #{node} has an invalid environment!" end node.environment end # Evaluate all of the classes specified by the node. # Classes with parameters are evaluated as if they were declared. # Classes without parameters or with an empty set of parameters are evaluated # as if they were included. This means classes with an empty set of # parameters won't conflict even if the class has already been included. def evaluate_node_classes if @node.classes.is_a? Hash classes_with_params, classes_without_params = @node.classes.partition {|name,params| params and !params.empty?} # The results from Hash#partition are arrays of pairs rather than hashes, # so we have to convert to the forms evaluate_classes expects (Hash, and # Array of class names) classes_with_params = Hash[classes_with_params] classes_without_params.map!(&:first) else classes_with_params = {} classes_without_params = @node.classes end evaluate_classes(classes_without_params, @node_scope || topscope) evaluate_classes(classes_with_params, @node_scope || topscope) end # Evaluate each specified class in turn. If there are any classes we can't # find, raise an error. This method really just creates resource objects # that point back to the classes, and then the resources are themselves # evaluated later in the process. # # Sometimes we evaluate classes with a fully qualified name already, in which # case, we tell scope.find_hostclass we've pre-qualified the name so it # doesn't need to search its namespaces again. This gets around a weird # edge case of duplicate class names, one at top scope and one nested in our # namespace and the wrong one (or both!) getting selected. See ticket #13349 # for more detail. --jeffweiss 26 apr 2012 def evaluate_classes(classes, scope, lazy_evaluate = true, fqname = false) raise Puppet::DevError, "No source for scope passed to evaluate_classes" unless scope.source class_parameters = nil # if we are a param class, save the classes hash # and transform classes to be the keys if classes.class == Hash class_parameters = classes classes = classes.keys end classes.each do |name| # If we can find the class, then make a resource that will evaluate it. if klass = scope.find_hostclass(name, :assume_fqname => fqname) # If parameters are passed, then attempt to create a duplicate resource # so the appropriate error is thrown. if class_parameters resource = klass.ensure_in_catalog(scope, class_parameters[name] || {}) else next if scope.class_scope(klass) resource = klass.ensure_in_catalog(scope) end # If they've disabled lazy evaluation (which the :include function does), # then evaluate our resource immediately. resource.evaluate unless lazy_evaluate else raise Puppet::Error, "Could not find class #{name} for #{node.name}" end end end def evaluate_relationships @relationships.each { |rel| rel.evaluate(catalog) } end # Return a resource by either its ref or its type and title. def_delegator :@catalog, :resource, :findresource def initialize(node, options = {}) @node = node set_options(options) initvars end # Create a new scope, with either a specified parent scope or # using the top scope. def newscope(parent, options = {}) parent ||= topscope scope = Puppet::Parser::Scope.new(self, options) scope.parent = parent scope end # Return any overrides for the given resource. def resource_overrides(resource) @resource_overrides[resource.ref] end def injector create_injector if @injector.nil? @injector end def loaders - @loaders ||= Puppet::Pops::Loaders.new() + @loaders ||= Puppet::Pops::Loaders.new(environment) end def boot_injector create_boot_injector(nil) if @boot_injector.nil? @boot_injector end # Creates the boot injector from registered system, default, and injector config. # @return [Puppet::Pops::Binder::Injector] the created boot injector # @api private Cannot be 'private' since it is called from the BindingsComposer. # def create_boot_injector(env_boot_bindings) assert_binder_active() pb = Puppet::Pops::Binder boot_contribution = pb::SystemBindings.injector_boot_contribution(env_boot_bindings) final_contribution = pb::SystemBindings.final_contribution binder = pb::Binder.new(pb::BindingsFactory.layered_bindings(final_contribution, boot_contribution)) @boot_injector = pb::Injector.new(binder) end # Answers if Puppet Binder should be active or not, and if it should and is not active, then it is activated. # @return [Boolean] true if the Puppet Binder should be activated def is_binder_active? should_be_active = Puppet[:binder] || Puppet[:parser] == 'future' if should_be_active # TODO: this should be in a central place, not just for ParserFactory anymore... Puppet::Parser::ParserFactory.assert_rgen_installed() @@binder_loaded ||= false unless @@binder_loaded require 'puppet/pops' require 'puppetx' @@binder_loaded = true end end should_be_active end private # If ast nodes are enabled, then see if we can find and evaluate one. def evaluate_ast_node return unless ast_nodes? # Now see if we can find the node. astnode = nil @node.names.each do |name| break if astnode = known_resource_types.node(name.to_s.downcase) end unless (astnode ||= known_resource_types.node("default")) raise Puppet::ParseError, "Could not find default node or by name with '#{node.names.join(", ")}'" end # Create a resource to model this node, and then add it to the list # of resources. resource = astnode.ensure_in_catalog(topscope) resource.evaluate @node_scope = topscope.class_scope(astnode) end # Evaluate our collections and return true if anything returned an object. # The 'true' is used to continue a loop, so it's important. def evaluate_collections return false if @collections.empty? exceptwrap do # We have to iterate over a dup of the array because # collections can delete themselves from the list, which # changes its length and causes some collections to get missed. Puppet::Util::Profiler.profile("Evaluated collections") do found_something = false @collections.dup.each do |collection| found_something = true if collection.evaluate end found_something end end end # Make sure all of our resources have been evaluated into native resources. # We return true if any resources have, so that we know to continue the # evaluate_generators loop. def evaluate_definitions exceptwrap do Puppet::Util::Profiler.profile("Evaluated definitions") do !unevaluated_resources.each do |resource| Puppet::Util::Profiler.profile("Evaluated resource #{resource}") do resource.evaluate end end.empty? end end end # Iterate over collections and resources until we're sure that the whole # compile is evaluated. This is necessary because both collections # and defined resources can generate new resources, which themselves could # be defined resources. def evaluate_generators count = 0 loop do done = true Puppet::Util::Profiler.profile("Iterated (#{count + 1}) on generators") do # Call collections first, then definitions. done = false if evaluate_collections done = false if evaluate_definitions end break if done count += 1 if count > 1000 raise Puppet::ParseError, "Somehow looped more than 1000 times while evaluating host catalog" end end end # Find and evaluate our main object, if possible. def evaluate_main @main = known_resource_types.find_hostclass([""], "") || known_resource_types.add(Puppet::Resource::Type.new(:hostclass, "")) @topscope.source = @main @main_resource = Puppet::Parser::Resource.new("class", :main, :scope => @topscope, :source => @main) @topscope.resource = @main_resource add_resource(@topscope, @main_resource) @main_resource.evaluate end # Make sure the entire catalog is evaluated. def fail_on_unevaluated fail_on_unevaluated_overrides fail_on_unevaluated_resource_collections end # If there are any resource overrides remaining, then we could # not find the resource they were supposed to override, so we # want to throw an exception. def fail_on_unevaluated_overrides remaining = @resource_overrides.values.flatten.collect(&:ref) if !remaining.empty? fail Puppet::ParseError, "Could not find resource(s) #{remaining.join(', ')} for overriding" end end # Make sure we don't have any remaining collections that specifically # look for resources, because we want to consider those to be # parse errors. def fail_on_unevaluated_resource_collections remaining = @collections.collect(&:resources).flatten.compact if !remaining.empty? raise Puppet::ParseError, "Failed to realize virtual resources #{remaining.join(', ')}" end end # Make sure all of our resources and such have done any last work # necessary. def finish evaluate_relationships resources.each do |resource| # Add in any resource overrides. if overrides = resource_overrides(resource) overrides.each do |over| resource.merge(over) end # Remove the overrides, so that the configuration knows there # are none left. overrides.clear end resource.finish if resource.respond_to?(:finish) end add_resource_metaparams end def add_resource_metaparams unless main = catalog.resource(:class, :main) raise "Couldn't find main" end names = Puppet::Type.metaparams.select do |name| !Puppet::Parser::Resource.relationship_parameter?(name) end data = {} catalog.walk(main, :out) do |source, target| if source_data = data[source] || metaparams_as_data(source, names) # only store anything in the data hash if we've actually got # data data[source] ||= source_data source_data.each do |param, value| target[param] = value if target[param].nil? end data[target] = source_data.merge(metaparams_as_data(target, names)) end target.tag(*(source.tags)) end end def metaparams_as_data(resource, params) data = nil params.each do |param| unless resource[param].nil? # Because we could be creating a hash for every resource, # and we actually probably don't often have any data here at all, # we're optimizing a bit by only creating a hash if there's # any data to put in it. data ||= {} data[param] = resource[param] end end data end # Set up all of our internal variables. def initvars # The list of overrides. This is used to cache overrides on objects # that don't exist yet. We store an array of each override. @resource_overrides = Hash.new do |overs, ref| overs[ref] = [] end # The list of collections that have been created. This is a global list, # but they each refer back to the scope that created them. @collections = [] # The list of relationships to evaluate. @relationships = [] # For maintaining the relationship between scopes and their resources. @catalog = Puppet::Resource::Catalog.new(@node.name) # MOVED HERE - SCOPE IS NEEDED (MOVE-SCOPE) # Create the initial scope, it is needed early @topscope = Puppet::Parser::Scope.new(self) # Need to compute overrides here, and remember them, because we are about to # enter the magic zone of known_resource_types and intial import. # Expensive entries in the context are bound lazily. @context_overrides = context_overrides() # This construct ensures that initial import (triggered by instantiating # the structure 'known_resource_types') has a configured context # It cannot survive the initvars method, and is later reinstated # as part of compiling... # Puppet.override( @context_overrides , "For initializing compiler") do # THE MAGIC STARTS HERE ! This triggers parsing, loading etc. @catalog.version = known_resource_types.version end @catalog.environment = @node.environment.to_s @catalog.add_resource(Puppet::Parser::Resource.new("stage", :main, :scope => @topscope)) # local resource array to maintain resource ordering @resources = [] # Make sure any external node classes are in our class list if @node.classes.class == Hash @catalog.add_class(*@node.classes.keys) else @catalog.add_class(*@node.classes) end end # Set the node's parameters into the top-scope as variables. def set_node_parameters node.parameters.each do |param, value| @topscope[param.to_s] = value end # These might be nil. catalog.client_version = node.parameters["clientversion"] catalog.server_version = node.parameters["serverversion"] if Puppet[:trusted_node_data] @topscope.set_trusted(node.trusted_data) end if(Puppet[:immutable_node_data]) facts_hash = node.facts.nil? ? {} : node.facts.values @topscope.set_facts(facts_hash) end end def create_settings_scope unless settings_type = environment.known_resource_types.hostclass("settings") settings_type = Puppet::Resource::Type.new :hostclass, "settings" environment.known_resource_types.add(settings_type) end settings_resource = Puppet::Parser::Resource.new("class", "settings", :scope => @topscope) @catalog.add_resource(settings_resource) settings_type.evaluate_code(settings_resource) scope = @topscope.class_scope(settings_type) Puppet.settings.each do |name, setting| next if name.to_s == "name" scope[name.to_s] = environment[name] end end # Return an array of all of the unevaluated resources. These will be definitions, # which need to get evaluated into native resources. def unevaluated_resources # The order of these is significant for speed due to short-circuting resources.reject { |resource| resource.evaluated? or resource.virtual? or resource.builtin_type? } end # Creates the injector from bindings found in the current environment. # @return [void] # @api private # def create_injector assert_binder_active() composer = Puppet::Pops::Binder::BindingsComposer.new() layered_bindings = composer.compose(topscope) @injector = Puppet::Pops::Binder::Injector.new(Puppet::Pops::Binder::Binder.new(layered_bindings)) end def assert_binder_active unless is_binder_active? raise ArgumentError, "The Puppet Binder is only available when either '--binder true' or '--parser future' is used" end end end diff --git a/lib/puppet/pops/loader/loader.rb b/lib/puppet/pops/loader/loader.rb index f162604f5..256fc373e 100644 --- a/lib/puppet/pops/loader/loader.rb +++ b/lib/puppet/pops/loader/loader.rb @@ -1,172 +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 @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/lib/puppet/pops/loader/module_loaders.rb b/lib/puppet/pops/loader/module_loaders.rb index fb54df70e..bf3a08ced 100644 --- a/lib/puppet/pops/loader/module_loaders.rb +++ b/lib/puppet/pops/loader/module_loaders.rb @@ -1,228 +1,242 @@ # =ModuleLoaders # A ModuleLoader loads items from a single module. # The ModuleLoaders (ruby) module contains various such loaders. There is currently one concrete # implementation, ModuleLoaders::FileBased that loads content from the file system. # Other implementations can be created - if they are based on name to path mapping where the path # is relative to a root path, they can derive the base behavior from the ModuleLoaders::AbstractPathBasedModuleLoader class. # # Examples of such extensions could be a zip/jar/compressed file base loader. # # Notably, a ModuleLoader does not configure itself - it is given the information it needs (the root, its name etc.) # Logic higher up in the loader hierarchy of things makes decisions based on the "shape of modules", and "available # modules" to determine which module loader to use for each individual module. (There could be differences in # internal layout etc.) # # A module loader is also not aware of the mapping of name to relative paths - this is performed by the # included module Puppet::Pops::Loader::PathBasedInstantatorConfig which knows about the map from type/name to # relative path, and the logic that can instantiate what is expected to be found in the content of that path. # # @api private # module Puppet::Pops::Loader::ModuleLoaders class AbstractPathBasedModuleLoader < Puppet::Pops::Loader::BaseLoader # The name of the module, or nil, if this is a global "component" attr_reader :module_name # The path to the location of the module/component - semantics determined by subclass attr_reader :path # A map of type to smart-paths that help with minimizing the number of paths to scan attr_reader :smart_paths + # A Module Loader has a private loader, it is lazily obtained on request to provide the visibility + # for entities contained in the module. Since a ModuleLoader also represents an environment and it is + # created a different way, this loader can be set explicitly by the loaders bootstrap logic. + # + # @api private + attr_accessor :private_loader + # Initialize a kind of ModuleLoader for one module # @param parent_loader [Puppet::Pops::Loader] loader with higher priority # @param module_name [String] the name of the module (non qualified name), may be nil for a global "component" # @param path [String] the path to the root of the module (semantics defined by subclass) # @param loader_name [String] a name that is used for human identification (useful when module_name is nil) # - def initialize(parent_loader, module_name, path, loader_name) + def initialize(parent_loader, loaders, module_name, path, loader_name) super parent_loader, loader_name # Irrespective of the path referencing a directory or file, the path must exist. unless Puppet::FileSystem.exist?(path) raise ArgumentError, "The given path '#{path}' does not exist!" end @module_name = module_name @path = path @smart_paths = Puppet::Pops::Loader::LoaderPaths::SmartPaths.new(self) + @loaders = loaders end # Finds typed/named entity in this module # @param typed_name [Puppet::Pops::Loader::TypedName] the type/name to find # @return [Puppet::Pops::Loader::Loader::NamedEntry, nil found/created entry, or nil if not found # def find(typed_name) # Assume it is a global name, and that all parts of the name should be used when looking up name_part_index = 0 name_parts = typed_name.name_parts # Certain types and names can be disqualified up front if name_parts.size > 1 # The name is in a name space. # Then entity cannot possible be in this module unless the name starts with the module name. # Note: If "module" represents a "global component", the module_name is nil and cannot match which is # ok since such a "module" cannot have namespaced content). # return nil unless name_parts[0] == module_name # Skip the first part of the name when computing the path since the path already contains the name of the # module name_part_index = 1 else # The name is in the global name space. # The only globally name-spaced elements that may be loaded from modules are functions and resource types case typed_name.type when :function when :resource_type else # anything else cannot possibly be in this module # TODO: should not be allowed anyway... may have to revisit this decision return nil end end # Get the paths that actually exist in this module (they are lazily processed once and cached). # The result is an array (that may be empty). # Find the file to instantiate, and instantiate the entity if file is found origin = nil if (smart_path = smart_paths.effective_paths(typed_name.type).find do |sp| origin = sp.effective_path(typed_name, name_part_index) existing_path(origin) end) value = smart_path.instantiator.create(self, typed_name, origin, get_contents(origin)) # cache the entry and return it set_entry(typed_name, value, origin) else nil end end # Abstract method that subclasses override that checks if it is meaningful to search using a generic smart path. # This optimization is performed to not be tricked into searching an empty directory over and over again. # The implementation may perform a deep search for file content other than directories and cache this in # and index. It is guaranteed that a call to meaningful_to_search? takes place before checking any other # path with relative_path_exists?. # # This optimization exists because many modules have been created from a template and they have # empty directories for functions, types, etc. (It is also the place to create a cached index of the content). # # @param relative_path [String] a path relative to the module's root # @return [Boolean] true if there is content in the directory appointed by the relative path # def meaningful_to_search?(smart_path) raise NotImplementedError.new end # Abstract method that subclasses override to answer if the given relative path exists, and if so returns that path # # @param relative_path [String] a path resolved by a smart path against the loader's root (if it has one) # @return [Boolean] true if the file exists # def existing_path(resolved_path) raise NotImplementedError.new end # Abstract method that subclasses override to produce the content of the effective path. # It should either succeed and return a String or fail with an exception. # # @param relative_path [String] a path as resolved by a smart path # @return [String] the content of the file # def get_contents(effective_path) raise NotImplementedError.new end # Abstract method that subclasses override to produce a source reference String used to identify the # system resource (resource in the URI sense). # # @param relative_path [String] a path relative to the module's root # @return [String] a reference to the source file (in file system, zip file, or elsewhere). # def get_source_ref(relative_path) raise NotImplementedError.new end + + # Produces the private loader for the module. If this module is not already resolved, this will trigger resolution + # + def private_loader + @private_loader ||= @loaders.private_loader_for_module(module_name) + end end # @api private # class FileBased < AbstractPathBasedModuleLoader attr_reader :smart_paths attr_reader :path_index # Create a kind of ModuleLoader for one module (Puppet Module, or module like) # # @param parent_loader [Puppet::Pops::Loader::Loader] typically the loader for the environment or root # @param module_name [String] the name of the module (non qualified name), may be nil for "modules" only containing globals # @param path [String] the path to the root of the module (semantics defined by subclass) # @param loader_name [String] a name that identifies the loader # - def initialize(parent_loader, module_name, path, loader_name) + def initialize(parent_loader, loaders, module_name, path, loader_name) super unless Puppet::FileSystem.directory?(path) raise ArgumentError, "The given module root path '#{path}' is not a directory (required for file system based module path entry)" end @path_index = Set.new() end def existing_path(effective_path) # Optimized, checks index instead of visiting file system @path_index.include?(effective_path) ? effective_path : nil end def meaningful_to_search?(smart_path) ! add_to_index(smart_path).empty? end def to_s() "(ModuleLoader::FileBased '#{loader_name()}' '#{module_name()}')" end def add_to_index(smart_path) found = Dir.glob(File.join(smart_path.generic_path, '**', "*#{smart_path.extension}")) @path_index.merge(found) found end def get_contents(effective_path) Puppet::FileSystem.read(effective_path) end end # Loads from a gem specified as a URI, gem://gemname/optional/path/in/gem, or just a String gemname. # The source reference (shown in errors etc.) is the expanded path of the gem as this is believed to be more # helpful - given the location it should be quite obvious which gem it is, without the location, the user would # need to go on a hunt for where the file actually is located. # # TODO: How does this get instantiated? Does the gemname refelect the name of the module (the namespace) # or is that specified a different way? Can a gem be the container of multiple modules? # # @api private # class GemBased < FileBased include Puppet::Pops::Loader::GemSupport attr_reader :gem_ref # Create a kind of ModuleLoader for one module # The parameters are: # * parent_loader - typically the loader for the root # * module_name - the name of the module (non qualified name) # * gem_ref - [URI, String] gem reference to the root of the module (URI, gem://gemname/optional/path/in/gem), or # just the gem's name as a String. # - def initialize(parent_loader, module_name, gem_ref, loader_name) + def initialize(parent_loader, loaders, module_name, gem_ref, loader_name) @gem_ref = gem_ref - super parent_loader, module_name, gem_dir(gem_ref), loader_name + super parent_loader, loaders, module_name, gem_dir(gem_ref), loader_name end def to_s() "(ModuleLoader::GemBased '#{loader_name()}' '#{@gem_ref}' [#{module_name()}])" end end end diff --git a/lib/puppet/pops/loader/ruby_function_instantiator.rb b/lib/puppet/pops/loader/ruby_function_instantiator.rb index d298eec2e..6c2b716cf 100644 --- a/lib/puppet/pops/loader/ruby_function_instantiator.rb +++ b/lib/puppet/pops/loader/ruby_function_instantiator.rb @@ -1,34 +1,34 @@ # The RubyFunctionInstantiator instantiates a Puppet::Functions::Function given the ruby source # that calls Puppet::Functions.create_function. # class Puppet::Pops::Loader::RubyFunctionInstantiator # Produces an instance of the Function class with the given typed_name, or fails with an error if the # given ruby source does not produce this instance when evaluated. # # @param loader [Puppet::Pops::Loader::Loader] The loader the function is associated with # @param typed_name [Puppet::Pops::Loader::TypedName] the type / name of the function to load # @param source_ref [URI, String] a reference to the source / origin of the ruby code to evaluate # @param ruby_code_string [String] ruby code in a string # # @return [Puppet::Pops::Functions.Function] - an instantiated function with global scope closure associated with the given loader # def self.create(loader, typed_name, source_ref, ruby_code_string) unless ruby_code_string.is_a?(String) && ruby_code_string =~ /Puppet\:\:Functions\.create_function/ raise ArgumentError, "The code loaded from #{source_ref} does not seem to be a Puppet 4x API function - no create_function call." end created = eval(ruby_code_string) unless created.is_a?(Class) raise ArgumentError, "The code loaded from #{source_ref} did not produce a Function class when evaluated. Got '#{created.class}'" end unless created.name.to_s == typed_name.name() raise ArgumentError, "The code loaded from #{source_ref} produced mis-matched name, expected '#{typed_name.name}', got #{created.name}" end # create the function instance - it needs closure (scope), and loader (i.e. where it should start searching for things # when calling functions etc. # It should be bound to global scope # TODO: Cheating wrt. scope - assuming it is found in the context closure_scope = Puppet.lookup(:global_scope) { {} } - created.new(closure_scope, loader) + created.new(closure_scope, loader.private_loader) end end diff --git a/lib/puppet/pops/loader/simple_environment_loader.rb b/lib/puppet/pops/loader/simple_environment_loader.rb index ef9a5994f..43e7a6f67 100644 --- a/lib/puppet/pops/loader/simple_environment_loader.rb +++ b/lib/puppet/pops/loader/simple_environment_loader.rb @@ -1,18 +1,20 @@ # SimpleEnvironmentLoader # === # This loader does not load anything and it is populated by the bootstrapping logic that loads # the site.pp or equivalent for an environment. It does not restrict the names of what it may contain, # and what is loaded here overrides any child loaders (modules). # class Puppet::Pops::Loader::SimpleEnvironmentLoader < Puppet::Pops::Loader::BaseLoader + attr_accessor :private_loader + # Never finds anything, everything "loaded" is set externally def find(typed_name) nil end def to_s() "(SimpleEnvironmentLoader '#{loader_name}')" end -end \ No newline at end of file +end diff --git a/lib/puppet/pops/loaders.rb b/lib/puppet/pops/loaders.rb index 076d66926..ed113291a 100644 --- a/lib/puppet/pops/loaders.rb +++ b/lib/puppet/pops/loaders.rb @@ -1,234 +1,240 @@ class Puppet::Pops::Loaders class LoaderError < Puppet::Error; end attr_reader :static_loader attr_reader :puppet_system_loader attr_reader :public_environment_loader attr_reader :private_environment_loader - def initialize() + def initialize(environment) # The static loader can only be changed after a reboot @@static_loader ||= Puppet::Pops::Loader::StaticLoader.new() # Create the set of loaders # 1. Puppet, loads from the "running" puppet - i.e. bundled functions, types, extension points and extensions # Does not change without rebooting the service running puppet. # @@puppet_system_loader ||= create_puppet_system_loader() # 2. Environment loader - i.e. what is bound across the environment, may change for each setup # TODO: loaders need to work when also running in an agent doing catalog application. There is no # concept of environment the same way as when running as a master (except when doing apply). # The creation mechanisms should probably differ between the two. # - @private_environment_loader = create_environment_loader() + @private_environment_loader = create_environment_loader(environment) # 3. module loaders are set up from the create_environment_loader, they register themselves end # Clears the cached static and puppet_system loaders (to enable testing) # def self.clear @@static_loader = nil @@puppet_system_loader = nil end def static_loader @@static_loader end def puppet_system_loader @@puppet_system_loader end - def self.create_loaders() - self.new() - end - def public_loader_for_module(module_name) md = @module_resolver[module_name] || (return nil) - # Note, this loader is not resolved until it is asked to load something it may contain + # Note, this loader is not resolved until there is interest in the visibility of entities from the + # perspective of something contained in the module. (Many request may pass through a module loader + # without it loading anything. + # See {#private_loader_for_module}, and not in {#configure_loaders_for_modules} md.public_loader end def private_loader_for_module(module_name) md = @module_resolver[module_name] || (return nil) + # Since there is interest in the visibility from the perspective of entities contained in the + # module, it must be resolved (to provide this visibility). + # See {#configure_loaders_for_modules} unless md.resolved? @module_resolver.resolve(md) end md.private_loader end private def create_puppet_system_loader() module_name = nil loader_name = 'puppet_system' # Puppet system may be installed in a fixed location via RPM, installed as a Gem, via source etc. # The only way to find this across the different ways puppet can be installed is # to search up the path from this source file's __FILE__ location until it finds the parent of # lib/puppet... e.g.. dirname(__FILE__)/../../.. (i.e. /lib/puppet/pops/loaders.rb). # puppet_lib = File.join(File.dirname(__FILE__), '../../..') - Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, module_name, puppet_lib, loader_name) + Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, self, module_name, puppet_lib, loader_name) end - def create_environment_loader() + def create_environment_loader(environment) # This defines where to start parsing/evaluating - the "initial import" (to use 3x terminology) # Is either a reference to a single .pp file, or a directory of manifests. If the environment becomes # a module and can hold functions, types etc. then these are available across all other modules without # them declaring this dependency - it is however valuable to be able to treat it the same way # bindings and other such system related configuration. # This is further complicated by the many options available: # - The environment may not have a directory, the code comes from one appointed 'manifest' (site.pp) # - The environment may have a directory and also point to a 'manifest' # - The code to run may be set in settings (code) # Further complication is that there is nothing specifying what the visibility is into # available modules. (3x is everyone sees everything). # Puppet binder currently reads confdir/bindings - that is bad, it should be using the new environment support. - current_environment = Puppet.lookup(:current_environment) # The environment is not a namespace, so give it a nil "module_name" module_name = nil - loader_name = "environment:#{current_environment.name}" - env_dir = Puppet[:environmentdir] - if env_dir.nil? - # Use an environment loader that can be populated externally - loader = Puppet::Pops::Loader::SimpleEnvironmentLoader.new(puppet_system_loader, loader_name) - else - envdir_path = File.join(env_dir, current_environment.name.to_s) - # TODO: Representing Environment as a Module - needs something different (not all types are supported), - # and it must be able to import .pp code from 3x manifest setting, or from code setting as well as from - # a manifests directory under the environment's root. The below is cheating... - # - loader = Puppet::Pops::Loader::ModuleLoaders::FileBased(puppet_system_loader, module_name, envdir_path, loader_name) - end + loader_name = "environment:#{environment.name}" + loader = Puppet::Pops::Loader::SimpleEnvironmentLoader.new(puppet_system_loader, loader_name) + # An environment has a module path even if it has a null loader - configure_loaders_for_modules(loader, current_environment) + configure_loaders_for_modules(loader, environment) # modules should see this loader @public_environment_loader = loader # Code in the environment gets to see all modules (since there is no metadata for the environment) # but since this is not given to the module loaders, they can not load global code (since they can not # have prior knowledge about this loader = Puppet::Pops::Loader::DependencyLoader.new(loader, "environment", @module_resolver.all_module_loaders()) + # The module loader gets the private loader via a lazy operation to look up the module's private loader. + # This does not work for an environment since it is not resolved the same way. + # TODO: The EnvironmentLoader could be a specialized loader instead of using a ModuleLoader to do the work. + # This is subject to future design - an Environment may move more in the direction of a Module. + @public_environment_loader.private_loader = loader loader end - def configure_loaders_for_modules(parent_loader, current_environment) + def configure_loaders_for_modules(parent_loader, environment) @module_resolver = mr = ModuleResolver.new() - current_environment.modules.each do |puppet_module| + environment.modules.each do |puppet_module| # Create data about this module md = LoaderModuleData.new(puppet_module) mr[puppet_module.name] = md - md.public_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(parent_loader, md.name, md.path, md.name) + md.public_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(parent_loader, self, md.name, md.path, md.name) end + # NOTE: Do not resolve all modules here - this is wasteful if only a subset of modules / functions are used + # The resolution is triggered by asking for a module's private loader, since this means there is interest + # in the visibility from that perspective. + # If later, it is wanted that all resolutions should be made up-front (to capture errors eagerly, this + # can be introduced (better for production), but may be irritating in development mode. end # =LoaderModuleData # Information about a Module and its loaders. # TODO: should have reference to real model element containing all module data; this is faking it # TODO: Should use Puppet::Module to get the metadata (as a hash) - a somewhat blunt instrument, but that is # what is available with a reasonable API. # class LoaderModuleData attr_accessor :state attr_accessor :public_loader attr_accessor :private_loader attr_accessor :resolutions # The Puppet::Module this LoaderModuleData represents in the loader configuration attr_reader :puppet_module # @param puppet_module [Puppet::Module] the module instance for the module being represented # def initialize(puppet_module) @state = :initial @puppet_module = puppet_module @resolutions = [] @public_loader = nil @private_loader = nil end def name @puppet_module.name end def version @puppet_module.version end def path @puppet_module.path end - def requirements - nil # FAKE: this says "wants to see everything" - end - def resolved? @state == :resolved end + + def restrict_to_dependencies? + @puppet_module.has_metadata? + end + + def unmet_dependencies? + @puppet_module.unmet_dependencies.any? + end + + def dependency_names + @puppet_module.dependencies_as_modules.collect(&:name) + end end # Resolves module loaders - resolution of model dependencies is done by Puppet::Module # class ModuleResolver def initialize() @index = {} @all_module_loaders = nil end def [](name) @index[name] end def []=(name, module_data) @index[name] = module_data end def all_module_loaders @all_module_loaders ||= @index.values.map {|md| md.public_loader } end def resolve(module_data) - return if module_data.resolved? - pm = module_data.puppet_module - # Resolution rules - # If dependencies.nil? means "see all other modules" (This to make older modules work, and modules w/o metadata) - # TODO: Control via flag/feature ? - module_data.private_loader = - if pm.dependencies.nil? - # see everything - if Puppet::Util::Log.level == :debug - Puppet.debug("ModuleLoader: module '#{module_data.name}' has unknown dependencies - it will have all other modules visible") - end - - Puppet::Pops::Loader::DependencyLoader.new(module_data.loader, module_data.name, all_module_loaders()) + if module_data.resolved? + return else - # If module has resolutions they must resolve - it will not see into other modules otherwise - # TODO: possible give errors if there are unresolved references - # i.e. !pm.unmet_dependencies.empty? (if module lacks metadata it is considered to have met all). - # The face "module" can display error information. - # Here, we are just giving up without explaining - the user can check with the module face (or console) - # - unless pm.unmet_dependencies.empty? - # TODO: Exception or just warning? - Puppet.warning("ModuleLoader: module '#{module_data.name}' has unresolved dependencies"+ - " - it will only see those that are resolved."+ - " Use 'puppet module list --tree' to see information about modules") - # raise Puppet::Pops::Loader::Loader::Error, "Loader Error: Module '#{module_data.name}' has unresolved dependencies - use 'puppet module list --tree' to see information" - end - dependency_loaders = pm.dependencies_as_modules.map { |dep| @index[dep.name].loader } - Puppet::Pops::Loader::DependencyLoader.new(module_data.loader, module_data.name, dependency_loaders) + module_data.private_loader = + if module_data.restrict_to_dependencies? + create_loader_with_only_dependencies_visible(module_data) + else + create_loader_with_all_modules_visible(module_data) + end end + end + + private + def create_loader_with_all_modules_visible(from_module_data) + Puppet.debug("ModuleLoader: module '#{from_module_data.name}' has unknown dependencies - it will have all other modules visible") + + Puppet::Pops::Loader::DependencyLoader.new(from_module_data.public_loader, from_module_data.name, all_module_loaders()) + end + + def create_loader_with_only_dependencies_visible(from_module_data) + if from_module_data.unmet_dependencies? + Puppet.warning("ModuleLoader: module '#{from_module_data.name}' has unresolved dependencies"+ + " - it will only see those that are resolved."+ + " Use 'puppet module list --tree' to see information about modules") + end + dependency_loaders = from_module_data.dependency_names.collect { |name| @index[name].public_loader } + Puppet::Pops::Loader::DependencyLoader.new(from_module_data.public_loader, from_module_data.name, dependency_loaders) end end -end \ No newline at end of file +end diff --git a/spec/fixtures/unit/pops/loaders/loaders/dependent_modules_with_metadata/usee/lib/puppet/functions/usee/callee.rb b/spec/fixtures/unit/pops/loaders/loaders/dependent_modules_with_metadata/usee/lib/puppet/functions/usee/callee.rb new file mode 100644 index 000000000..d3bf028d6 --- /dev/null +++ b/spec/fixtures/unit/pops/loaders/loaders/dependent_modules_with_metadata/usee/lib/puppet/functions/usee/callee.rb @@ -0,0 +1,5 @@ +Puppet::Functions.create_function(:'usee::callee') do + def callee(value) + "usee::callee() was told '#{value}'" + end +end diff --git a/spec/fixtures/unit/pops/loaders/loaders/dependent_modules_with_metadata/user/lib/puppet/functions/user/caller.rb b/spec/fixtures/unit/pops/loaders/loaders/dependent_modules_with_metadata/user/lib/puppet/functions/user/caller.rb new file mode 100644 index 000000000..bc1ed57dd --- /dev/null +++ b/spec/fixtures/unit/pops/loaders/loaders/dependent_modules_with_metadata/user/lib/puppet/functions/user/caller.rb @@ -0,0 +1,5 @@ +Puppet::Functions.create_function(:'user::caller') do + def caller() + call_function('usee::callee', 'passed value') + " + I am user::caller()" + end +end diff --git a/spec/fixtures/unit/pops/loaders/loaders/dependent_modules_with_metadata/user/metadata.json b/spec/fixtures/unit/pops/loaders/loaders/dependent_modules_with_metadata/user/metadata.json new file mode 100644 index 000000000..833e45932 --- /dev/null +++ b/spec/fixtures/unit/pops/loaders/loaders/dependent_modules_with_metadata/user/metadata.json @@ -0,0 +1,9 @@ +{ + "name": "test-user", + "author": "test", + "description": "", + "license": "", + "source": "", + "version": "1.0.0", + "dependencies": [{ "name": "test/usee" }] +} diff --git a/spec/fixtures/unit/pops/loaders/loaders/wo_metadata_module/modules/moduleb/lib/puppet/functions/moduleb/rb_func_b.rb b/spec/fixtures/unit/pops/loaders/loaders/wo_metadata_module/modules/moduleb/lib/puppet/functions/moduleb/rb_func_b.rb new file mode 100644 index 000000000..785b2f4dc --- /dev/null +++ b/spec/fixtures/unit/pops/loaders/loaders/wo_metadata_module/modules/moduleb/lib/puppet/functions/moduleb/rb_func_b.rb @@ -0,0 +1,6 @@ +Puppet::Functions.create_function(:'moduleb::rb_func_b') do + def rb_func_b() + # Should be able to call modulea::rb_func_a() + call_function('modulea::rb_func_a') + " + I am moduleb::rb_func_b()" + end +end \ No newline at end of file diff --git a/spec/fixtures/unit/pops/loaders/loaders/wo_metadata_module/modules/moduleb/manifests/init.pp b/spec/fixtures/unit/pops/loaders/loaders/wo_metadata_module/modules/moduleb/manifests/init.pp new file mode 100644 index 000000000..4cd7ab7b7 --- /dev/null +++ b/spec/fixtures/unit/pops/loaders/loaders/wo_metadata_module/modules/moduleb/manifests/init.pp @@ -0,0 +1,3 @@ +class moduleb { + +} diff --git a/spec/unit/functions/assert_type_spec.rb b/spec/unit/functions/assert_type_spec.rb index 7fb84d651..d47f47e30 100644 --- a/spec/unit/functions/assert_type_spec.rb +++ b/spec/unit/functions/assert_type_spec.rb @@ -1,52 +1,59 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/loaders' describe 'the assert_type function' do after(:all) { Puppet::Pops::Loaders.clear } + + around(:each) do |example| + loaders = Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, [])) + Puppet.override({:loaders => loaders}, "test-example") do + example.run + end + end + let(:func) do - loaders = Puppet::Pops::Loaders.new() - loaders.puppet_system_loader.load(:function, 'assert_type') + Puppet.lookup(:loaders).puppet_system_loader.load(:function, 'assert_type') end it 'asserts compliant type by returning the value' do expect(func.call({}, type(String), 'hello world')).to eql('hello world') end it 'accepts type given as a String' do expect(func.call({}, 'String', 'hello world')).to eql('hello world') end it 'asserts non compliant type by raising an error' do expect do func.call({}, type(Integer), 'hello world') end.to raise_error(Puppet::ParseError, /does not match actual/) end it 'checks that first argument is a type' do expect do func.call({}, 10, 10) end.to raise_error(ArgumentError, Regexp.new(Regexp.escape( "function 'assert_type' called with mis-matched arguments expected one of: assert_type(Type type, Optional[Object] value) - arg count {2} assert_type(String type_string, Optional[Object] value) - arg count {2} actual: assert_type(Integer, Integer) - arg count {2}"))) end it 'allows the second arg to be undef/nil)' do expect do func.call({}, optional(String), nil) end.to_not raise_error(ArgumentError) end def optional(type_ref) Puppet::Pops::Types::TypeFactory.optional(type(type_ref)) end def type(type_ref) Puppet::Pops::Types::TypeFactory.type_of(type_ref) end end diff --git a/spec/unit/functions4_spec.rb b/spec/unit/functions4_spec.rb index 9c82e98f4..a5404cc1e 100644 --- a/spec/unit/functions4_spec.rb +++ b/spec/unit/functions4_spec.rb @@ -1,671 +1,671 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/loaders' require 'puppet_spec/pops' require 'puppet_spec/scope' module FunctionAPISpecModule class TestDuck end class TestFunctionLoader < Puppet::Pops::Loader::StaticLoader def initialize @functions = {} end def add_function(name, function) typed_name = Puppet::Pops::Loader::Loader::TypedName.new(:function, name) entry = Puppet::Pops::Loader::Loader::NamedEntry.new(typed_name, function, __FILE__) @functions[typed_name] = entry end # override StaticLoader def load_constant(typed_name) @functions[typed_name] end end end describe 'the 4x function api' do include FunctionAPISpecModule include PuppetSpec::Pops include PuppetSpec::Scope let(:loader) { FunctionAPISpecModule::TestFunctionLoader.new } it 'allows a simple function to be created without dispatch declaration' do f = Puppet::Functions.create_function('min') do def min(x,y) x <= y ? x : y end end # the produced result is a Class inheriting from Function expect(f.class).to be(Class) expect(f.superclass).to be(Puppet::Functions::Function) # and this class had the given name (not a real Ruby class name) expect(f.name).to eql('min') end it 'refuses to create functions that are not based on the Function class' do expect do Puppet::Functions.create_function('testing', Object) {} end.to raise_error(ArgumentError, 'Functions must be based on Puppet::Pops::Functions::Function. Got Object') end it 'a function without arguments can be defined and called without dispatch declaration' do f = create_noargs_function_class() func = f.new(:closure_scope, :loader) expect(func.call({})).to eql(10) end it 'an error is raised when calling a no arguments function with arguments' do f = create_noargs_function_class() func = f.new(:closure_scope, :loader) expect{func.call({}, 'surprise')}.to raise_error(ArgumentError, "function 'test' called with mis-matched arguments expected: test() - arg count {0} actual: test(String) - arg count {1}") end it 'a simple function can be called' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect(func.call({}, 10,20)).to eql(10) end it 'an error is raised if called with too few arguments' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true signature = if RUBY_VERSION =~ /^1\.8/ 'Object{2}' else 'Object x, Object y' end expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2} actual: min(Integer) - arg count {1}") end it 'an error is raised if called with too many arguments' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true signature = if RUBY_VERSION =~ /^1\.8/ 'Object{2}' else 'Object x, Object y' end expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, Regexp.new(Regexp.escape( "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}"))) end it 'an error is raised if simple function-name and method are not matched' do expect do f = create_badly_named_method_function_class() end.to raise_error(ArgumentError, /Function Creation Error, cannot create a default dispatcher for function 'mix', no method with this name found/) end it 'the implementation separates dispatchers for different functions' do # this tests that meta programming / construction puts class attributes in the correct class f1 = create_min_function_class() f2 = create_max_function_class() d1 = f1.dispatcher d2 = f2.dispatcher expect(d1).to_not eql(d2) expect(d1.dispatchers[0]).to_not eql(d2.dispatchers[0]) end context 'when using regular dispatch' do it 'a function can be created using dispatch and called' do f = create_min_function_class_using_dispatch() func = f.new(:closure_scope, :loader) expect(func.call({}, 3,4)).to eql(3) end it 'an error is raised with reference to given parameter names when called with mis-matched arguments' do f = create_min_function_class_using_dispatch() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, Regexp.new(Regexp.escape( "function 'min' called with mis-matched arguments expected: min(Numeric a, Numeric b) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}"))) end it 'an error includes optional indicators and count for last element' do f = create_function_with_optionals_and_varargs() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true signature = if RUBY_VERSION =~ /^1\.8/ 'Object{2,}' else 'Object x, Object y, Object a?, Object b?, Object c{0,}' end expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2,} actual: min(Integer) - arg count {1}") end it 'an error includes optional indicators and count for last element when defined via dispatch' do f = create_function_with_optionals_and_varargs_via_dispatch() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(Numeric x, Numeric y, Numeric a?, Numeric b?, Numeric c{0,}) - arg count {2,} actual: min(Integer) - arg count {1}") end it 'a function can be created using dispatch and called' do f = create_min_function_class_disptaching_to_two_methods() func = f.new(:closure_scope, :loader) expect(func.call({}, 3,4)).to eql(3) expect(func.call({}, 'Apple', 'Banana')).to eql('Apple') end it 'an error is raised with reference to multiple methods when called with mis-matched arguments' do f = create_min_function_class_disptaching_to_two_methods() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected one of: min(Numeric a, Numeric b) - arg count {2} min(String s1, String s2) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}") end context 'can use injection' do before :all do injector = Puppet::Pops::Binder::Injector.create('test') do bind.name('a_string').to('evoe') bind.name('an_int').to(42) end Puppet.push_context({:injector => injector}, "injector for testing function API") end after :all do Puppet.pop_context() end it 'attributes can be injected' do f1 = create_function_with_class_injection() f = f1.new(:closure_scope, :loader) expect(f.test_attr2()).to eql("evoe") expect(f.serial().produce(nil)).to eql(42) expect(f.test_attr().class.name).to eql("FunctionAPISpecModule::TestDuck") end it 'parameters can be injected and woven with regular dispatch' do f1 = create_function_with_param_injection_regular() f = f1.new(:closure_scope, :loader) expect(f.call(nil, 10, 20)).to eql("evoe! 10, and 20 < 42 = true") expect(f.call(nil, 50, 20)).to eql("evoe! 50, and 20 < 42 = false") end end context 'when requesting a type' do it 'responds with a Callable for a single signature' do tf = Puppet::Pops::Types::TypeFactory fc = create_min_function_class_using_dispatch() t = fc.dispatcher.to_type expect(t.class).to be(Puppet::Pops::Types::PCallableType) expect(t.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t.param_types.types).to eql([tf.numeric(), tf.numeric()]) expect(t.block_type).to be_nil end it 'responds with a Variant[Callable...] for multiple signatures' do tf = Puppet::Pops::Types::TypeFactory fc = create_min_function_class_disptaching_to_two_methods() t = fc.dispatcher.to_type expect(t.class).to be(Puppet::Pops::Types::PVariantType) expect(t.types.size).to eql(2) t1 = t.types[0] expect(t1.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t1.param_types.types).to eql([tf.numeric(), tf.numeric()]) expect(t1.block_type).to be_nil t2 = t.types[1] expect(t2.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t2.param_types.types).to eql([tf.string(), tf.string()]) expect(t2.block_type).to be_nil end end context 'supports lambdas' do it 'such that, a required block can be defined and given as an argument' do # use a Function as callable the_callable = create_min_function_class().new(:closure_scope, :loader) the_function = create_function_with_required_block_all_defaults().new(:closure_scope, :loader) result = the_function.call({}, 10, the_callable) expect(result).to be(the_callable) end it 'such that, a missing required block when called raises an error' do # use a Function as callable the_function = create_function_with_required_block_all_defaults().new(:closure_scope, :loader) expect do the_function.call({}, 10) end.to raise_error(ArgumentError, "function 'test' called with mis-matched arguments expected: test(Integer x, Callable block) - arg count {2} actual: test(Integer) - arg count {1}") end it 'such that, an optional block can be defined and given as an argument' do # use a Function as callable the_callable = create_min_function_class().new(:closure_scope, :loader) the_function = create_function_with_optional_block_all_defaults().new(:closure_scope, :loader) result = the_function.call({}, 10, the_callable) expect(result).to be(the_callable) end it 'such that, an optional block can be omitted when called and gets the value nil' do # use a Function as callable the_function = create_function_with_optional_block_all_defaults().new(:closure_scope, :loader) expect(the_function.call({}, 10)).to be_nil end end context 'provides signature information' do it 'about capture rest (varargs)' do fc = create_function_with_optionals_and_varargs signatures = fc.signatures expect(signatures.size).to eql(1) signature = signatures[0] expect(signature.last_captures_rest?).to be_true end it 'about optional and required parameters' do fc = create_function_with_optionals_and_varargs signature = fc.signatures[0] expect(signature.args_range).to eql( [2, Puppet::Pops::Types::INFINITY ] ) expect(signature.infinity?(signature.args_range[1])).to be_true end it 'about block not being allowed' do fc = create_function_with_optionals_and_varargs signature = fc.signatures[0] expect(signature.block_range).to eql( [ 0, 0 ] ) expect(signature.block_type).to be_nil end it 'about required block' do fc = create_function_with_required_block_all_defaults signature = fc.signatures[0] expect(signature.block_range).to eql( [ 1, 1 ] ) expect(signature.block_type).to_not be_nil end it 'about optional block' do fc = create_function_with_optional_block_all_defaults signature = fc.signatures[0] expect(signature.block_range).to eql( [ 0, 1 ] ) expect(signature.block_type).to_not be_nil end it 'about the type' do fc = create_function_with_optional_block_all_defaults signature = fc.signatures[0] expect(signature.type.class).to be(Puppet::Pops::Types::PCallableType) end # conditional on Ruby 1.8.7 which does not do parameter introspection if Method.method_defined?(:parameters) it 'about parameter names obtained from ruby introspection' do fc = create_min_function_class signature = fc.signatures[0] expect(signature.parameter_names).to eql(['x', 'y']) end end it 'about parameter names specified with dispatch' do fc = create_min_function_class_using_dispatch signature = fc.signatures[0] expect(signature.parameter_names).to eql(['a', 'b']) end it 'about block_name when it is *not* given in the definition' do # neither type, nor name fc = create_function_with_required_block_all_defaults signature = fc.signatures[0] expect(signature.block_name).to eql('block') # no name given, only type fc = create_function_with_required_block_given_type signature = fc.signatures[0] expect(signature.block_name).to eql('block') end it 'about block_name when it *is* given in the definition' do # neither type, nor name fc = create_function_with_required_block_default_type signature = fc.signatures[0] expect(signature.block_name).to eql('the_block') # no name given, only type fc = create_function_with_required_block_fully_specified signature = fc.signatures[0] expect(signature.block_name).to eql('the_block') end end context 'supports calling other functions' do before(:all) do - Puppet.push_context( {:loaders => Puppet::Pops::Loaders.new()}) + Puppet.push_context( {:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))}) end after(:all) do Puppet.pop_context() end it 'such that, other functions are callable by name' do fc = Puppet::Functions.create_function(:test) do def test() # Call a function available in the puppet system call_function('assert_type', 'Integer', 10) end end # initiate the function the same way the loader initiates it f = fc.new(:closure_scope, Puppet.lookup(:loaders).puppet_system_loader) expect(f.call({})).to eql(10) end it 'such that, calling a non existing function raises an error' do fc = Puppet::Functions.create_function(:test) do def test() # Call a function not available in the puppet system call_function('no_such_function', 'Integer', 'hello') end end # initiate the function the same way the loader initiates it f = fc.new(:closure_scope, Puppet.lookup(:loaders).puppet_system_loader) expect{f.call({})}.to raise_error(ArgumentError, "Function test(): cannot call function 'no_such_function' - not found") end end context 'supports calling ruby functions with lambda from puppet' do before(:all) do - Puppet.push_context( {:loaders => Puppet::Pops::Loaders.new()}) + Puppet.push_context( {:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))}) end after(:all) do Puppet.pop_context() end before(:each) do Puppet[:strict_variables] = true # These must be set since the is 3x logic that triggers on these even if the tests are explicit # about selection of parser and evaluator # Puppet[:parser] = 'future' Puppet[:evaluator] = 'future' # Puppetx cannot be loaded until the correct parser has been set (injector is turned off otherwise) require 'puppetx' end let(:parser) { Puppet::Pops::Parser::EvaluatingParser::Transitional.new } let(:node) { 'node.example.com' } let(:scope) { s = create_test_scope_for_node(node); s } it 'function with required block can be called' do # construct ruby function to call fc = Puppet::Functions.create_function('testing::test') do dispatch :test do param 'Integer', 'x' # block called 'the_block', and using "all_callables" required_block_param #(all_callables(), 'the_block') end def test(x, block) # call the block with x block.call(closure_scope, x) end end # add the function to the loader (as if it had been loaded from somewhere) the_loader = loader() f = fc.new({}, the_loader) loader.add_function('testing::test', f) # evaluate a puppet call source = "testing::test(10) |$x| { $x+1 }" program = parser.parse_string(source, __FILE__) Puppet::Pops::Adapters::LoaderAdapter.adapt(program.model).loader = the_loader expect(parser.evaluate(scope, program)).to eql(11) end end end def create_noargs_function_class f = Puppet::Functions.create_function('test') do def test() 10 end end end def create_min_function_class f = Puppet::Functions.create_function('min') do def min(x,y) x <= y ? x : y end end end def create_max_function_class f = Puppet::Functions.create_function('max') do def max(x,y) x >= y ? x : y end end end def create_badly_named_method_function_class f = Puppet::Functions.create_function('mix') do def mix_up(x,y) x <= y ? x : y end end end def create_min_function_class_using_dispatch f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', 'a' param 'Numeric', 'b' end def min(x,y) x <= y ? x : y end end end def create_min_function_class_disptaching_to_two_methods f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', 'a' param 'Numeric', 'b' end dispatch :min_s do param 'String', 's1' param 'String', 's2' end def min(x,y) x <= y ? x : y end def min_s(x,y) cmp = (x.downcase <=> y.downcase) cmp <= 0 ? x : y end end end def create_function_with_optionals_and_varargs f = Puppet::Functions.create_function('min') do def min(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_optionals_and_varargs_via_dispatch f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', 'x' param 'Numeric', 'y' param 'Numeric', 'a' param 'Numeric', 'b' param 'Numeric', 'c' arg_count 2, :default end def min(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_class_injection f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do attr_injected Puppet::Pops::Types::TypeFactory.type_of(FunctionAPISpecModule::TestDuck), :test_attr attr_injected Puppet::Pops::Types::TypeFactory.string(), :test_attr2, "a_string" attr_injected_producer Puppet::Pops::Types::TypeFactory.integer(), :serial, "an_int" def test(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_param_injection_regular f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do attr_injected Puppet::Pops::Types::TypeFactory.type_of(FunctionAPISpecModule::TestDuck), :test_attr attr_injected Puppet::Pops::Types::TypeFactory.string(), :test_attr2, "a_string" attr_injected_producer Puppet::Pops::Types::TypeFactory.integer(), :serial, "an_int" dispatch :test do injected_param Puppet::Pops::Types::TypeFactory.string, 'x', 'a_string' injected_producer_param Puppet::Pops::Types::TypeFactory.integer, 'y', 'an_int' param 'Scalar', 'a' param 'Scalar', 'b' end def test(x,y,a,b) y_produced = y.produce(nil) "#{x}! #{a}, and #{b} < #{y_produced} = #{ !!(a < y_produced && b < y_produced)}" end end end def create_function_with_required_block_all_defaults f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' required_block_param end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_required_block_default_type f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' required_block_param 'the_block' end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_required_block_given_type f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' required_block_param end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_required_block_fully_specified f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' required_block_param('Callable', 'the_block') end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_optional_block_all_defaults f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' optional_block_param end def test(x, block=nil) # returns the block to make it easy to test what it got when called # a default of nil must be used or the call will fail with a missing parameter block end end end end diff --git a/spec/unit/pops/loaders/dependency_loader_spec.rb b/spec/unit/pops/loaders/dependency_loader_spec.rb index 5f12528a2..dbea5b208 100644 --- a/spec/unit/pops/loaders/dependency_loader_spec.rb +++ b/spec/unit/pops/loaders/dependency_loader_spec.rb @@ -1,43 +1,44 @@ 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, 'testmodule', module_dir, 'test1') + 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, 'testmodule', module_dir, 'test1') + 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 end def typed_name(type, name) Puppet::Pops::Loader::Loader::TypedName.new(type, name) end end diff --git a/spec/unit/pops/loaders/loader_paths_spec.rb b/spec/unit/pops/loaders/loader_paths_spec.rb index 4e098a097..2929fe7f8 100644 --- a/spec/unit/pops/loaders/loader_paths_spec.rb +++ b/spec/unit/pops/loaders/loader_paths_spec.rb @@ -1,71 +1,66 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' describe 'loader paths' do include PuppetSpec::Files before(:each) { Puppet[:biff] = true } let(:static_loader) { Puppet::Pops::Loader::StaticLoader.new() } - - it 'expects dir_containing to create a temp directory structure from a hash' do - module_dir = dir_containing('testmodule', { 'test.txt' => 'Hello world', 'sub' => { 'foo.txt' => 'foo'}}) - expect(File.read(File.join(module_dir, 'test.txt'))).to be_eql('Hello world') - expect(File.read(File.join(module_dir, 'sub', 'foo.txt'))).to be_eql('foo') - end + let(:unused_loaders) { nil } describe 'the relative_path_for_types method' do it 'produces paths to load in precendence order' do module_dir = dir_containing('testmodule', { 'functions' => {}, 'lib' => { 'puppet' => { 'functions' => {}, 'parser' => { 'functions' => {}, } }}}) - # Must have a File/Path based loader to talk to - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, 'testmodule', module_dir, 'test1') - effective_paths = Puppet::Pops::Loader::LoaderPaths.relative_paths_for_type(:function, module_loader) - expect(effective_paths.size).to be_eql(2) - # 4x - expect(effective_paths[0].generic_path).to be_eql(File.join(module_dir, 'lib', 'puppet', 'functions')) - # 3x - expect(effective_paths[1].generic_path).to be_eql(File.join(module_dir, 'lib', 'puppet','parser', 'functions')) + module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, unused_loaders, 'testmodule', module_dir, 'test1') + + effective_paths = Puppet::Pops::Loader::LoaderPaths.relative_paths_for_type(:function, module_loader) + + expect(effective_paths.collect(&:generic_path)).to eq([ + File.join(module_dir, 'lib', 'puppet', 'functions'), # 4x functions + File.join(module_dir, 'lib', 'puppet','parser', 'functions') # 3x functions + ]) end it 'module loader has smart-paths that prunes unavailable paths' do module_dir = dir_containing('testmodule', {'lib' => {'puppet' => {'functions' => {'foo.rb' => 'Puppet::Functions.create_function("testmodule::foo") { def foo; end; }' }}}}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, 'testmodule', module_dir, 'test1') + module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, unused_loaders, 'testmodule', module_dir, 'test1') effective_paths = module_loader.smart_paths.effective_paths(:function) expect(effective_paths.size).to be_eql(1) expect(effective_paths[0].generic_path).to be_eql(File.join(module_dir, 'lib', 'puppet', 'functions')) expect(module_loader.path_index.size).to be_eql(1) expect(module_loader.path_index.include?(File.join(module_dir, 'lib', 'puppet', 'functions', 'foo.rb'))).to be(true) end it 'all function smart-paths produces entries if they exist' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => {'foo4x.rb' => 'ignored in this test'}, 'parser' => { 'functions' => {'foo3x.rb' => 'ignored in this test'}, } }}}) - # Must have a File/Path based loader to talk to - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, 'testmodule', module_dir, 'test1') + module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, unused_loaders, 'testmodule', module_dir, 'test1') + effective_paths = module_loader.smart_paths.effective_paths(:function) + expect(effective_paths.size).to eq(2) expect(module_loader.path_index.size).to eq(2) path_index = module_loader.path_index expect(path_index.include?(File.join(module_dir, 'lib', 'puppet', 'functions', 'foo4x.rb'))).to eq(true) expect(path_index.include?(File.join(module_dir, 'lib', 'puppet', 'parser', 'functions', 'foo3x.rb'))).to eq(true) end end - end diff --git a/spec/unit/pops/loaders/loaders_spec.rb b/spec/unit/pops/loaders/loaders_spec.rb index a3ee91ac3..daa9c716c 100644 --- a/spec/unit/pops/loaders/loaders_spec.rb +++ b/spec/unit/pops/loaders/loaders_spec.rb @@ -1,78 +1,105 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' describe 'loaders' do include PuppetSpec::Files - def config_dir(config_name) - my_fixture(config_name) - end + 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() - expect(loaders.puppet_system_loader().class).to be(Puppet::Pops::Loader::ModuleLoaders::FileBased) + 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() - # When this test is running, there is no environments dir configured, and a NullLoader is therefore used a.t.m - expect(loaders.public_environment_loader().class).to be(Puppet::Pops::Loader::SimpleEnvironmentLoader) - # The default name of the enironment is '*root*', and the loader should identify itself that way - expect(loaders.public_environment_loader().to_s).to eql("(SimpleEnvironmentLoader 'environment:*root*')") + loaders = Puppet::Pops::Loaders.new(empty_test_env) - expect(loaders.private_environment_loader().class).to be(Puppet::Pops::Loader::DependencyLoader) + 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 - context 'when delegating 3x to 4x' do - before(:each) { Puppet[:biff] = true } + 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 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) - it 'the puppet system loader can load 3x functions' do - loaders = Puppet::Pops::Loaders.new() - 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.is_a?(Puppet::Functions::Function)).to eq(true) - end + 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 - # TODO: LOADING OF MODULES ON MODULEPATH - context 'loading from path with single module' do - before do - env = Puppet::Node::Environment.create(:'*test*', [File.join(config_dir('single_module'), 'modules')], '') - overrides = { - :current_environment => env - } - Puppet.push_context(overrides, "single-module-test-loaders") - end - - after do - Puppet.pop_context() - end - - it 'can load from a module path' do - loaders = Puppet::Pops::Loaders.new() - modulea_loader = loaders.public_loader_for_module('modulea') - expect(modulea_loader.class).to eql(Puppet::Pops::Loader::ModuleLoaders::FileBased) - - function = modulea_loader.load_typed(typed_name(:function, 'rb_func_a')).value - expect(function.is_a?(Puppet::Functions::Function)).to eq(true) - expect(function.class.name).to eq('rb_func_a') - - function = modulea_loader.load_typed(typed_name(:function, 'modulea::rb_func_a')).value - expect(function.is_a?(Puppet::Functions::Function)).to eq(true) - expect(function.class.name).to eq('modulea::rb_func_a') - 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 diff --git a/spec/unit/pops/loaders/module_loaders_spec.rb b/spec/unit/pops/loaders/module_loaders_spec.rb index 24bad0cf4..9d0c1a86d 100644 --- a/spec/unit/pops/loaders/module_loaders_spec.rb +++ b/spec/unit/pops/loaders/module_loaders_spec.rb @@ -1,122 +1,119 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' -describe 'module loaders' do +describe 'FileBased module loader' do include PuppetSpec::Files let(:static_loader) { Puppet::Pops::Loader::StaticLoader.new() } + let(:loaders) { Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, [])) } + + it 'can load a 4x function API ruby function in global name space' do + module_dir = dir_containing('testmodule', { + 'lib' => { + 'puppet' => { + 'functions' => { + 'foo4x.rb' => <<-CODE + Puppet::Functions.create_function(:foo4x) do + def foo4x() + 'yay' + end + end + CODE + } + } + } + }) - describe 'FileBased module loader' do - it 'can load a 4x function API ruby function in global name space' do - module_dir = dir_containing('testmodule', { - 'lib' => { - 'puppet' => { - 'functions' => { + module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') + function = module_loader.load_typed(typed_name(:function, 'foo4x')).value + + expect(function.class.name).to eq('foo4x') + expect(function.is_a?(Puppet::Functions::Function)).to eq(true) + end + + it 'can load a 4x function API ruby function in qualified name space' do + module_dir = dir_containing('testmodule', { + 'lib' => { + 'puppet' => { + 'functions' => { + 'testmodule' => { 'foo4x.rb' => <<-CODE - Puppet::Functions.create_function(:foo4x) do + Puppet::Functions.create_function('testmodule::foo4x') do def foo4x() 'yay' end end CODE - } } } - }) + } + }}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, 'testmodule', module_dir, 'test1') - function = module_loader.load_typed(typed_name(:function, 'foo4x')).value - expect(function.class.name).to eq('foo4x') - expect(function.is_a?(Puppet::Functions::Function)).to eq(true) - end + module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') + function = module_loader.load_typed(typed_name(:function, 'testmodule::foo4x')).value + expect(function.class.name).to eq('testmodule::foo4x') + expect(function.is_a?(Puppet::Functions::Function)).to eq(true) + end + + it 'makes parent loader win over entries in child' do + module_dir = dir_containing('testmodule', { + 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { + 'foo.rb' => <<-CODE + Puppet::Functions.create_function('testmodule::foo') do + def foo() + 'yay' + end + end + CODE + }}}}}) + module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') + + module_dir2 = dir_containing('testmodule2', { + 'lib' => { 'puppet' => { 'functions' => { 'testmodule2' => { + 'foo.rb' => <<-CODE + raise "should not get here" + CODE + }}}}}) + module_loader2 = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(module_loader, loaders, 'testmodule2', module_dir2, 'test2') + + function = module_loader2.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 a 4x function API ruby function in qualified name space' do + context 'when delegating 3x to 4x' do + before(:each) { Puppet[:biff] = true } + + it 'can load a 3x function API ruby function in global name space' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { - 'functions' => { - 'testmodule' => { - 'foo4x.rb' => <<-CODE - Puppet::Functions.create_function('testmodule::foo4x') do - def foo4x() - 'yay' - end - end + 'parser' => { + 'functions' => { + 'foo3x.rb' => <<-CODE + Puppet::Parser::Functions::newfunction( + :foo3x, :type => :rvalue, + :arity => 1 + ) do |args| + args[0] + end CODE - } + } } } }}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, 'testmodule', module_dir, 'test1') - function = module_loader.load_typed(typed_name(:function, 'testmodule::foo4x')).value - expect(function.class.name).to eq('testmodule::foo4x') - expect(function.is_a?(Puppet::Functions::Function)).to eq(true) - end - - it 'makes parent loader win over entries in child' do - module_dir = dir_containing('testmodule', { - 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { - 'foo.rb' => <<-CODE - Puppet::Functions.create_function('testmodule::foo') do - def foo() - 'yay' - end - end - CODE - }}}}}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, 'testmodule', module_dir, 'test1') - - module_dir2 = dir_containing('testmodule2', { - 'lib' => { 'puppet' => { 'functions' => { 'testmodule2' => { - 'foo.rb' => <<-CODE - raise "should not get here" - CODE - }}}}}) - module_loader2 = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(module_loader, 'testmodule2', module_dir2, 'test2') - - function = module_loader2.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 - - context 'when delegating 3x to 4x' do - before(:each) { Puppet[:biff] = true } - - it 'can load a 3x function API ruby function in global name space' do - module_dir = dir_containing('testmodule', { - 'lib' => { - 'puppet' => { - 'parser' => { - 'functions' => { - 'foo3x.rb' => <<-CODE - Puppet::Parser::Functions::newfunction( - :foo3x, :type => :rvalue, - :arity => 1 - ) do |args| - args[0] - end - CODE - } - } - } - }}) - - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, 'testmodule', module_dir, 'test1') - function = module_loader.load_typed(typed_name(:function, 'foo3x')).value - expect(function.class.name).to eq('foo3x') - expect(function.is_a?(Puppet::Functions::Function)).to eq(true) - end + module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') + function = module_loader.load_typed(typed_name(:function, 'foo3x')).value + expect(function.class.name).to eq('foo3x') + expect(function.is_a?(Puppet::Functions::Function)).to eq(true) end - - # Gives error when loading something with mismatched name - end def typed_name(type, name) Puppet::Pops::Loader::Loader::TypedName.new(type, name) end end