diff --git a/acceptance/tests/loader/func4x_loadable_from_modules.rb b/acceptance/tests/loader/func4x_loadable_from_modules.rb new file mode 100644 index 000000000..53690a9ec --- /dev/null +++ b/acceptance/tests/loader/func4x_loadable_from_modules.rb @@ -0,0 +1,76 @@ +test_name "Exercise a module with 4x function and 4x system function" + +# Purpose: +# Test that a packed puppet can call a 4x system function, and that a 4x function in +# a module can be called. +# +# Method: +# * Manually construct a very simple module with a manifest that creates a file. +# * The file has content that depends on logic that calls both a system function (reduce), and +# a function supplied in the module (helloworld::mul10). +# * The module is manually constructed to allow the test to also run on Windows where the module tool +# is not supported. +# * The module is included by calling 'include' from 'puppet apply'. +# * Puppet apply is executed to generate the file with the content. +# * The generated contents is asserted. + +# TODO: The test can be improved by adding yet another module that calls the function in helloworld. +# TODO: The test can be improved to also test loading of a non namespaced function + +require 'puppet/acceptance/temp_file_utils' +extend Puppet::Acceptance::TempFileUtils +initialize_temp_dirs + +agents.each do |agent| + # The modulepath to use in environment 'dev' + envs_path = get_test_file_path(agent, 'environments') + dev_modulepath = get_test_file_path(agent, 'environments/dev/modules') + target_path = get_test_file_path(agent, 'output') + mkdirs agent, target_path + + # make sure that we use the modulepath from the dev environment + puppetconf = get_test_file_path(agent, 'puppet.conf') + on agent, puppet("config", "set", "environment", "dev", "--section", "user", "--config", puppetconf) + on agent, puppet("config", "set", "environmentpath", envs_path, "--section", "main", "--config", puppetconf) + + # Where the functions in the written modules should go + helloworld_functions = 'helloworld/lib/puppet/functions/helloworld' + # Clean out the module that will be written to ensure no interference from a previous run + on agent, "rm -rf #{File.join(dev_modulepath, 'helloworld')}" + mkdirs agent, File.join(dev_modulepath, helloworld_functions) + + # Write a module + # Write the function helloworld::mul10, that multiplies its argument by 10 + create_remote_file(agent, File.join(dev_modulepath, helloworld_functions, "mul10.rb"), <<'SOURCE') +Puppet::Functions.create_function(:'helloworld::mul10') do + def mul10(x) + x * 10 + end +end +SOURCE + + # Write a manifest that calls a 4x function (reduce), and calls a function defined in the module + # (helloworld::mul10). + # + mkdirs agent, File.join(dev_modulepath, "helloworld", "manifests") + create_remote_file(agent, File.join(dev_modulepath, "helloworld", "manifests", "init.pp"), < 'file', + mode => '0666', + content => [1,2,3].reduce("Generated") |$memo, $n| { + "${memo}, ${n} => ${helloworld::mul10($n)}" + } + } +} +SOURCE + + # Run apply to generate the file with the output + on agent, puppet('apply', '--parser', 'future', '-e', "'include helloworld'", '--config', puppetconf) + + # Assert that the file was written with the generated content + on(agent, "cat #{File.join(target_path, 'result.txt')}") do + assert_match(/^Generated, 1 => 10, 2 => 20, 3 => 30$/, stdout, "Generated the wrong content") + end + +end diff --git a/lib/puppet/pops/loader/loader_paths.rb b/lib/puppet/pops/loader/loader_paths.rb index 09bb7e5b0..505887915 100644 --- a/lib/puppet/pops/loader/loader_paths.rb +++ b/lib/puppet/pops/loader/loader_paths.rb @@ -1,118 +1,118 @@ # LoaderPaths # === # The central loader knowledge about paths, what they represent and how to instantiate from them. # Contains helpers (*smart paths*) to deal with lazy resolution of paths. # # TODO: Currently only supports loading of functions (3 kinds) # module Puppet::Pops::Loader::LoaderPaths # Returns an array of SmartPath, each instantiated with a reference to the given loader (for root path resolution # and existence checks). The smart paths in the array appear in precedence order. The returned array may be # mutated. # def self.relative_paths_for_type(type, loader) result = case type when :function [FunctionPath4x.new(loader)] else # unknown types, simply produce an empty result; no paths to check, nothing to find... move along... [] end result end # # DO NOT REMOVE YET. needed later? when there is the need to decamel a classname # def de_camel(fq_name) # fq_name.to_s.gsub(/::/, '/'). # gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). # gsub(/([a-z\d])([A-Z])/,'\1_\2'). # tr("-", "_"). # downcase # end class SmartPath # Generic path, in the sense of "if there are any entities of this kind to load, where are they?" attr_reader :generic_path # Creates SmartPath for the given loader (loader knows how to check for existence etc.) def initialize(loader) @loader = loader end def generic_path() return @generic_path unless @generic_path.nil? root_path = @loader.path @generic_path = (root_path.nil? ? relative_path : File.join(root_path, relative_path)) end # Effective path is the generic path + the name part(s) + extension. # def effective_path(typed_name, start_index_in_name) "#{File.join(generic_path, typed_name.name_parts)}#{extension}" end def relative_path() raise NotImplementedError.new end def instantiator() raise NotImplementedError.new end end class RubySmartPath < SmartPath def extension ".rb" end # Duplication of extension information, but avoids one call def effective_path(typed_name, start_index_in_name) "#{File.join(generic_path, typed_name.name_parts)}.rb" end end class FunctionPath4x < RubySmartPath - FUNCTION_PATH_4X = File.join('lib', 'puppet', 'functions') + FUNCTION_PATH_4X = File.join('puppet', 'functions') def relative_path FUNCTION_PATH_4X end def instantiator() Puppet::Pops::Loader::RubyFunctionInstantiator end end # SmartPaths # === # Holds effective SmartPath instances per type # class SmartPaths def initialize(path_based_loader) @loader = path_based_loader @smart_paths = {} end # Ensures that the paths for the type have been probed and pruned to what is existing relative to # the given root. # # @param type [Symbol] the entity type to load # @return [Array] array of effective paths for type (may be empty) # def effective_paths(type) smart_paths = @smart_paths loader = @loader unless effective_paths = smart_paths[type] # type not yet processed, does the various directories for the type exist ? # Get the relative dirs for the type paths_for_type = Puppet::Pops::Loader::LoaderPaths.relative_paths_for_type(type, loader) # Check which directories exist in the loader's content/index effective_paths = smart_paths[type] = paths_for_type.select { |sp| loader.meaningful_to_search?(sp) } end effective_paths end end end diff --git a/lib/puppet/pops/loader/module_loaders.rb b/lib/puppet/pops/loader/module_loaders.rb index 40ff5141d..3edd51561 100644 --- a/lib/puppet/pops/loader/module_loaders.rb +++ b/lib/puppet/pops/loader/module_loaders.rb @@ -1,242 +1,256 @@ # =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 + def self.system_loader_from(parent_loader, loaders) + # 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 base of + # puppet. + # + puppet_lib = File.join(File.dirname(__FILE__), '../../..') + Puppet::Pops::Loader::ModuleLoaders::FileBased.new(parent_loader, + loaders, + nil, + puppet_lib, + 'puppet_system') + end + + def self.module_loader_from(parent_loader, loaders, module_name, module_path) + Puppet::Pops::Loader::ModuleLoaders::FileBased.new(parent_loader, + loaders, + module_name, + File.join(module_path, 'lib'), + module_name) + end + 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, 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 smart_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 resolved_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 effective_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, 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, loaders, module_name, gem_ref, loader_name) @gem_ref = gem_ref 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/loaders.rb b/lib/puppet/pops/loaders.rb index ed113291a..f6a147fe1 100644 --- a/lib/puppet/pops/loaders.rb +++ b/lib/puppet/pops/loaders.rb @@ -1,240 +1,231 @@ 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(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(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 public_loader_for_module(module_name) md = @module_resolver[module_name] || (return nil) # 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, self, module_name, puppet_lib, loader_name) + Puppet::Pops::Loader::ModuleLoaders.system_loader_from(static_loader, self) end 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. # The environment is not a namespace, so give it a nil "module_name" module_name = nil 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, 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, environment) @module_resolver = mr = ModuleResolver.new() 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, self, md.name, md.path, md.name) + md.public_loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(parent_loader, self, md.name, md.path) 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 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) if module_data.resolved? return else 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 diff --git a/spec/unit/functions/epp_spec.rb b/spec/unit/functions/epp_spec.rb index 382fd9548..da58748b3 100644 --- a/spec/unit/functions/epp_spec.rb +++ b/spec/unit/functions/epp_spec.rb @@ -1,155 +1,154 @@ - require 'spec_helper' describe "the epp function" do include PuppetSpec::Files before :all do Puppet::Parser::Functions.autoloader.loadall end before :each do Puppet[:parser] = 'future' end let :node do Puppet::Node.new('localhost') end let :compiler do Puppet::Parser::Compiler.new(node) end let :scope do Puppet::Parser::Scope.new(compiler) end context "when accessing scope variables as $ variables" do it "looks up the value from the scope" do scope["what"] = "are belong" eval_template("all your base <%= $what %> to us").should == "all your base are belong to us" end it "get nil accessing a variable that does not exist" do eval_template("<%= $kryptonite == undef %>").should == "true" end it "get nil accessing a variable that is undef" do scope['undef_var'] = nil eval_template("<%= $undef_var == undef %>").should == "true" end it "gets shadowed variable if args are given" do scope['phantom'] = 'of the opera' eval_template_with_args("<%= $phantom == dragos %>", 'phantom' => 'dragos').should == "true" end it "can use values from the enclosing scope for defaults" do scope['phantom'] = 'of the opera' eval_template("<%- |$phantom = $phantom| -%><%= $phantom %>").should == "of the opera" end it "uses the default value if the given value is undef/nil" do eval_template_with_args("<%- |$phantom = 'inside your mind'| -%><%= $phantom %>", 'phantom' => nil).should == "inside your mind" end it "gets shadowed variable if args are given and parameters are specified" do scope['x'] = 'wrong one' eval_template_with_args("<%- |$x| -%><%= $x == correct %>", 'x' => 'correct').should == "true" end it "raises an error if required variable is not given" do scope['x'] = 'wrong one' expect do eval_template_with_args("<%-| $x |-%><%= $x == correct %>", 'y' => 'correct') end.to raise_error(/no value given for required parameters x/) end it "raises an error if too many arguments are given" do scope['x'] = 'wrong one' expect do eval_template_with_args("<%-| $x |-%><%= $x == correct %>", 'x' => 'correct', 'y' => 'surplus') end.to raise_error(/Too many arguments: 2 for 1/) end end context "when given an empty template" do it "allows the template file to be empty" do expect(eval_template("")).to eq("") end it "allows the template to have empty body after parameters" do expect(eval_template_with_args("<%-|$x|%>", 'x'=>1)).to eq("") end end context "when using typed parameters" do it "allows a passed value that matches the parameter's type" do expect(eval_template_with_args("<%-|String $x|-%><%= $x == correct %>", 'x' => 'correct')).to eq("true") end it "does not allow slurped parameters" do expect do eval_template_with_args("<%-|*$x|-%><%= $x %>", 'x' => 'incorrect') end.to raise_error(/'captures rest' - not supported in an Epp Template/) end it "raises an error when the passed value does not match the parameter's type" do expect do eval_template_with_args("<%-|Integer $x|-%><%= $x %>", 'x' => 'incorrect') end.to raise_error(/expected.*Integer.*actual.*String/m) end it "raises an error when the default value does not match the parameter's type" do expect do eval_template("<%-|Integer $x = 'nope'|-%><%= $x %>") end.to raise_error(/expected.*Integer.*actual.*String/m) end it "allows an parameter to default to undef" do expect(eval_template("<%-|Optional[Integer] $x = undef|-%><%= $x == undef %>")).to eq("true") end end # although never a problem with epp it "is not interfered with by having a variable named 'string' (#14093)" do scope['string'] = "this output should not be seen" eval_template("some text that is static").should == "some text that is static" end it "has access to a variable named 'string' (#14093)" do scope['string'] = "the string value" eval_template("string was: <%= $string %>").should == "string was: the string value" end describe 'when loading from modules' do include PuppetSpec::Files it 'an epp template is found' do modules_dir = dir_containing('modules', { 'testmodule' => { 'templates' => { 'the_x.epp' => 'The x is <%= $x %>' } }}) Puppet.override({:current_environment => (env = Puppet::Node::Environment.create(:testload, [ modules_dir ]))}, "test") do node.environment = env expect(epp_function.call(scope, 'testmodule/the_x.epp', { 'x' => '3'} )).to eql("The x is 3") end end end def eval_template_with_args(content, args_hash) file_path = tmpdir('epp_spec_content') filename = File.join(file_path, "template.epp") File.open(filename, "w+") { |f| f.write(content) } Puppet::Parser::Files.stubs(:find_template).returns(filename) epp_function.call(scope, 'template', args_hash) end def eval_template(content) file_path = tmpdir('epp_spec_content') filename = File.join(file_path, "template.epp") File.open(filename, "w+") { |f| f.write(content) } Puppet::Parser::Files.stubs(:find_template).returns(filename) epp_function.call(scope, 'template') end def epp_function() epp_func = scope.compiler.loaders.public_environment_loader.load(:function, 'epp') end end diff --git a/spec/unit/pops/loaders/dependency_loader_spec.rb b/spec/unit/pops/loaders/dependency_loader_spec.rb index cbdefe897..31d937766 100644 --- a/spec/unit/pops/loaders/dependency_loader_spec.rb +++ b/spec/unit/pops/loaders/dependency_loader_spec.rb @@ -1,61 +1,66 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' describe 'dependency loader' do include PuppetSpec::Files let(:static_loader) { Puppet::Pops::Loader::StaticLoader.new() } let(:loaders) { Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, [])) } describe 'FileBased module loader' do it 'load something in global name space raises an error' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { 'foo.rb' => 'Puppet::Functions.create_function("foo") { def foo; end; }' }}}}}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') - dep_loader = Puppet::Pops::Loader::DependencyLoader.new(static_loader, 'test-dep', [module_loader]) + loader = loader_for('testmodule', module_dir) + expect do - dep_loader.load_typed(typed_name(:function, 'testmodule::foo')).value + loader.load_typed(typed_name(:function, 'testmodule::foo')).value end.to raise_error(ArgumentError, /produced mis-matched name, expected 'testmodule::foo', got foo/) end it 'can load something in a qualified name space' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { 'foo.rb' => 'Puppet::Functions.create_function("testmodule::foo") { def foo; end; }' }}}}}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') - dep_loader = Puppet::Pops::Loader::DependencyLoader.new(static_loader, 'test-dep', [module_loader]) - function = dep_loader.load_typed(typed_name(:function, 'testmodule::foo')).value + loader = loader_for('testmodule', module_dir) + + function = loader.load_typed(typed_name(:function, 'testmodule::foo')).value expect(function.class.name).to eq('testmodule::foo') expect(function.is_a?(Puppet::Functions::Function)).to eq(true) end it 'can load something in a qualified name space more than once' do module_dir = dir_containing('testmodule', { 'lib' => { 'puppet' => { 'functions' => { 'testmodule' => { 'foo.rb' => 'Puppet::Functions.create_function("testmodule::foo") { def foo; end; }' }}}}}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') - dep_loader = Puppet::Pops::Loader::DependencyLoader.new(static_loader, 'test-dep', [module_loader]) - function = dep_loader.load_typed(typed_name(:function, 'testmodule::foo')).value + loader = loader_for('testmodule', module_dir) + + function = loader.load_typed(typed_name(:function, 'testmodule::foo')).value expect(function.class.name).to eq('testmodule::foo') expect(function.is_a?(Puppet::Functions::Function)).to eq(true) - function = dep_loader.load_typed(typed_name(:function, 'testmodule::foo')).value + function = 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 loader_for(name, dir) + module_loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(static_loader, loaders, name, dir) + Puppet::Pops::Loader::DependencyLoader.new(static_loader, 'test-dep', [module_loader]) + 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 2cd565a8c..7f65f44c1 100644 --- a/spec/unit/pops/loaders/loader_paths_spec.rb +++ b/spec/unit/pops/loaders/loader_paths_spec.rb @@ -1,55 +1,43 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' describe 'loader paths' do include PuppetSpec::Files let(:static_loader) { Puppet::Pops::Loader::StaticLoader.new() } 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' => {}, - }}}) - 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') - ]) - 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, 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')) - 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'}, - }}}) - 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(1) - expect(module_loader.path_index.size).to eq(1) - path_index = module_loader.path_index - expect(path_index).to include(File.join(module_dir, 'lib', 'puppet', 'functions', 'foo4x.rb')) - 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.module_loader_from(static_loader, unused_loaders, 'testmodule', module_dir) + + 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')) + 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'}, + }}}) + module_loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(static_loader, unused_loaders, 'testmodule', module_dir) + + effective_paths = module_loader.smart_paths.effective_paths(:function) + + expect(effective_paths.size).to eq(1) + expect(module_loader.path_index.size).to eq(1) + path_index = module_loader.path_index + expect(path_index).to include(File.join(module_dir, 'lib', 'puppet', 'functions', 'foo4x.rb')) end end diff --git a/spec/unit/pops/loaders/module_loaders_spec.rb b/spec/unit/pops/loaders/module_loaders_spec.rb index 058e765de..d15a8ee0d 100644 --- a/spec/unit/pops/loaders/module_loaders_spec.rb +++ b/spec/unit/pops/loaders/module_loaders_spec.rb @@ -1,90 +1,90 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' 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 } } } }) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') + module_loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(static_loader, loaders, 'testmodule', module_dir) 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('testmodule::foo4x') do def foo4x() 'yay' end end CODE } } } }}) - module_loader = Puppet::Pops::Loader::ModuleLoaders::FileBased.new(static_loader, loaders, 'testmodule', module_dir, 'test1') + module_loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(static_loader, loaders, 'testmodule', module_dir) 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_loader = Puppet::Pops::Loader::ModuleLoaders.module_loader_from(static_loader, loaders, 'testmodule', module_dir) 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 def typed_name(type, name) Puppet::Pops::Loader::Loader::TypedName.new(type, name) end end