diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb index 8148d71e3..c4828698f 100644 --- a/lib/puppet/parser/compiler.rb +++ b/lib/puppet/parser/compiler.rb @@ -1,624 +1,624 @@ 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 errors = node.environment.validation_errors if !errors.empty? errors.each { |e| Puppet.err(e) } if errors.size > 1 errmsg = [ "Compilation has been halted because: #{errors.first}", "For more information, see http://docs.puppetlabs.com/puppet/latest/reference/environments.html", ] raise(Puppet::Error, errmsg.join(' ')) end 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 @catalog.environment_instance = environment # Set the client's parameters into the top scope. Puppet::Util::Profiler.profile("Compile: Set node parameters", [:compiler, :set_node_params]) { set_node_parameters } Puppet::Util::Profiler.profile("Compile: Created settings scope", [:compiler, :create_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", [:compiler, :create_injector]) { injector } end Puppet::Util::Profiler.profile("Compile: Evaluated main", [:compiler, :evaluate_main]) { evaluate_main } Puppet::Util::Profiler.profile("Compile: Evaluated AST node", [:compiler, :evaluate_ast_node]) { evaluate_ast_node } Puppet::Util::Profiler.profile("Compile: Evaluated node classes", [:compiler, :evaluate_node_classes]) { evaluate_node_classes } Puppet::Util::Profiler.profile("Compile: Evaluated generators", [:compiler, :evaluate_generators]) { evaluate_generators } Puppet::Util::Profiler.profile("Compile: Finished catalog", [:compiler, :finish_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 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_with_params, @node_scope || topscope) evaluate_classes(classes_without_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 hostclasses = classes.collect do |name| scope.find_hostclass(name, :assume_fqname => fqname) or raise Puppet::Error, "Could not find class #{name} for #{node.name}" end if class_parameters resources = ensure_classes_with_parameters(scope, hostclasses, class_parameters) if !lazy_evaluate resources.each(&:evaluate) end resources else already_included, newly_included = ensure_classes_without_parameters(scope, hostclasses) if !lazy_evaluate newly_included.each(&:evaluate) end already_included + newly_included 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(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 'puppet/spi' + require 'puppet/plugins/configuration' @@binder_loaded = true end end should_be_active end private def ensure_classes_with_parameters(scope, hostclasses, parameters) hostclasses.collect do |klass| klass.ensure_in_catalog(scope, parameters[klass.name] || {}) end end def ensure_classes_without_parameters(scope, hostclasses) already_included = [] newly_included = [] hostclasses.each do |klass| class_scope = scope.class_scope(klass) if class_scope already_included << class_scope.resource else newly_included << klass.ensure_in_catalog(scope) end end [already_included, newly_included] end # 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", [:compiler, :evaluate_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", [:compiler, :evaluate_definitions]) do !unevaluated_resources.each do |resource| Puppet::Util::Profiler.profile("Evaluated resource #{resource}", [:compiler, :evaluate_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", [:compiler, :iterate_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, @node.environment) # 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 initial 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.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 settings_type = Puppet::Resource::Type.new :hostclass, "settings" environment.known_resource_types.add(settings_type) 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) env = environment Puppet.settings.each do |name, setting| next if name == :name scope[name.to_s] = env[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/plugins.rb b/lib/puppet/plugins.rb new file mode 100644 index 000000000..090a294fc --- /dev/null +++ b/lib/puppet/plugins.rb @@ -0,0 +1,10 @@ +# The Puppet Plugins module defines extension points where plugins can be configured +# to add or modify puppet's behavior. +# +# @api public +# +module Puppet::Plugins +# require 'puppet/plugins/binding_schemes' +# require 'puppet/plugins/syntax_checkers' +# require 'puppet/plugins/configuration' +end \ No newline at end of file diff --git a/lib/puppet/spi/binding_schemes.rb b/lib/puppet/plugins/binding_schemes.rb similarity index 98% rename from lib/puppet/spi/binding_schemes.rb rename to lib/puppet/plugins/binding_schemes.rb index c95a284e5..6fc18b6aa 100644 --- a/lib/puppet/spi/binding_schemes.rb +++ b/lib/puppet/plugins/binding_schemes.rb @@ -1,139 +1,140 @@ -module Puppet::Spi; module BindingSchemes +require 'puppet/plugins' +module Puppet::Plugins::BindingSchemes # The lookup **key** for the multibind containing a map from scheme name to scheme handler class for bindings schemes. # @api public SPI_BINDINGS_SCHEMES = 'puppet::binding::schemes' # The lookup **type** for the multibind containing a map from scheme name to scheme handler class for bindings schemes. # @api public - BINDINGS_SCHEMES_TYPE = 'Puppet::Spi::BindingSchemes::BindingsSchemeHandler' + BINDINGS_SCHEMES_TYPE = 'Puppet::Plugins::BindingSchemes::BindingsSchemeHandler' # BindingsSchemeHandler is a Puppet Extension Point for the purpose of extending Puppet with a # handler of a URI scheme used in the Puppet Bindings / Injector system. # The intended use is to create a class derived from this class and then register it with the # Puppet Binder. # # Creating the Extension Class # ---------------------------- # As an example, a class for getting LDAP data and transforming into bindings based on an LDAP URI scheme (such as RFC 2255, 4516) # may be authored in say a puppet module called 'exampleorg/ldap'. The name of the class should start with `Puppetx::::`, # e.g. 'Puppetx::Exampleorg::Ldap::LdapBindingsSchemeHandler" and # be located in `lib/puppetx/exampleorg/Ldap/LdapBindingsSchemeHandler.rb`. (These rules are not enforced, but it make the class # both auto-loadable, and guaranteed to not have a name that clashes with some other LdapBindingsSchemeHandler from some other # author/organization. # # The Puppet Binder will auto-load the file when it # has a binding to the class `Puppetx::Exampleorg::Ldap::LdapBindingsSchemeHandler' # The Ruby Module `Puppetx` is created by Puppet, the remaining modules should be created by the loaded logic - e.g.: # # @example Defining an LdapBindingsSchemeHandler # module Puppetx::Exampleorg # module Ldap # class LdapBindingsSchemeHandler < Puppetx::Puppetlabs::BindingsSchemeHandler # # implement the methods # end # end # end # # # The expand_included method # -------------------------- # This method is given a URI (as entred by a user in a bindings configuration) and the handler's first task is to # perform checking, transformation, and possible expansion into multiple URIs for loading. The result is always an array # of URIs. This method allows users to enter wild-cards, or to represent something symbolic that is transformed into one or # more "real URIs" to load. (It is allowed to change scheme!). # If the "optional" feature is supported, the handler should not include the URI in the result unless it will be able to produce # bindings for the given URI (as an option it may produce an empty set of bindings). # # The expand_excluded method # --------------------------- # This method is given an URI (as entered by the user in a bindings configuration), and it is the handler's second task # to perform checking, transformation, and possible expansion into multiple URIs that should not be loaded. The result is always # an array of URIs. The user may be allowed to enter wild-cards etc. The URIs produced by this method should have the same syntax # as those produced by {#expand_included} since they are excluded by comparison. # # The contributed_bindings method # ------------------------------- # As the last step, the handler is being called once per URI that was included, and not later excluded to produce the # contributed bindings. It is given three arguments, uri (the uri to load), scope (to provide access to the rest of the # environment), and an acceptor (of issues), on which issues can be recorded. # # Reporting Errors/Issues # ----------------------- # Issues are reported by calling the given composer's acceptor, which takes a severity (e.g. `:error`, # `:warning`, or `:ignore`), an {Puppet::Pops::Issues::Issue Issue} instance, and a {Puppet::Pops::Adapters::SourcePosAdapter # SourcePosAdapter} (which describes details about linenumber, position, and length of the problem area). If the scheme is # not based on file, line, pos - nil can be passed. The URI itself can be passed as file. # # @example Reporting an issue # # create an issue with a symbolic name (that can serve as a reference to more details about the problem), # # make the name unique # issue = Puppet::Pops::Issues::issue(:EXAMPLEORG_LDAP_ILLEGAL_URI) { "The URI is not a valid Ldap URI" } # source_pos = nil # # # report it # composer.acceptor.accept(Puppet::Pops::Validation::Diagnostic.new(:error, issue, uri.to_s, source_pos, {})) # # Instead of reporting issues, an exception can be raised. # # @abstract # @api public # class BindingsSchemeHandler # Produces the bindings contributed to the binding system based on the given URI. # @param uri [URI] the URI to load bindings from # @param scope [Puppet::Pops::Parser::Scope] access to scope and the rest of the environment # @param composer [Puppet::Pops::Binder::BindingsComposer] a composer giving access to modules by name, and a diagnostics acceptor # @return [Puppet::Pops::Binder::Bindings::ContributedBindings] the bindings to contribute, most conveniently # created by calling {Puppet::Pops::Binder::BindingsFactory.contributed_bindings}. # @api public # def contributed_bindings(uri, scope, composer) raise NotImplementedError, "The BindingsProviderScheme for uri: '#{uri}' must implement 'contributed_bindings'" end # Expands the given URI for the purpose of including the bindings it refers to. The input may contain # wild-cards (if supported by this handler), and it is this methods responsibility to transform such into # real loadable URIs. # # A scheme handler that does not support optionality, or wildcards should simply return the given URI # in an Array. # # @param uri [URI] the uri for which bindings are to be produced. # @param composer [Puppet::Pops::Binder::BindingsComposer] a composer giving access to modules by name, and a diagnostics acceptor # @return [Array] the transformed, and possibly expanded set of URIs to include. # @api public # def expand_included(uri, composer) [uri] end # Expands the given URI for the purpose of excluding the bindings it refers to. The input may contain # wild-cards (if supported by this handler), and it is this methods responsibility to transform such into # real loadable URIs (that match those produced by {#expand_included} that should be excluded from the result. # # A scheme handler that does not support optionality, or wildcards should simply return the given URI # in an Array. # # @param uri [URI] the uri for which bindings are to be produced. # @param composer [Puppet::Pops::Binder::BindingsComposer] a composer giving access to modules by name, and a diagnostics acceptor # @return [Array] the transformed, and possibly expanded set of URIs to include- # @api public # def expand_excluded(uri, composer) [uri] end # Returns whether the uri is optional or not. A scheme handler does not have to use this method # to determine optionality, but if it supports such a feature, and there is no technical problem in supporting # it this way, it should be done the same (or at least similar) way across all scheme handlers. # # This method interprets a URI ending with `?` or has query that is '?optional' as optional. # # @return [Boolean] whether the uri is an optional reference or not. def is_optional?(uri) (query = uri.query) && query == '' || query == 'optional' end end -end; end \ No newline at end of file +end diff --git a/lib/puppet/spi/configuration.rb b/lib/puppet/plugins/configuration.rb similarity index 80% rename from lib/puppet/spi/configuration.rb rename to lib/puppet/plugins/configuration.rb index 009ba447a..71d623f88 100644 --- a/lib/puppet/spi/configuration.rb +++ b/lib/puppet/plugins/configuration.rb @@ -1,67 +1,67 @@ -# Configures the Puppet Service Programming Inteterface SPI, by registering extension points +# Configures the Puppet Plugins, by registering extension points # and default implementations. # # See the respective configured services for more information. # # @api private # +require 'puppet/plugins' -module Puppet::Spi::Configuration +module Puppet::Plugins::Configuration # TODO: This should always be true in Puppet 4.0 (the way it is done now does not allow toggling) return unless ::Puppet[:binder] || ::Puppet[:parser] == 'future' - require 'puppet/spi/binding_schemes' - require 'puppet/spi/syntax_checkers' + require 'puppet/plugins/binding_schemes' + require 'puppet/plugins/syntax_checkers' # Extension-points are registered here: # # - If in a Ruby submodule it is best to create it here # - The class does not have to be required; it will be auto required when the binder # needs it. # - If the extension is a multibind, it can be registered here; either with a required # class or a class reference in string form. - checkers_name = Puppet::Spi::SyntaxCheckers::SPI_SYNTAX_CHECKERS - checkers_type = Puppet::Spi::SyntaxCheckers::SYNTAX_CHECKERS_TYPE + checkers_name = Puppet::Plugins::SyntaxCheckers::SPI_SYNTAX_CHECKERS + checkers_type = Puppet::Plugins::SyntaxCheckers::SYNTAX_CHECKERS_TYPE - schemes_name = Puppet::Spi::BindingSchemes::SPI_BINDINGS_SCHEMES - schemes_type = Puppet::Spi::BindingSchemes::BINDINGS_SCHEMES_TYPE + schemes_name = Puppet::Plugins::BindingSchemes::SPI_BINDINGS_SCHEMES + schemes_type = Puppet::Plugins::BindingSchemes::BINDINGS_SCHEMES_TYPE # Register extension points # ------------------------- system_bindings = ::Puppet::Pops::Binder::SystemBindings extensions = system_bindings.extensions() extensions.multibind(checkers_name).name(checkers_name).hash_of(checkers_type) extensions.multibind(schemes_name).name(schemes_name).hash_of(schemes_type) # Register injector boot bindings # ------------------------------- boot_bindings = system_bindings.injector_boot_bindings() # Register the default bindings scheme handlers { 'module' => 'ModuleScheme', 'confdir' => 'ConfdirScheme', }.each do |scheme, class_name| boot_bindings \ .bind.name(scheme) \ .instance_of(schemes_type) \ .in_multibind(schemes_name) \ .to_instance("Puppet::Pops::Binder::SchemeHandler::#{class_name}") end # Default extensions delivered in Puppet Core are included here # ------------------------------------------------------------- # Classes in this name-space are lazily loaded as they may be overridden and/or never used # (Lazy loading is done by binding to the name of a class instead of a Class instance). # Register extensions # ------------------- bindings = system_bindings.default_bindings() bindings.bind do name('json') instance_of(checkers_type) in_multibind(checkers_name) to_instance('Puppet::SyntaxCheckers::Json') end - -end \ No newline at end of file +end diff --git a/lib/puppet/spi/syntax_checkers.rb b/lib/puppet/plugins/syntax_checkers.rb similarity index 97% rename from lib/puppet/spi/syntax_checkers.rb rename to lib/puppet/plugins/syntax_checkers.rb index 595b0591d..e1b5ee5cb 100644 --- a/lib/puppet/spi/syntax_checkers.rb +++ b/lib/puppet/plugins/syntax_checkers.rb @@ -1,102 +1,103 @@ -module Puppet::Spi; module SyntaxCheckers +require 'puppet/plugins' +module Puppet::Plugins::SyntaxCheckers # The lookup **key** for the multibind containing syntax checkers used to syntax check embedded string in non # puppet DSL syntax. # @api public SPI_SYNTAX_CHECKERS = 'puppet::syntaxcheckers' # The lookup **type** for the multibind containing syntax checkers used to syntax check embedded string in non # puppet DSL syntax. # @api public - SYNTAX_CHECKERS_TYPE = 'Puppet::Spi::SyntaxCheckers::SyntaxChecker' + SYNTAX_CHECKERS_TYPE = 'Puppet::Plugins::SyntaxCheckers::SyntaxChecker' # SyntaxChecker is a Puppet Extension Point for the purpose of extending Puppet with syntax checkers. # The intended use is to create a class derived from this class and then register it with the # Puppet Binder. # # Creating the Extension Class # ---------------------------- # As an example, a class for checking custom xml (aware of some custom schemes) may be authored in # say a puppet module called 'exampleorg/xmldata'. The name of the class should start with `Puppetx::::`, # e.g. 'Puppetx::Exampleorg::XmlData::XmlChecker" and # be located in `lib/puppetx/exampleorg/xml_data/xml_checker.rb`. The Puppet Binder will auto-load this file when it # has a binding to the class `Puppetx::Exampleorg::XmlData::XmlChecker' # The Ruby Module `Puppetx` is created by Puppet, the remaining modules should be created by the loaded logic - e.g.: # # @example Defining an XmlChecker # module Puppetx::Exampleorg # module XmlData # class XmlChecker < Puppetx::Puppetlabs::SyntaxCheckers::SyntaxChecker # def check(text, syntax_identifier, acceptor, location_hash) # # do the checking # end # end # end # end # # Implementing the check method # ----------------------------- # The implementation of the {#check} method should naturally perform syntax checking of the given text/string and # produce found issues on the given `acceptor`. These can be warnings or errors. The method should return `false` if # any warnings or errors were produced (it is up to the caller to check for error/warning conditions and report them # to the user). # # Issues are reported by calling the given `acceptor`, which takes a severity (e.g. `:error`, # or `:warning), an {Puppet::Pops::Issues::Issue} instance, and a {Puppet::Pops::Adapters::SourcePosAdapter} # (which describes details about linenumber, position, and length of the problem area). Note that the # `location_info` given to the check method holds information about the location of the string in its *container* # (e.g. the source position of a Heredoc); this information can be used if more detailed information is not # available, or combined if there are more details (relative to the start of the checked string). # # @example Reporting an issue # # create an issue with a symbolic name (that can serve as a reference to more details about the problem), # # make the name unique # issue = Puppet::Pops::Issues::issue(:EXAMPLEORG_XMLDATA_ILLEGAL_XML) { "syntax error found in xml text" } # source_pos = Puppet::Pops::Adapters::SourcePosAdapter.new() # source_pos.line = info[:line] # use this if there is no detail from the used parser # source_pos.pos = info[:pos] # use this pos if there is no detail from used parser # # # report it # acceptor.accept(Puppet::Pops::Validation::Diagnostic.new(:error, issue, info[:file], source_pos, {})) # # There is usually a cap on the number of errors/warnings that are presented to the user, this is handled by the # reporting logic, but care should be taken to not generate too many as the issues are kept in memory until # the checker returns. The acceptor may set a limit and simply ignore issues past a certain (high) number of reported # issues (this number is typically higher than the cap on issues reported to the user). # # The `syntax_identifier` # ----------------------- # The extension makes use of a syntax identifier written in mime-style. This identifier can be something simple # as 'xml', or 'json', but can also consist of several segments joined with '+' where the most specific syntax variant # is placed first. When searching for a syntax checker; say for JSON having some special traits, say 'userdata', the # author of the text may indicate this as the text having the syntax "userdata+json" - when a checker is looked up it is # first checked if there is a checker for "userdata+json", if none is found, a lookup is made for "json" (since the text # must at least be valid json). The given identifier is passed to the checker (to allow the same checker to check for # several dialects/specializations). # # Use in Puppet DSL # ----------------- # The Puppet DSL Heredoc support and Puppet Templates makes use of the syntax checker extension. A user of a # heredoc can specify the syntax in the heredoc tag, e.g.`@(END:userdata+json)`. # # # @abstract # class SyntaxChecker # Checks the text for syntax issues and reports them to the given acceptor. # This implementation is abstract, it raises {NotImplementedError} since a subclass should have implemented the # method. # # @param text [String] The text to check # @param syntax_identifier [String] The syntax identifier in mime style (e.g. 'json', 'json-patch+json', 'xml', 'myapp+xml' # @option location_info [String] :file The filename where the string originates # @option location_info [Integer] :line The line number identifying the location where the string is being used/checked # @option location_info [Integer] :position The position on the line identifying the location where the string is being used/checked # @return [Boolean] Whether the checked string had issues (warnings and/or errors) or not. # @api public # def check(text, syntax_identifier, acceptor, location_info) raise NotImplementedError, "The class #{self.class.name} should have implemented the method check()" end end -end; end +end diff --git a/lib/puppet/pops/binder/bindings_composer.rb b/lib/puppet/pops/binder/bindings_composer.rb index 7c849d7f7..815b38443 100644 --- a/lib/puppet/pops/binder/bindings_composer.rb +++ b/lib/puppet/pops/binder/bindings_composer.rb @@ -1,177 +1,177 @@ # The BindingsComposer handles composition of multiple bindings sources # It is directed by a {Puppet::Pops::Binder::Config::BinderConfig BinderConfig} that indicates how # the final composition should be layered, and what should be included/excluded in each layer # # The bindings composer is intended to be used once per environment as the compiler starts its work. # # TODO: Possibly support envdir: scheme / relative to environment root (== same as confdir if there is only one environment). # This is probably easier to do after ENC changes described in ARM-8 have been implemented. # TODO: If same config is loaded in a higher layer, skip it in the lower (since it is meaningless to load it again with lower # precedence. (Optimization, or possibly an error, should produce a warning). # -require 'puppet/spi/binding_schemes' +require 'puppet/plugins/binding_schemes' class Puppet::Pops::Binder::BindingsComposer # The BindingsConfig instance holding the read and parsed, but not evaluated configuration # @api public # attr_reader :config # map of scheme name to handler # @api private attr_reader :scheme_handlers # @return [Hash{String => Puppet::Module}] map of module name to module instance # @api private attr_reader :name_to_module # @api private attr_reader :confdir # @api private attr_reader :diagnostics # Container of all warnings and errors produced while initializing and loading bindings # # @api public attr_reader :acceptor # @api public def initialize() @acceptor = Puppet::Pops::Validation::Acceptor.new() @diagnostics = Puppet::Pops::Binder::Config::DiagnosticProducer.new(acceptor) @config = Puppet::Pops::Binder::Config::BinderConfig.new(@diagnostics) if acceptor.errors? Puppet::Pops::IssueReporter.assert_and_report(acceptor, :message => 'Binding Composer: error while reading config.') raise Puppet::DevError.new("Internal Error: IssueReporter did not raise exception for errors in bindings config.") end end # Configures and creates the boot injector. # The read config may optionally contain mapping of bindings scheme handler name to handler class, and # mapping of biera2 backend symbolic name to backend class. # If present, these are turned into bindings in the category 'extension' (which is only used in the boot injector) which # has higher precedence than 'default'. This is done to allow users to override the default bindings for # schemes and backends. # @param scope [Puppet::Parser:Scope] the scope (used to find compiler and injector for the environment) # @api private # def configure_and_create_injector(scope) # create the injector (which will pick up the bindings registered above) @scheme_handlers = SchemeHandlerHelper.new(scope) # get extensions from the config # ------------------------------ scheme_extensions = @config.scheme_extensions # Define a named bindings that are known by the SystemBindings boot_bindings = Puppet::Pops::Binder::BindingsFactory.named_bindings(Puppet::Pops::Binder::SystemBindings::ENVIRONMENT_BOOT_BINDINGS_NAME) do scheme_extensions.each_pair do |scheme, class_name| # turn each scheme => class_name into a binding (contribute to the buildings-schemes multibind). # do this in category 'extensions' to allow them to override the 'default' bind do name(scheme) - instance_of(Puppet::Spi::BindingSchemes::BINDINGS_SCHEMES_TYPE) - in_multibind(Puppet::Spi::BindingSchemes::SPI_BINDINGS_SCHEMES) + instance_of(Puppet::Plugins::BindingSchemes::BINDINGS_SCHEMES_TYPE) + in_multibind(Puppet::Plugins::BindingSchemes::SPI_BINDINGS_SCHEMES) to_instance(class_name) end end end @injector = scope.compiler.create_boot_injector(boot_bindings.model) end # @return [Puppet::Pops::Binder::Bindings::LayeredBindings] def compose(scope) # The boot injector is used to lookup scheme-handlers configure_and_create_injector(scope) # get all existing modules and their root path @name_to_module = {} scope.environment.modules.each {|mod| name_to_module[mod.name] = mod } # setup the confdir @confdir = Puppet.settings[:confdir] factory = Puppet::Pops::Binder::BindingsFactory contributions = [] configured_layers = @config.layering_config.collect do | layer_config | # get contributions contribs = configure_layer(layer_config, scope, diagnostics) # collect the contributions separately for later checking of category precedence contributions.concat(contribs) # create a named layer with all the bindings for this layer factory.named_layer(layer_config['name'], *contribs.collect {|c| c.bindings }.flatten) end # Add the two system layers; the final - highest ("can not be overridden" layer), and the lowest # Everything here can be overridden 'default' layer. # configured_layers.insert(0, Puppet::Pops::Binder::SystemBindings.final_contribution) configured_layers.insert(-1, Puppet::Pops::Binder::SystemBindings.default_contribution) # and finally... create the resulting structure factory.layered_bindings(*configured_layers) end private def configure_layer(layer_description, scope, diagnostics) name = layer_description['name'] # compute effective set of uris to load (and get rid of any duplicates in the process included_uris = array_of_uris(layer_description['include']) excluded_uris = array_of_uris(layer_description['exclude']) effective_uris = Set.new(expand_included_uris(included_uris)).subtract(Set.new(expand_excluded_uris(excluded_uris))) # Each URI should result in a ContributedBindings effective_uris.collect { |uri| scheme_handlers[uri.scheme].contributed_bindings(uri, scope, self) } end def array_of_uris(descriptions) return [] unless descriptions descriptions = [descriptions] unless descriptions.is_a?(Array) descriptions.collect {|d| URI.parse(d) } end def expand_included_uris(uris) result = [] uris.each do |uri| unless handler = scheme_handlers[uri.scheme] raise ArgumentError, "Unknown bindings provider scheme: '#{uri.scheme}'" end result.concat(handler.expand_included(uri, self)) end result end def expand_excluded_uris(uris) result = [] uris.each do |uri| unless handler = scheme_handlers[uri.scheme] raise ArgumentError, "Unknown bindings provider scheme: '#{uri.scheme}'" end result.concat(handler.expand_excluded(uri, self)) end result end class SchemeHandlerHelper T = Puppet::Pops::Types::TypeFactory - HASH_OF_HANDLER = T.hash_of(T.type_of(Puppet::Spi::BindingSchemes::BINDINGS_SCHEMES_TYPE)) + HASH_OF_HANDLER = T.hash_of(T.type_of(Puppet::Plugins::BindingSchemes::BINDINGS_SCHEMES_TYPE)) def initialize(scope) @scope = scope @cache = nil end def [] (scheme) load_schemes unless @cache @cache[scheme] end def load_schemes - @cache = @scope.compiler.boot_injector.lookup(@scope, HASH_OF_HANDLER, Puppet::Spi::BindingSchemes::SPI_BINDINGS_SCHEMES) || {} + @cache = @scope.compiler.boot_injector.lookup(@scope, HASH_OF_HANDLER, Puppet::Plugins::BindingSchemes::SPI_BINDINGS_SCHEMES) || {} end end end diff --git a/lib/puppet/pops/binder/scheme_handler/symbolic_scheme.rb b/lib/puppet/pops/binder/scheme_handler/symbolic_scheme.rb index 752bc8179..625b9a290 100644 --- a/lib/puppet/pops/binder/scheme_handler/symbolic_scheme.rb +++ b/lib/puppet/pops/binder/scheme_handler/symbolic_scheme.rb @@ -1,53 +1,53 @@ # Abstract base class for schemes based on symbolic names of bindings. # This class helps resolve symbolic names by computing a path from a fully qualified name (fqn). # There are also helper methods do determine if the symbolic name contains a wild-card ('*') in the first # portion of the fqn (with the convention that '*' means 'any module'). # # @abstract # @api public # -class Puppet::Pops::Binder::SchemeHandler::SymbolicScheme < Puppet::Spi::BindingSchemes::BindingsSchemeHandler +class Puppet::Pops::Binder::SchemeHandler::SymbolicScheme < Puppet::Plugins::BindingSchemes::BindingsSchemeHandler # Shared implementation for module: and confdir: since the distinction is only in checks if a symbolic name # exists as a loadable file or not. Once this method is called it is assumed that the name is relative # and that it should exist relative to some loadable ruby location. # # TODO: this needs to be changed once ARM-8 Puppet DSL concrete syntax is also supported. # @api public # def contributed_bindings(uri, scope, composer) fqn = fqn_from_path(uri)[1] bindings = Puppet::Pops::Binder::BindingsLoader.provide(scope, fqn) raise ArgumentError, "Cannot load bindings '#{uri}' - no bindings found." unless bindings # Must clone as the rest mutates the model cloned_bindings = Marshal.load(Marshal.dump(bindings)) Puppet::Pops::Binder::BindingsFactory.contributed_bindings(fqn, cloned_bindings) end # @api private def fqn_from_path(uri) split_path = uri.path.split('/') if split_path.size > 1 && split_path[-1].empty? split_path.delete_at(-1) end fqn = split_path[ 1 ] raise ArgumentError, "Module scheme binding reference has no name." unless fqn split_name = fqn.split('::') # drop leading '::' split_name.shift if split_name[0] && split_name[0].empty? [split_name, split_name.join('::')] end # True if super thinks it is optional or if it contains a wildcard. # @return [Boolean] true if the uri represents an optional set of bindings. # @api public def is_optional?(uri) super(uri) || has_wildcard?(uri) end # @api private def has_wildcard?(uri) (path = uri.path) && path.split('/')[1].start_with?('*::') end end diff --git a/lib/puppet/pops/evaluator/external_syntax_support.rb b/lib/puppet/pops/evaluator/external_syntax_support.rb index 1b51d7423..68e7f688f 100644 --- a/lib/puppet/pops/evaluator/external_syntax_support.rb +++ b/lib/puppet/pops/evaluator/external_syntax_support.rb @@ -1,52 +1,52 @@ # This module is an integral part of the evaluator. It deals with the concern of validating # external syntax in text produced by heredoc and templates. # -require 'puppet/spi/syntax_checkers' +require 'puppet/plugins/syntax_checkers' module Puppet::Pops::Evaluator::ExternalSyntaxSupport # TODO: This can be simplified if the Factory directly supported hash_of/type_of TYPES = Puppet::Pops::Types::TypeFactory - SERVICE_TYPE = Puppet::Spi::SyntaxCheckers::SYNTAX_CHECKERS_TYPE - SERVICE_NAME = Puppet::Spi::SyntaxCheckers::SPI_SYNTAX_CHECKERS + SERVICE_TYPE = Puppet::Plugins::SyntaxCheckers::SYNTAX_CHECKERS_TYPE + SERVICE_NAME = Puppet::Plugins::SyntaxCheckers::SPI_SYNTAX_CHECKERS def assert_external_syntax(scope, result, syntax, reference_expr) @@HASH_OF_SYNTAX_CHECKERS ||= TYPES.hash_of(TYPES.type_of(SERVICE_TYPE)) # ignore 'unspecified syntax' return if syntax.nil? || syntax == '' checker = checker_for_syntax(scope, syntax) # ignore syntax with no matching checker return unless checker # Call checker and give it the location information from the expression # (as opposed to where the heredoc tag is (somewhere on the line above)). acceptor = Puppet::Pops::Validation::Acceptor.new() source_pos = find_closest_positioned(reference_expr) checker.check(result, syntax, acceptor, source_pos) if acceptor.error_count > 0 checker_message = "Invalid produced text having syntax: '#{syntax}'." Puppet::Pops::IssueReporter.assert_and_report(acceptor, :message => checker_message) raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception" end end # Finds the most significant checker for the given syntax (most significant is to the right). # Returns nil if there is no registered checker. # def checker_for_syntax(scope, syntax) checkers_hash = scope.compiler.injector.lookup(scope, @@HASH_OF_SYNTAX_CHECKERS, SERVICE_NAME) || {} checkers_hash[lookup_keys_for_syntax(syntax).find {|x| checkers_hash[x] }] end # Returns an array of possible syntax names def lookup_keys_for_syntax(syntax) segments = syntax.split(/\+/) result = [] begin result << segments.join("+") segments.shift end until segments.empty? result end end diff --git a/lib/puppet/spi.rb b/lib/puppet/spi.rb deleted file mode 100644 index 50088cce6..000000000 --- a/lib/puppet/spi.rb +++ /dev/null @@ -1,10 +0,0 @@ -# The Puppet Service Programming Interface Module. -# -# @api public -# - -module Puppet::Spi - require 'puppet/spi/binding_schemes' - require 'puppet/spi/syntax_checkers' - require 'puppet/spi/configuration' -end \ No newline at end of file diff --git a/lib/puppet/syntax_checkers/json.rb b/lib/puppet/syntax_checkers/json.rb index 81b772710..a2291866d 100644 --- a/lib/puppet/syntax_checkers/json.rb +++ b/lib/puppet/syntax_checkers/json.rb @@ -1,37 +1,37 @@ # A syntax checker for JSON. # @api public require 'puppet/syntax_checkers' -class Puppet::SyntaxCheckers::Json < Puppet::Spi::SyntaxCheckers::SyntaxChecker +class Puppet::SyntaxCheckers::Json < Puppet::Plugins::SyntaxCheckers::SyntaxChecker # Checks the text for JSON syntax issues and reports them to the given acceptor. # This implementation is abstract, it raises {NotImplementedError} since a subclass should have implemented the # method. # # Error messages from the checker are capped at 100 chars from the source text. # # @param text [String] The text to check # @param syntax [String] The syntax identifier in mime style (e.g. 'json', 'json-patch+json', 'xml', 'myapp+xml' # @param acceptor [#accept] A Diagnostic acceptor # @param source_pos [Puppet::Pops::Adapters::SourcePosAdapter] A source pos adapter with location information # @api public # def check(text, syntax, acceptor, source_pos) raise ArgumentError.new("Json syntax checker: the text to check must be a String.") unless text.is_a?(String) raise ArgumentError.new("Json syntax checker: the syntax identifier must be a String, e.g. json, data+json") unless syntax.is_a?(String) raise ArgumentError.new("Json syntax checker: invalid Acceptor, got: '#{acceptor.class.name}'.") unless acceptor.is_a?(Puppet::Pops::Validation::Acceptor) #raise ArgumentError.new("Json syntax checker: location_info must be a Hash") unless location_info.is_a?(Hash) begin JSON.parse(text) rescue => e # Cap the message to 100 chars and replace newlines msg = "JSON syntax checker: Cannot parse invalid JSON string. \"#{e.message().slice(0,100).gsub(/\r?\n/, "\\n")}\"" # TODO: improve the pops API to allow simpler diagnostic creation while still maintaining capabilities # and the issue code. (In this case especially, where there is only a single error message being issued). # issue = Puppet::Pops::Issues::issue(:ILLEGAL_JSON) { msg } acceptor.accept(Puppet::Pops::Validation::Diagnostic.new(:error, issue, source_pos.locator.file, source_pos, {})) end end end diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppetx/awesome2/echo_scheme_handler.rb b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppetx/awesome2/echo_scheme_handler.rb index bfcd94d5d..33159fcaa 100644 --- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppetx/awesome2/echo_scheme_handler.rb +++ b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppetx/awesome2/echo_scheme_handler.rb @@ -1,18 +1,18 @@ -require 'puppet/spi/binding_schemes' +require 'puppet/plugins/binding_schemes' module Puppetx module Awesome2 # A binding scheme that echos its path # 'echo:/quick/brown/fox' becomes key '::quick::brown::fox' => 'echo: quick brown fox'. # (silly class for testing loading of extension) # - class EchoSchemeHandler < Puppet::Spi::BindingSchemes::BindingsSchemeHandler + class EchoSchemeHandler < Puppet::Plugins::BindingSchemes::BindingsSchemeHandler def contributed_bindings(uri, scope, composer) factory = ::Puppet::Pops::Binder::BindingsFactory bindings = factory.named_bindings("echo") bindings.bind.name(uri.path.gsub(/\//, '::')).to("echo: #{uri.path.gsub(/\//, ' ').strip!}") result = factory.contributed_bindings("echo", bindings.model) ### , nil) end end end end \ No newline at end of file diff --git a/spec/unit/application_spec.rb b/spec/unit/application_spec.rb index 4b214d5da..1a5420f6e 100755 --- a/spec/unit/application_spec.rb +++ b/spec/unit/application_spec.rb @@ -1,628 +1,628 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/application' require 'puppet' require 'getoptlong' require 'timeout' describe Puppet::Application do before(:each) do @app = Class.new(Puppet::Application).new @appclass = @app.class @app.stubs(:name).returns("test_app") end describe "application commandline" do it "should not pick up changes to the array of arguments" do args = %w{subcommand --arg} command_line = Puppet::Util::CommandLine.new('puppet', args) app = Puppet::Application.new(command_line) args[0] = 'different_subcommand' args[1] = '--other-arg' app.command_line.subcommand_name.should == 'subcommand' app.command_line.args.should == ['--arg'] end end describe "application defaults" do it "should fail if required app default values are missing" do @app.stubs(:app_defaults).returns({ :foo => 'bar' }) Puppet.expects(:err).with(regexp_matches(/missing required app default setting/)) expect { @app.run - }.to exit_with 1 + }.to exit_with(1) end end describe "finding" do before do @klass = Puppet::Application @klass.stubs(:puts) end it "should find classes in the namespace" do @klass.find("Agent").should == @klass::Agent end it "should not find classes outside the namespace" do expect { @klass.find("String") }.to raise_error(LoadError) end it "should error if it can't find a class" do Puppet.expects(:err).with do |value| value =~ /Unable to find application 'ThisShallNeverEverEverExist'/ and value =~ /puppet\/application\/thisshallneverevereverexist/ and value =~ /no such file to load|cannot load such file/ end expect { @klass.find("ThisShallNeverEverEverExist") }.to raise_error(LoadError) end it "#12114: should prevent File namespace collisions" do # have to require the file face once, then the second time around it would fail @klass.find("File").should == Puppet::Application::File @klass.find("File").should == Puppet::Application::File end end describe "#available_application_names" do it 'should be able to find available application names' do apps = %w{describe filebucket kick queue resource agent cert apply doc master} Puppet::Util::Autoload.expects(:files_to_load).returns(apps) Puppet::Application.available_application_names.should =~ apps end it 'should find applications from multiple paths' do Puppet::Util::Autoload.expects(:files_to_load).with('puppet/application').returns(%w{ /a/foo.rb /b/bar.rb }) Puppet::Application.available_application_names.should =~ %w{ foo bar } end it 'should return unique application names' do Puppet::Util::Autoload.expects(:files_to_load).with('puppet/application').returns(%w{ /a/foo.rb /b/foo.rb }) Puppet::Application.available_application_names.should == %w{ foo } end end describe ".run_mode" do it "should default to user" do @appclass.run_mode.name.should == :user end it "should set and get a value" do @appclass.run_mode :agent @appclass.run_mode.name.should == :agent end end # These tests may look a little weird and repetative in its current state; # it used to illustrate several ways that the run_mode could be changed # at run time; there are fewer ways now, but it would still be nice to # get to a point where it was entirely impossible. describe "when dealing with run_mode" do class TestApp < Puppet::Application run_mode :master def run_command # no-op end end it "should sadly and frighteningly allow run_mode to change at runtime via #initialize_app_defaults" do Puppet.features.stubs(:syslog?).returns(true) app = TestApp.new app.initialize_app_defaults Puppet.run_mode.should be_master end it "should sadly and frighteningly allow run_mode to change at runtime via #run" do app = TestApp.new app.run app.class.run_mode.name.should == :master Puppet.run_mode.should be_master end end it "should explode when an invalid run mode is set at runtime, for great victory" do expect { class InvalidRunModeTestApp < Puppet::Application run_mode :abracadabra def run_command # no-op end end }.to raise_error end it "should have a run entry-point" do @app.should respond_to(:run) end it "should have a read accessor to options" do @app.should respond_to(:options) end it "should include a default setup method" do @app.should respond_to(:setup) end it "should include a default preinit method" do @app.should respond_to(:preinit) end it "should include a default run_command method" do @app.should respond_to(:run_command) end it "should invoke main as the default" do @app.expects( :main ) @app.run_command end describe 'when invoking clear!' do before :each do Puppet::Application.run_status = :stop_requested Puppet::Application.clear! end it 'should have nil run_status' do Puppet::Application.run_status.should be_nil end it 'should return false for restart_requested?' do Puppet::Application.restart_requested?.should be_false end it 'should return false for stop_requested?' do Puppet::Application.stop_requested?.should be_false end it 'should return false for interrupted?' do Puppet::Application.interrupted?.should be_false end it 'should return true for clear?' do Puppet::Application.clear?.should be_true end end describe 'after invoking stop!' do before :each do Puppet::Application.run_status = nil Puppet::Application.stop! end after :each do Puppet::Application.run_status = nil end it 'should have run_status of :stop_requested' do Puppet::Application.run_status.should == :stop_requested end it 'should return true for stop_requested?' do Puppet::Application.stop_requested?.should be_true end it 'should return false for restart_requested?' do Puppet::Application.restart_requested?.should be_false end it 'should return true for interrupted?' do Puppet::Application.interrupted?.should be_true end it 'should return false for clear?' do Puppet::Application.clear?.should be_false end end describe 'when invoking restart!' do before :each do Puppet::Application.run_status = nil Puppet::Application.restart! end after :each do Puppet::Application.run_status = nil end it 'should have run_status of :restart_requested' do Puppet::Application.run_status.should == :restart_requested end it 'should return true for restart_requested?' do Puppet::Application.restart_requested?.should be_true end it 'should return false for stop_requested?' do Puppet::Application.stop_requested?.should be_false end it 'should return true for interrupted?' do Puppet::Application.interrupted?.should be_true end it 'should return false for clear?' do Puppet::Application.clear?.should be_false end end describe 'when performing a controlled_run' do it 'should not execute block if not :clear?' do Puppet::Application.run_status = :stop_requested target = mock 'target' target.expects(:some_method).never Puppet::Application.controlled_run do target.some_method end end it 'should execute block if :clear?' do Puppet::Application.run_status = nil target = mock 'target' target.expects(:some_method).once Puppet::Application.controlled_run do target.some_method end end describe 'on POSIX systems', :if => Puppet.features.posix? do it 'should signal process with HUP after block if restart requested during block execution' do Timeout::timeout(3) do # if the signal doesn't fire, this causes failure. has_run = false old_handler = trap('HUP') { has_run = true } begin Puppet::Application.controlled_run do Puppet::Application.run_status = :restart_requested end # Ruby 1.9 uses a separate OS level thread to run the signal # handler, so we have to poll - ideally, in a way that will kick # the OS into running other threads - for a while. # # You can't just use the Ruby Thread yield thing either, because # that is just an OS hint, and Linux ... doesn't take that # seriously. --daniel 2012-03-22 sleep 0.001 while not has_run ensure trap('HUP', old_handler) end end end end after :each do Puppet::Application.run_status = nil end end describe "when parsing command-line options" do before :each do @app.command_line.stubs(:args).returns([]) Puppet.settings.stubs(:optparse_addargs).returns([]) end it "should pass the banner to the option parser" do option_parser = stub "option parser" option_parser.stubs(:on) option_parser.stubs(:parse!) @app.class.instance_eval do banner "banner" end OptionParser.expects(:new).with("banner").returns(option_parser) @app.parse_options end it "should ask OptionParser to parse the command-line argument" do @app.command_line.stubs(:args).returns(%w{ fake args }) OptionParser.any_instance.expects(:parse!).with(%w{ fake args }) @app.parse_options end describe "when using --help" do it "should call exit" do @app.stubs(:puts) expect { @app.handle_help(nil) }.to exit_with 0 end end describe "when using --version" do it "should declare a version option" do @app.should respond_to(:handle_version) end it "should exit after printing the version" do @app.stubs(:puts) expect { @app.handle_version(nil) }.to exit_with 0 end end describe "when dealing with an argument not declared directly by the application" do it "should pass it to handle_unknown if this method exists" do Puppet.settings.stubs(:optparse_addargs).returns([["--not-handled", :REQUIRED]]) @app.expects(:handle_unknown).with("--not-handled", "value").returns(true) @app.command_line.stubs(:args).returns(["--not-handled", "value"]) @app.parse_options end it "should transform boolean option to normal form for Puppet.settings" do @app.expects(:handle_unknown).with("--option", true) @app.send(:handlearg, "--[no-]option", true) end it "should transform boolean option to no- form for Puppet.settings" do @app.expects(:handle_unknown).with("--no-option", false) @app.send(:handlearg, "--[no-]option", false) end end end describe "when calling default setup" do before :each do @app.options.stubs(:[]) end [ :debug, :verbose ].each do |level| it "should honor option #{level}" do @app.options.stubs(:[]).with(level).returns(true) Puppet::Util::Log.stubs(:newdestination) @app.setup Puppet::Util::Log.level.should == (level == :verbose ? :info : :debug) end end it "should honor setdest option" do @app.options.stubs(:[]).with(:setdest).returns(false) Puppet::Util::Log.expects(:setup_default) @app.setup end end describe "when configuring routes" do include PuppetSpec::Files before :each do Puppet::Node.indirection.reset_terminus_class end after :each do Puppet::Node.indirection.reset_terminus_class end it "should use the routes specified for only the active application" do Puppet[:route_file] = tmpfile('routes') File.open(Puppet[:route_file], 'w') do |f| f.print <<-ROUTES test_app: node: terminus: exec other_app: node: terminus: plain catalog: terminus: invalid ROUTES end @app.configure_indirector_routes Puppet::Node.indirection.terminus_class.should == 'exec' end it "should not fail if the route file doesn't exist" do Puppet[:route_file] = "/dev/null/non-existent" expect { @app.configure_indirector_routes }.to_not raise_error end it "should raise an error if the routes file is invalid" do Puppet[:route_file] = tmpfile('routes') File.open(Puppet[:route_file], 'w') do |f| f.print <<-ROUTES invalid : : yaml ROUTES end expect { @app.configure_indirector_routes }.to raise_error end end describe "when running" do before :each do @app.stubs(:preinit) @app.stubs(:setup) @app.stubs(:parse_options) end it "should call preinit" do @app.stubs(:run_command) @app.expects(:preinit) @app.run end it "should call parse_options" do @app.stubs(:run_command) @app.expects(:parse_options) @app.run end it "should call run_command" do @app.expects(:run_command) @app.run end it "should call run_command" do @app.expects(:run_command) @app.run end it "should call main as the default command" do @app.expects(:main) @app.run end it "should warn and exit if no command can be called" do Puppet.expects(:err) expect { @app.run }.to exit_with 1 end it "should raise an error if dispatch returns no command" do @app.stubs(:get_command).returns(nil) Puppet.expects(:err) expect { @app.run }.to exit_with 1 end it "should raise an error if dispatch returns an invalid command" do @app.stubs(:get_command).returns(:this_function_doesnt_exist) Puppet.expects(:err) expect { @app.run }.to exit_with 1 end end describe "when metaprogramming" do describe "when calling option" do it "should create a new method named after the option" do @app.class.option("--test1","-t") do end @app.should respond_to(:handle_test1) end it "should transpose in option name any '-' into '_'" do @app.class.option("--test-dashes-again","-t") do end @app.should respond_to(:handle_test_dashes_again) end it "should create a new method called handle_test2 with option(\"--[no-]test2\")" do @app.class.option("--[no-]test2","-t") do end @app.should respond_to(:handle_test2) end describe "when a block is passed" do it "should create a new method with it" do @app.class.option("--[no-]test2","-t") do raise "I can't believe it, it works!" end expect { @app.handle_test2 }.to raise_error end it "should declare the option to OptionParser" do OptionParser.any_instance.stubs(:on) OptionParser.any_instance.expects(:on).with { |*arg| arg[0] == "--[no-]test3" } @app.class.option("--[no-]test3","-t") do end @app.parse_options end it "should pass a block that calls our defined method" do OptionParser.any_instance.stubs(:on) OptionParser.any_instance.stubs(:on).with('--test4','-t').yields(nil) @app.expects(:send).with(:handle_test4, nil) @app.class.option("--test4","-t") do end @app.parse_options end end describe "when no block is given" do it "should declare the option to OptionParser" do OptionParser.any_instance.stubs(:on) OptionParser.any_instance.expects(:on).with("--test4","-t") @app.class.option("--test4","-t") @app.parse_options end it "should give to OptionParser a block that adds the value to the options array" do OptionParser.any_instance.stubs(:on) OptionParser.any_instance.stubs(:on).with("--test4","-t").yields(nil) @app.options.expects(:[]=).with(:test4,nil) @app.class.option("--test4","-t") @app.parse_options end end end end describe "#handle_logdest_arg" do let(:test_arg) { "arg_test_logdest" } it "should log an exception that is raised" do our_exception = Puppet::DevError.new("test exception") Puppet::Util::Log.expects(:newdestination).with(test_arg).raises(our_exception) Puppet.expects(:log_exception).with(our_exception) @app.handle_logdest_arg(test_arg) end it "should set the new log destination" do Puppet::Util::Log.expects(:newdestination).with(test_arg) @app.handle_logdest_arg(test_arg) end it "should set the flag that a destination is set in the options hash" do Puppet::Util::Log.stubs(:newdestination).with(test_arg) @app.handle_logdest_arg(test_arg) @app.options[:setdest].should be_true end end end diff --git a/spec/unit/functions4_spec.rb b/spec/unit/functions4_spec.rb index 5ba4e0a67..8bbe855a1 100644 --- a/spec/unit/functions4_spec.rb +++ b/spec/unit/functions4_spec.rb @@ -1,670 +1,670 @@ 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/ 'Any{2}' else 'Any x, Any 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/ 'Any{2}' else 'Any x, Any 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/ 'Any{2,}' else 'Any x, Any y, Any a?, Any b?, Any 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::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::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' - # Spi Configuration cannot be loaded until the correct parser has been set (injector is turned off otherwise) - require 'puppet/spi' + # Plugins Configuration cannot be loaded until the correct parser has been set (injector is turned off otherwise) + require 'puppet/plugins' end let(:parser) { Puppet::Pops::Parser::EvaluatingParser.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/binder/bindings_composer_spec.rb b/spec/unit/pops/binder/bindings_composer_spec.rb index d8e4b6f9c..d07792e97 100644 --- a/spec/unit/pops/binder/bindings_composer_spec.rb +++ b/spec/unit/pops/binder/bindings_composer_spec.rb @@ -1,66 +1,67 @@ require 'spec_helper' require 'puppet/pops' require 'puppet_spec/pops' +require 'puppet/plugins' describe 'BinderComposer' do include PuppetSpec::Pops def config_dir(config_name) my_fixture(config_name) end let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() } let(:diag) { Puppet::Pops::Binder::Config::DiagnosticProducer.new(acceptor) } let(:issues) { Puppet::Pops::Binder::Config::Issues } let(:node) { Puppet::Node.new('localhost') } let(:compiler) { Puppet::Parser::Compiler.new(node)} let(:scope) { Puppet::Parser::Scope.new(compiler) } let(:parser) { Puppet::Pops::Parser::Parser.new() } let(:factory) { Puppet::Pops::Binder::BindingsFactory } before(:each) do Puppet[:binder] = true end it 'should load default config if no config file exists' do diagnostics = diag composer = Puppet::Pops::Binder::BindingsComposer.new() composer.compose(scope) end context "when loading a complete configuration with modules" do let(:config_directory) { config_dir('ok') } it 'should load everything without errors' do Puppet.settings[:confdir] = config_directory Puppet.settings[:libdir] = File.join(config_directory, 'lib') Puppet.override(:environments => Puppet::Environments::Static.new(Puppet::Node::Environment.create(:production, [File.join(config_directory, 'modules')]))) do # this ensure the binder is active at the right time # (issues with getting a /dev/null path for "confdir" / "libdir") raise "Binder not active" unless scope.compiler.is_binder_active? diagnostics = diag composer = Puppet::Pops::Binder::BindingsComposer.new() the_scope = scope the_scope['fqdn'] = 'localhost' the_scope['environment'] = 'production' layered_bindings = composer.compose(scope) # puts Puppet::Pops::Binder::BindingsModelDumper.new().dump(layered_bindings) binder = Puppet::Pops::Binder::Binder.new(layered_bindings) injector = Puppet::Pops::Binder::Injector.new(binder) expect(injector.lookup(scope, 'awesome_x')).to be == 'golden' expect(injector.lookup(scope, 'good_x')).to be == 'golden' expect(injector.lookup(scope, 'rotten_x')).to be == nil expect(injector.lookup(scope, 'the_meaning_of_life')).to be == 42 expect(injector.lookup(scope, 'has_funny_hat')).to be == 'the pope' expect(injector.lookup(scope, 'all your base')).to be == 'are belong to us' expect(injector.lookup(scope, 'env_meaning_of_life')).to be == 'production thinks it is 42' expect(injector.lookup(scope, '::quick::brown::fox')).to be == 'echo: quick brown fox' end end end # TODO: test error conditions (see BinderConfigChecker for what to test) end diff --git a/spec/unit/pops/evaluator/evaluating_parser_spec.rb b/spec/unit/pops/evaluator/evaluating_parser_spec.rb index fc666cb1d..08f74092c 100644 --- a/spec/unit/pops/evaluator/evaluating_parser_spec.rb +++ b/spec/unit/pops/evaluator/evaluating_parser_spec.rb @@ -1,1325 +1,1325 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/pops/evaluator/evaluator_impl' require 'puppet/loaders' require 'puppet_spec/pops' require 'puppet_spec/scope' require 'puppet/parser/e4_parser_adapter' # relative to this spec file (./) does not work as this file is loaded by rspec #require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper') describe 'Puppet::Pops::Evaluator::EvaluatorImpl' do include PuppetSpec::Pops include PuppetSpec::Scope before(:each) do Puppet[:strict_variables] = true # These must be set since the 3x logic switches some behaviors on these even if the tests explicitly # use the 4x parser and evaluator. # Puppet[:parser] = 'future' - # Spi Configuration cannot be loaded until the correct parser has been set (injector is turned off otherwise) - require 'puppet/spi' + # Plugins Configuration cannot be loaded until the correct parser has been set (injector is turned off otherwise) + require 'puppet/plugins' # Tests needs a known configuration of node/scope/compiler since it parses and evaluates # snippets as the compiler will evaluate them, butwithout the overhead of compiling a complete # catalog for each tested expression. # @parser = Puppet::Pops::Parser::EvaluatingParser.new @node = Puppet::Node.new('node.example.com') @node.environment = Puppet::Node::Environment.create(:testing, []) @compiler = Puppet::Parser::Compiler.new(@node) @scope = Puppet::Parser::Scope.new(@compiler) @scope.source = Puppet::Resource::Type.new(:node, 'node.example.com') @scope.parent = @compiler.topscope end let(:parser) { @parser } let(:scope) { @scope } types = Puppet::Pops::Types::TypeFactory context "When evaluator evaluates literals" do { "1" => 1, "010" => 8, "0x10" => 16, "3.14" => 3.14, "0.314e1" => 3.14, "31.4e-1" => 3.14, "'1'" => '1', "'banana'" => 'banana', '"banana"' => 'banana', "banana" => 'banana', "banana::split" => 'banana::split', "false" => false, "true" => true, "Array" => types.array_of_data(), "/.*/" => /.*/ }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When the evaluator evaluates Lists and Hashes" do { "[]" => [], "[1,2,3]" => [1,2,3], "[1,[2.0, 2.1, [2.2]],[3.0, 3.1]]" => [1,[2.0, 2.1, [2.2]],[3.0, 3.1]], "[2 + 2]" => [4], "[1,2,3] == [1,2,3]" => true, "[1,2,3] != [2,3,4]" => true, "[1,2,3] == [2,2,3]" => false, "[1,2,3] != [1,2,3]" => false, "[1,2,3][2]" => 3, "[1,2,3] + [4,5]" => [1,2,3,4,5], "[1,2,3] + [[4,5]]" => [1,2,3,[4,5]], "[1,2,3] + 4" => [1,2,3,4], "[1,2,3] << [4,5]" => [1,2,3,[4,5]], "[1,2,3] << {'a' => 1, 'b'=>2}" => [1,2,3,{'a' => 1, 'b'=>2}], "[1,2,3] << 4" => [1,2,3,4], "[1,2,3,4] - [2,3]" => [1,4], "[1,2,3,4] - [2,5]" => [1,3,4], "[1,2,3,4] - 2" => [1,3,4], "[1,2,3,[2],4] - 2" => [1,3,[2],4], "[1,2,3,[2,3],4] - [[2,3]]" => [1,2,3,4], "[1,2,3,3,2,4,2,3] - [2,3]" => [1,4], "[1,2,3,['a',1],['b',2]] - {'a' => 1, 'b'=>2}" => [1,2,3], "[1,2,3,{'a'=>1,'b'=>2}] - [{'a' => 1, 'b'=>2}]" => [1,2,3], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "[1,2,3] + {'a' => 1, 'b'=>2}" => [1,2,3,['a',1],['b',2]], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do # This test must be done with match_array since the order of the hash # is undefined and Ruby 1.8.7 and 1.9.3 produce different results. expect(parser.evaluate_string(scope, source, __FILE__)).to match_array(result) end end { "[1,2,3][a]" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end { "{}" => {}, "{'a'=>1,'b'=>2}" => {'a'=>1,'b'=>2}, "{'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}" => {'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}, "{'a'=> 2 + 2}" => {'a'=> 4}, "{'a'=> 1, 'b'=>2} == {'a'=> 1, 'b'=>2}" => true, "{'a'=> 1, 'b'=>2} != {'x'=> 1, 'b'=>2}" => true, "{'a'=> 1, 'b'=>2} == {'a'=> 2, 'b'=>3}" => false, "{'a'=> 1, 'b'=>2} != {'a'=> 1, 'b'=>2}" => false, "{a => 1, b => 2}[b]" => 2, "{2+2 => sum, b => 2}[4]" => 'sum', "{'a'=>1, 'b'=>2} + {'c'=>3}" => {'a'=>1,'b'=>2,'c'=>3}, "{'a'=>1, 'b'=>2} + {'b'=>3}" => {'a'=>1,'b'=>3}, "{'a'=>1, 'b'=>2} + ['c', 3, 'b', 3]" => {'a'=>1,'b'=>3, 'c'=>3}, "{'a'=>1, 'b'=>2} + [['c', 3], ['b', 3]]" => {'a'=>1,'b'=>3, 'c'=>3}, "{'a'=>1, 'b'=>2} - {'b' => 3}" => {'a'=>1}, "{'a'=>1, 'b'=>2, 'c'=>3} - ['b', 'c']" => {'a'=>1}, "{'a'=>1, 'b'=>2, 'c'=>3} - 'c'" => {'a'=>1, 'b'=>2}, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "{'a' => 1, 'b'=>2} << 1" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When the evaluator perform comparisons" do { "'a' == 'a'" => true, "'a' == 'b'" => false, "'a' != 'a'" => false, "'a' != 'b'" => true, "'a' < 'b' " => true, "'a' < 'a' " => false, "'b' < 'a' " => false, "'a' <= 'b'" => true, "'a' <= 'a'" => true, "'b' <= 'a'" => false, "'a' > 'b' " => false, "'a' > 'a' " => false, "'b' > 'a' " => true, "'a' >= 'b'" => false, "'a' >= 'a'" => true, "'b' >= 'a'" => true, "'a' == 'A'" => true, "'a' != 'A'" => false, "'a' > 'A'" => false, "'a' >= 'A'" => true, "'A' < 'a'" => false, "'A' <= 'a'" => true, "1 == 1" => true, "1 == 2" => false, "1 != 1" => false, "1 != 2" => true, "1 < 2 " => true, "1 < 1 " => false, "2 < 1 " => false, "1 <= 2" => true, "1 <= 1" => true, "2 <= 1" => false, "1 > 2 " => false, "1 > 1 " => false, "2 > 1 " => true, "1 >= 2" => false, "1 >= 1" => true, "2 >= 1" => true, "1 == 1.0 " => true, "1 < 1.1 " => true, "'1' < 1.1" => true, "1.0 == 1 " => true, "1.0 < 2 " => true, "1.0 < 'a'" => true, "'1.0' < 1.1" => true, "'1.0' < 'a'" => true, "'1.0' < '' " => true, "'1.0' < ' '" => true, "'a' > '1.0'" => true, "/.*/ == /.*/ " => true, "/.*/ != /a.*/" => true, "true == true " => true, "false == false" => true, "true == false" => false, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'a' =~ /.*/" => true, "'a' =~ '.*'" => true, "/.*/ != /a.*/" => true, "'a' !~ /b.*/" => true, "'a' !~ 'b.*'" => true, '$x = a; a =~ "$x.*"' => true, "a =~ Pattern['a.*']" => true, "a =~ Regexp['a.*']" => false, # String is not subtype of Regexp. PUP-957 "$x = /a.*/ a =~ $x" => true, "$x = Pattern['a.*'] a =~ $x" => true, "1 =~ Integer" => true, "1 !~ Integer" => false, "[1,2,3] =~ Array[Integer[1,10]]" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "666 =~ /6/" => :error, "[a] =~ /a/" => :error, "{a=>1} =~ /a/" => :error, "/a/ =~ /a/" => :error, "Array =~ /A/" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end { "1 in [1,2,3]" => true, "4 in [1,2,3]" => false, "a in {x=>1, a=>2}" => true, "z in {x=>1, a=>2}" => false, "ana in bananas" => true, "xxx in bananas" => false, "/ana/ in bananas" => true, "/xxx/ in bananas" => false, "ANA in bananas" => false, # ANA is a type, not a String "String[1] in bananas" => false, # Philosophically true though :-) "'ANA' in bananas" => true, "ana in 'BANANAS'" => true, "/ana/ in 'BANANAS'" => false, "/ANA/ in 'BANANAS'" => true, "xxx in 'BANANAS'" => false, "[2,3] in [1,[2,3],4]" => true, "[2,4] in [1,[2,3],4]" => false, "[a,b] in ['A',['A','B'],'C']" => true, "[x,y] in ['A',['A','B'],'C']" => false, "a in {a=>1}" => true, "x in {a=>1}" => false, "'A' in {a=>1}" => true, "'X' in {a=>1}" => false, "a in {'A'=>1}" => true, "x in {'A'=>1}" => false, "/xxx/ in {'aaaxxxbbb'=>1}" => true, "/yyy/ in {'aaaxxxbbb'=>1}" => false, "15 in [1, 0xf]" => true, "15 in [1, '0xf']" => true, "'15' in [1, 0xf]" => true, "15 in [1, 115]" => false, "1 in [11, '111']" => false, "'1' in [11, '111']" => false, "Array[Integer] in [2, 3]" => false, "Array[Integer] in [2, [3, 4]]" => true, "Array[Integer] in [2, [a, 4]]" => false, "Integer in { 2 =>'a'}" => true, "Integer[5,10] in [1,5,3]" => true, "Integer[5,10] in [1,2,3]" => false, "Integer in {'a'=>'a'}" => false, "Integer in {'a'=>1}" => false, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "if /(ana)/ in bananas {$1}" => 'ana', "if /(xyz)/ in bananas {$1} else {$1}" => nil, "$a = bananas =~ /(ana)/; $b = /(xyz)/ in bananas; $1" => 'ana', "$a = xyz =~ /(xyz)/; $b = /(ana)/ in bananas; $1" => 'ana', "if /p/ in [pineapple, bananas] {$0}" => 'p', "if /b/ in [pineapple, bananas] {$0}" => 'b', }.each do |source, result| it "sets match variables for a regexp search using in such that '#{source}' produces '#{result}'" do parser.evaluate_string(scope, source, __FILE__).should == result end end { 'Any' => ['Data', 'Scalar', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Collection', 'Array', 'Hash', 'CatalogEntry', 'Resource', 'Class', 'Undef', 'File', 'NotYetKnownResourceType'], # Note, Data > Collection is false (so not included) 'Data' => ['Scalar', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Array', 'Hash',], 'Scalar' => ['Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern'], 'Numeric' => ['Integer', 'Float'], 'CatalogEntry' => ['Class', 'Resource', 'File', 'NotYetKnownResourceType'], 'Integer[1,10]' => ['Integer[2,3]'], }.each do |general, specials| specials.each do |special | it "should compute that #{general} > #{special}" do parser.evaluate_string(scope, "#{general} > #{special}", __FILE__).should == true end it "should compute that #{special} < #{general}" do parser.evaluate_string(scope, "#{special} < #{general}", __FILE__).should == true end it "should compute that #{general} != #{special}" do parser.evaluate_string(scope, "#{special} != #{general}", __FILE__).should == true end end end { 'Integer[1,10] > Integer[2,3]' => true, 'Integer[1,10] == Integer[2,3]' => false, 'Integer[1,10] > Integer[0,5]' => false, 'Integer[1,10] > Integer[1,10]' => false, 'Integer[1,10] >= Integer[1,10]' => true, 'Integer[1,10] == Integer[1,10]' => true, }.each do |source, result| it "should parse and evaluate the integer range comparison expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When the evaluator performs arithmetic" do context "on Integers" do { "2+2" => 4, "2 + 2" => 4, "7 - 3" => 4, "6 * 3" => 18, "6 / 3" => 2, "6 % 3" => 0, "10 % 3" => 1, "-(6/3)" => -2, "-6/3 " => -2, "8 >> 1" => 4, "8 << 1" => 16, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end context "on Floats" do { "2.2 + 2.2" => 4.4, "7.7 - 3.3" => 4.4, "6.1 * 3.1" => 18.91, "6.6 / 3.3" => 2.0, "-(6.0/3.0)" => -2.0, "-6.0/3.0 " => -2.0, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "3.14 << 2" => :error, "3.14 >> 2" => :error, "6.6 % 3.3" => 0.0, "10.0 % 3.0" => 1.0, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "on strings requiring boxing to Numeric" do { "'2' + '2'" => 4, "'-2' + '2'" => 0, "'- 2' + '2'" => 0, '"-\t 2" + "2"' => 0, "'+2' + '2'" => 4, "'+ 2' + '2'" => 4, "'2.2' + '2.2'" => 4.4, "'-2.2' + '2.2'" => 0.0, "'0xF7' + '010'" => 0xFF, "'0xF7' + '0x8'" => 0xFF, "'0367' + '010'" => 0xFF, "'012.3' + '010'" => 20.3, "'-0x2' + '0x4'" => 2, "'+0x2' + '0x4'" => 6, "'-02' + '04'" => 2, "'+02' + '04'" => 6, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'0888' + '010'" => :error, "'0xWTF' + '010'" => :error, "'0x12.3' + '010'" => :error, "'0x12.3' + '010'" => :error, '"-\n 2" + "2"' => :error, '"-\v 2" + "2"' => :error, '"-2\n" + "2"' => :error, '"-2\n " + "2"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end end end # arithmetic context "When the evaluator evaluates assignment" do { "$a = 5" => 5, "$a = 5; $a" => 5, "$a = 5; $b = 6; $a" => 5, "$a = $b = 5; $a == $b" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "[a,b,c] = [1,2,3]" => /attempt to assign to 'an Array Expression'/, "[a,b,c] = {b=>2,c=>3,a=>1}" => /attempt to assign to 'an Array Expression'/, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to error with #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Puppet::ParseError, result) end end end context "When the evaluator evaluates conditionals" do { "if true {5}" => 5, "if false {5}" => nil, "if false {2} else {5}" => 5, "if false {2} elsif true {5}" => 5, "if false {2} elsif false {5}" => nil, "unless false {5}" => 5, "unless true {5}" => nil, "unless true {2} else {5}" => 5, "$a = if true {5} $a" => 5, "$a = if false {5} $a" => nil, "$a = if false {2} else {5} $a" => 5, "$a = if false {2} elsif true {5} $a" => 5, "$a = if false {2} elsif false {5} $a" => nil, "$a = unless false {5} $a" => 5, "$a = unless true {5} $a" => nil, "$a = unless true {2} else {5} $a" => 5, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "case 1 { 1 : { yes } }" => 'yes', "case 2 { 1,2,3 : { yes} }" => 'yes', "case 2 { 1,3 : { no } 2: { yes} }" => 'yes', "case 2 { 1,3 : { no } 5: { no } default: { yes }}" => 'yes', "case 2 { 1,3 : { no } 5: { no } }" => nil, "case 'banana' { 1,3 : { no } /.*ana.*/: { yes } }" => 'yes', "case 'banana' { /.*(ana).*/: { $1 } }" => 'ana', "case [1] { Array : { yes } }" => 'yes', "case [1] { Array[String] : { no } Array[Integer]: { yes } }" => 'yes', "case 1 { Integer : { yes } Type[Integer] : { no } }" => 'yes', "case Integer { Integer : { no } Type[Integer] : { yes } }" => 'yes', # supports unfold "case ringo { *[paul, john, ringo, george] : { 'beatle' } }" => 'beatle', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "2 ? { 1 => no, 2 => yes}" => 'yes', "3 ? { 1 => no, 2 => no, default => yes }" => 'yes', "3 ? { 1 => no, default => yes, 3 => no }" => 'no', "3 ? { 1 => no, 3 => no, default => yes }" => 'no', "4 ? { 1 => no, default => yes, 3 => no }" => 'yes', "4 ? { 1 => no, 3 => no, default => yes }" => 'yes', "'banana' ? { /.*(ana).*/ => $1 }" => 'ana', "[2] ? { Array[String] => yes, Array => yes}" => 'yes', "ringo ? *[paul, john, ringo, george] => 'beatle'" => 'beatle', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end it 'fails if a selector does not match' do expect{parser.evaluate_string(scope, "2 ? 3 => 4")}.to raise_error(/No matching entry for selector parameter with value '2'/) end end context "When evaluator evaluated unfold" do { "*[1,2,3]" => [1,2,3], "*1" => [1], "*'a'" => ['a'] }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end it "should parse and evaluate the expression '*{a=>10, b=>20} to [['a',10],['b',20]]" do result = parser.evaluate_string(scope, '*{a=>10, b=>20}', __FILE__) expect(result).to include(['a', 10]) expect(result).to include(['b', 20]) end end context "When evaluator performs [] operations" do { "[1,2,3][0]" => 1, "[1,2,3][2]" => 3, "[1,2,3][3]" => nil, "[1,2,3][-1]" => 3, "[1,2,3][-2]" => 2, "[1,2,3][-4]" => nil, "[1,2,3,4][0,2]" => [1,2], "[1,2,3,4][1,3]" => [2,3,4], "[1,2,3,4][-2,2]" => [3,4], "[1,2,3,4][-3,2]" => [2,3], "[1,2,3,4][3,5]" => [4], "[1,2,3,4][5,2]" => [], "[1,2,3,4][0,-1]" => [1,2,3,4], "[1,2,3,4][0,-2]" => [1,2,3], "[1,2,3,4][0,-4]" => [1], "[1,2,3,4][0,-5]" => [], "[1,2,3,4][-5,2]" => [1], "[1,2,3,4][-5,-3]" => [1,2], "[1,2,3,4][-6,-3]" => [1,2], "[1,2,3,4][2,-3]" => [], "[1,*[2,3],4]" => [1,2,3,4], "[1,*[2,3],4][1]" => 2, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "{a=>1, b=>2, c=>3}[a]" => 1, "{a=>1, b=>2, c=>3}[c]" => 3, "{a=>1, b=>2, c=>3}[x]" => nil, "{a=>1, b=>2, c=>3}[c,b]" => [3,2], "{a=>1, b=>2, c=>3}[a,b,c]" => [1,2,3], "{a=>{b=>{c=>'it works'}}}[a][b][c]" => 'it works', "$a = {undef => 10} $a[free_lunch]" => nil, "$a = {undef => 10} $a[undef]" => 10, "$a = {undef => 10} $a[$a[free_lunch]]" => 10, "$a = {} $a[free_lunch] == undef" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'abc'[0]" => 'a', "'abc'[2]" => 'c', "'abc'[-1]" => 'c', "'abc'[-2]" => 'b', "'abc'[-3]" => 'a', "'abc'[-4]" => '', "'abc'[3]" => '', "abc[0]" => 'a', "abc[2]" => 'c', "abc[-1]" => 'c', "abc[-2]" => 'b', "abc[-3]" => 'a', "abc[-4]" => '', "abc[3]" => '', "'abcd'[0,2]" => 'ab', "'abcd'[1,3]" => 'bcd', "'abcd'[-2,2]" => 'cd', "'abcd'[-3,2]" => 'bc', "'abcd'[3,5]" => 'd', "'abcd'[5,2]" => '', "'abcd'[0,-1]" => 'abcd', "'abcd'[0,-2]" => 'abc', "'abcd'[0,-4]" => 'a', "'abcd'[0,-5]" => '', "'abcd'[-5,2]" => 'a', "'abcd'[-5,-3]" => 'ab', "'abcd'[-6,-3]" => 'ab', "'abcd'[2,-3]" => '', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # Type operations (full set tested by tests covering type calculator) { "Array[Integer]" => types.array_of(types.integer), "Array[Integer,1]" => types.constrain_size(types.array_of(types.integer),1, :default), "Array[Integer,1,2]" => types.constrain_size(types.array_of(types.integer),1, 2), "Array[Integer,Integer[1,2]]" => types.constrain_size(types.array_of(types.integer),1, 2), "Array[Integer,Integer[1]]" => types.constrain_size(types.array_of(types.integer),1, :default), "Hash[Integer,Integer]" => types.hash_of(types.integer, types.integer), "Hash[Integer,Integer,1]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, :default), "Hash[Integer,Integer,1,2]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, 2), "Hash[Integer,Integer,Integer[1,2]]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, 2), "Hash[Integer,Integer,Integer[1]]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, :default), "Resource[File]" => types.resource('File'), "Resource['File']" => types.resource(types.resource('File')), "File[foo]" => types.resource('file', 'foo'), "File[foo, bar]" => [types.resource('file', 'foo'), types.resource('file', 'bar')], "Pattern[a, /b/, Pattern[c], Regexp[d]]" => types.pattern('a', 'b', 'c', 'd'), "String[1,2]" => types.constrain_size(types.string,1, 2), "String[Integer[1,2]]" => types.constrain_size(types.string,1, 2), "String[Integer[1]]" => types.constrain_size(types.string,1, :default), }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # LHS where [] not supported, and missing key(s) { "Array[]" => :error, "'abc'[]" => :error, "Resource[]" => :error, "File[]" => :error, "String[]" => :error, "1[]" => :error, "3.14[]" => :error, "/.*/[]" => :error, "$a=[1] $a[]" => :error, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(/Syntax error/) end end # Errors when wrong number/type of keys are used { "Array[0]" => 'Array-Type[] arguments must be types. Got Fixnum', "Hash[0]" => 'Hash-Type[] arguments must be types. Got Fixnum', "Hash[Integer, 0]" => 'Hash-Type[] arguments must be types. Got Fixnum', "Array[Integer,1,2,3]" => 'Array-Type[] accepts 1 to 3 arguments. Got 4', "Array[Integer,String]" => "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got a String-Type", "Hash[Integer,String, 1,2,3]" => 'Hash-Type[] accepts 1 to 4 arguments. Got 5', "'abc'[x]" => "The value 'x' cannot be converted to Numeric", "'abc'[1.0]" => "A String[] cannot use Float where Integer is expected", "'abc'[1,2,3]" => "String supports [] with one or two arguments. Got 3", "Resource[0]" => 'First argument to Resource[] must be a resource type or a String. Got Integer', "Resource[a, 0]" => 'Error creating type specialization of a Resource-Type, Cannot use Integer where a resource title String is expected', "File[0]" => 'Error creating type specialization of a File-Type, Cannot use Integer where a resource title String is expected', "String[a]" => "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got a String", "Pattern[0]" => 'Error creating type specialization of a Pattern-Type, Cannot use Integer where String or Regexp or Pattern-Type or Regexp-Type is expected', "Regexp[0]" => 'Error creating type specialization of a Regexp-Type, Cannot use Integer where String or Regexp is expected', "Regexp[a,b]" => 'A Regexp-Type[] accepts 1 argument. Got 2', "true[0]" => "Operator '[]' is not applicable to a Boolean", "1[0]" => "Operator '[]' is not applicable to an Integer", "3.14[0]" => "Operator '[]' is not applicable to a Float", "/.*/[0]" => "Operator '[]' is not applicable to a Regexp", "[1][a]" => "The value 'a' cannot be converted to Numeric", "[1][0.0]" => "An Array[] cannot use Float where Integer is expected", "[1]['0.0']" => "An Array[] cannot use Float where Integer is expected", "[1,2][1, 0.0]" => "An Array[] cannot use Float where Integer is expected", "[1,2][1.0, -1]" => "An Array[] cannot use Float where Integer is expected", "[1,2][1, -1.0]" => "An Array[] cannot use Float where Integer is expected", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Regexp.new(Regexp.quote(result))) end end context "on catalog types" do it "[n] gets resource parameter [n]" do source = "notify { 'hello': message=>'yo'} Notify[hello][message]" parser.evaluate_string(scope, source, __FILE__).should == 'yo' end it "[n] gets class parameter [n]" do source = "class wonka($produces='chocolate'){ } include wonka Class[wonka][produces]" # This is more complicated since it needs to run like 3.x and do an import_ast adapted_parser = Puppet::Parser::E4ParserAdapter.new adapted_parser.file = __FILE__ ast = adapted_parser.parse(source) Puppet.override({:global_scope => scope}, "test") do scope.known_resource_types.import_ast(ast, '') ast.code.safeevaluate(scope).should == 'chocolate' end end # Resource default and override expressions and resource parameter access with [] { # Properties "notify { id: message=>explicit} Notify[id][message]" => "explicit", "Notify { message=>by_default} notify {foo:} Notify[foo][message]" => "by_default", "notify {foo:} Notify[foo]{message =>by_override} Notify[foo][message]" => "by_override", # Parameters "notify { id: withpath=>explicit} Notify[id][withpath]" => "explicit", "Notify { withpath=>by_default } notify { foo: } Notify[foo][withpath]" => "by_default", "notify {foo:} Notify[foo]{withpath=>by_override} Notify[foo][withpath]" => "by_override", # Metaparameters "notify { foo: tag => evoe} Notify[foo][tag]" => "evoe", # Does not produce the defaults for tag parameter (title, type or names of scopes) "notify { foo: } Notify[foo][tag]" => nil, # But a default may be specified on the type "Notify { tag=>by_default } notify { foo: } Notify[foo][tag]" => "by_default", "Notify { tag=>by_default } notify { foo: } Notify[foo]{ tag=>by_override } Notify[foo][tag]" => "by_override", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # Virtual and realized resource default and overridden resource parameter access with [] { # Properties "@notify { id: message=>explicit } Notify[id][message]" => "explicit", "@notify { id: message=>explicit } realize Notify[id] Notify[id][message]" => "explicit", "Notify { message=>by_default } @notify { id: } Notify[id][message]" => "by_default", "Notify { message=>by_default } @notify { id: tag=>thisone } Notify <| tag == thisone |>; Notify[id][message]" => "by_default", "@notify { id: } Notify[id]{message=>by_override} Notify[id][message]" => "by_override", # Parameters "@notify { id: withpath=>explicit } Notify[id][withpath]" => "explicit", "Notify { withpath=>by_default } @notify { id: } Notify[id][withpath]" => "by_default", "@notify { id: } realize Notify[id] Notify[id]{withpath=>by_override} Notify[id][withpath]" => "by_override", # Metaparameters "@notify { id: tag=>explicit } Notify[id][tag]" => "explicit", }.each do |source, result| it "parses and evaluates virtual and realized resources in the expression '#{source}' to #{result}" do expect(parser.evaluate_string(scope, source, __FILE__)).to eq(result) end end # Exported resource attributes { "@@notify { id: message=>explicit } Notify[id][message]" => "explicit", "@@notify { id: message=>explicit, tag=>thisone } Notify <<| tag == thisone |>> Notify[id][message]" => "explicit", }.each do |source, result| it "parses and evaluates exported resources in the expression '#{source}' to #{result}" do expect(parser.evaluate_string(scope, source, __FILE__)).to eq(result) end end # Resource default and override expressions and resource parameter access error conditions { "notify { xid: message=>explicit} Notify[id][message]" => /Resource not found/, "notify { id: message=>explicit} Notify[id][mustard]" => /does not have a parameter called 'mustard'/, # NOTE: these meta-esque parameters are not recognized as such "notify { id: message=>explicit} Notify[id][title]" => /does not have a parameter called 'title'/, "notify { id: message=>explicit} Notify[id]['type']" => /does not have a parameter called 'type'/, "notify { id: message=>explicit } Notify[id]{message=>override}" => /'message' is already set on Notify\[id\]/ }.each do |source, result| it "should parse '#{source}' and raise error matching #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(result) end end context 'with errors' do { "Class['fail-whale']" => /Illegal name/, "Class[0]" => /An Integer cannot be used where a String is expected/, "Class[/.*/]" => /A Regexp cannot be used where a String is expected/, "Class[4.1415]" => /A Float cannot be used where a String is expected/, "Class[Integer]" => /An Integer-Type cannot be used where a String is expected/, "Class[File['tmp']]" => /A File\['tmp'\] Resource-Reference cannot be used where a String is expected/, }.each do | source, error_pattern| it "an error is flagged for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(error_pattern) end end end end # end [] operations end context "When the evaluator performs boolean operations" do { "true and true" => true, "false and true" => false, "true and false" => false, "false and false" => false, "true or true" => true, "false or true" => true, "true or false" => true, "false or false" => false, "! true" => false, "!! true" => true, "!! false" => false, "! 'x'" => false, "! ''" => false, "! undef" => true, "! [a]" => false, "! []" => false, "! {a=>1}" => false, "! {}" => false, "true and false and '0xwtf' + 1" => false, "false or true or '0xwtf' + 1" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "false || false || '0xwtf' + 1" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluator performs operations on literal undef" do it "computes non existing hash lookup as undef" do parser.evaluate_string(scope, "{a => 1}[b] == undef", __FILE__).should == true parser.evaluate_string(scope, "undef == {a => 1}[b]", __FILE__).should == true end end context "When evaluator performs calls" do let(:populate) do parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]") end { 'sprintf( "x%iy", $a )' => "x10y", # unfolds 'sprintf( *["x%iy", $a] )' => "x10y", '"x%iy".sprintf( $a )' => "x10y", '$b.reduce |$memo,$x| { $memo + $x }' => 6, 'reduce($b) |$memo,$x| { $memo + $x }' => 6, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do populate parser.evaluate_string(scope, source, __FILE__).should == result end end { '"value is ${a*2} yo"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end it "provides location information on error in unparenthesized call logic" do expect{parser.evaluate_string(scope, "include non_existing_class", __FILE__)}.to raise_error(Puppet::ParseError, /line 1\:1/) end it 'defaults can be given in a lambda and used only when arg is missing' do env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:test) do dispatch :test do param 'Integer', 'count' required_block_param end def test(count, block) block.call({}, *[].fill(10, 0, count)) end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'test', the_func, __FILE__) expect(parser.evaluate_string(scope, "test(1) |$x, $y=20| { $x + $y}")).to eql(30) expect(parser.evaluate_string(scope, "test(2) |$x, $y=20| { $x + $y}")).to eql(20) end it 'a given undef does not select the default value' do env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:test) do dispatch :test do param 'Any', 'lambda_arg' required_block_param end def test(lambda_arg, block) block.call({}, lambda_arg) end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'test', the_func, __FILE__) expect(parser.evaluate_string(scope, "test(undef) |$x=20| { $x == undef}")).to eql(true) end it 'a given undef is given as nil' do env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:assert_no_undef) do dispatch :assert_no_undef do param 'Any', 'x' end def assert_no_undef(x) case x when Array return unless x.include?(:undef) when Hash return unless x.keys.include?(:undef) || x.values.include?(:undef) else return unless x == :undef end raise "contains :undef" end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'assert_no_undef', the_func, __FILE__) expect{parser.evaluate_string(scope, "assert_no_undef(undef)")}.to_not raise_error() expect{parser.evaluate_string(scope, "assert_no_undef([undef])")}.to_not raise_error() expect{parser.evaluate_string(scope, "assert_no_undef({undef => 1})")}.to_not raise_error() expect{parser.evaluate_string(scope, "assert_no_undef({1 => undef})")}.to_not raise_error() end context 'using the 3x function api' do it 'can call a 3x function' do Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0] } parser.evaluate_string(scope, "bazinga(42)", __FILE__).should == 42 end it 'maps :undef to empty string' do Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0] } parser.evaluate_string(scope, "$a = {} bazinga($a[nope])", __FILE__).should == '' parser.evaluate_string(scope, "bazinga(undef)", __FILE__).should == '' end it 'does not map :undef to empty string in arrays' do Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0][0] } parser.evaluate_string(scope, "$a = {} $b = [$a[nope]] bazinga($b)", __FILE__).should == :undef parser.evaluate_string(scope, "bazinga([undef])", __FILE__).should == :undef end it 'does not map :undef to empty string in hashes' do Puppet::Parser::Functions.newfunction("bazinga", :type => :rvalue) { |args| args[0]['a'] } parser.evaluate_string(scope, "$a = {} $b = {a => $a[nope]} bazinga($b)", __FILE__).should == :undef parser.evaluate_string(scope, "bazinga({a => undef})", __FILE__).should == :undef end end end context "When evaluator performs string interpolation" do let(:populate) do parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]") end { '"value is $a yo"' => "value is 10 yo", '"value is \$a yo"' => "value is $a yo", '"value is ${a} yo"' => "value is 10 yo", '"value is \${a} yo"' => "value is ${a} yo", '"value is ${$a} yo"' => "value is 10 yo", '"value is ${$a*2} yo"' => "value is 20 yo", '"value is ${sprintf("x%iy",$a)} yo"' => "value is x10y yo", '"value is ${"x%iy".sprintf($a)} yo"' => "value is x10y yo", '"value is ${[1,2,3]} yo"' => "value is [1, 2, 3] yo", '"value is ${/.*/} yo"' => "value is /.*/ yo", '$x = undef "value is $x yo"' => "value is yo", '$x = default "value is $x yo"' => "value is default yo", '$x = Array[Integer] "value is $x yo"' => "value is Array[Integer] yo", '"value is ${Array[Integer]} yo"' => "value is Array[Integer] yo", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do populate parser.evaluate_string(scope, source, __FILE__).should == result end end it "should parse and evaluate an interpolation of a hash" do source = '"value is ${{a=>1,b=>2}} yo"' # This test requires testing against two options because a hash to string # produces a result that is unordered hashstr = {'a' => 1, 'b' => 2}.to_s alt_results = ["value is {a => 1, b => 2} yo", "value is {b => 2, a => 1} yo" ] populate parse_result = parser.evaluate_string(scope, source, __FILE__) alt_results.include?(parse_result).should == true end it 'should accept a variable with leading underscore when used directly' do source = '$_x = 10; "$_x"' expect(parser.evaluate_string(scope, source, __FILE__)).to eql('10') end it 'should accept a variable with leading underscore when used as an expression' do source = '$_x = 10; "${_x}"' expect(parser.evaluate_string(scope, source, __FILE__)).to eql('10') end { '"value is ${a*2} yo"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluating variables" do context "that are non existing an error is raised for" do it "unqualified variable" do expect { parser.evaluate_string(scope, "$quantum_gravity", __FILE__) }.to raise_error(/Unknown variable/) end it "qualified variable" do expect { parser.evaluate_string(scope, "$quantum_gravity::graviton", __FILE__) }.to raise_error(/Unknown variable/) end end it "a lex error should be raised for '$foo::::bar'" do expect { parser.evaluate_string(scope, "$foo::::bar") }.to raise_error(Puppet::LexError, /Illegal fully qualified name at line 1:7/) end { '$a = $0' => nil, '$a = $1' => nil, }.each do |source, value| it "it is ok to reference numeric unassigned variables '#{source}'" do parser.evaluate_string(scope, source, __FILE__).should == value end end { '$00 = 0' => /must be a decimal value/, '$0xf = 0' => /must be a decimal value/, '$0777 = 0' => /must be a decimal value/, '$123a = 0' => /must be a decimal value/, }.each do |source, error_pattern| it "should raise an error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(error_pattern) end end context "an initial underscore in the last segment of a var name is allowed" do { '$_a = 1' => 1, '$__a = 1' => 1, }.each do |source, value| it "as in this example '#{source}'" do parser.evaluate_string(scope, source, __FILE__).should == value end end end end context "When evaluating relationships" do it 'should form a relation with File[a] -> File[b]' do source = "File[a] -> File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'b']) end it 'should form a relation with resource -> resource' do source = "notify{a:} -> notify{b:}" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['Notify', 'a', '->', 'Notify', 'b']) end it 'should form a relation with [File[a], File[b]] -> [File[x], File[y]]' do source = "[File[a], File[b]] -> [File[x], File[y]]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'x']) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'x']) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'y']) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'y']) end it 'should tolerate (eliminate) duplicates in operands' do source = "[File[a], File[a]] -> File[x]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'x']) scope.compiler.relationships.size.should == 1 end it 'should form a relation with <-' do source = "File[a] <- File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'a']) end it 'should form a relation with <-' do source = "File[a] <~ File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'b', '~>', 'File', 'a']) end end context "When evaluating heredoc" do it "evaluates plain heredoc" do src = "@(END)\nThis is\nheredoc text\nEND\n" parser.evaluate_string(scope, src).should == "This is\nheredoc text\n" end it "parses heredoc with margin" do src = [ "@(END)", " This is", " heredoc text", " | END", "" ].join("\n") parser.evaluate_string(scope, src).should == "This is\nheredoc text\n" end it "parses heredoc with margin and right newline trim" do src = [ "@(END)", " This is", " heredoc text", " |- END", "" ].join("\n") parser.evaluate_string(scope, src).should == "This is\nheredoc text" end it "parses escape specification" do src = <<-CODE @(END/t) Tex\\tt\\n |- END CODE parser.evaluate_string(scope, src).should == "Tex\tt\\n" end it "parses syntax checked specification" do src = <<-CODE @(END:json) ["foo", "bar"] |- END CODE parser.evaluate_string(scope, src).should == '["foo", "bar"]' end it "parses syntax checked specification with error and reports it" do src = <<-CODE @(END:json) ['foo', "bar"] |- END CODE expect { parser.evaluate_string(scope, src)}.to raise_error(/Cannot parse invalid JSON string/) end it "parses interpolated heredoc expression" do src = <<-CODE $name = 'Fjodor' @("END") Hello $name |- END CODE parser.evaluate_string(scope, src).should == "Hello Fjodor" end end context "Handles Deprecations and Discontinuations" do it 'of import statements' do source = "\nimport foo" # Error references position 5 at the opening '{' # Set file to nil to make it easier to match with line number (no file name in output) expect { parser.evaluate_string(scope, source) }.to raise_error(/'import' has been discontinued.*line 2:1/) end end context "Detailed Error messages are reported" do it 'for illegal type references' do source = '1+1 { "title": }' # Error references position 5 at the opening '{' # Set file to nil to make it easier to match with line number (no file name in output) expect { parser.evaluate_string(scope, source) }.to raise_error( /Illegal Resource Type expression, expected result to be a type name, or untitled Resource.*line 1:2/) end it 'for non r-value producing <| |>' do expect { parser.parse_string("$a = File <| |>", nil) }.to raise_error(/A Virtual Query does not produce a value at line 1:6/) end it 'for non r-value producing <<| |>>' do expect { parser.parse_string("$a = File <<| |>>", nil) }.to raise_error(/An Exported Query does not produce a value at line 1:6/) end it 'for non r-value producing define' do Puppet.expects(:err).with("Invalid use of expression. A 'define' expression does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = define foo { }", nil) }.to raise_error(/2 errors/) end it 'for non r-value producing class' do Puppet.expects(:err).with("Invalid use of expression. A Host Class Definition does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = class foo { }", nil) }.to raise_error(/2 errors/) end it 'for unclosed quote with indication of start position of string' do source = <<-SOURCE.gsub(/^ {6}/,'') $a = "xx yyy SOURCE # first char after opening " reported as being in error. expect { parser.parse_string(source) }.to raise_error(/Unclosed quote after '"' followed by 'xx\\nyy\.\.\.' at line 1:7/) end it 'for multiple errors with a summary exception' do Puppet.expects(:err).with("Invalid use of expression. A Node Definition does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = node x { }",nil) }.to raise_error(/2 errors/) end it 'for a bad hostname' do expect { parser.parse_string("node 'macbook+owned+by+name' { }", nil) }.to raise_error(/The hostname 'macbook\+owned\+by\+name' contains illegal characters.*at line 1:6/) end it 'for a hostname with interpolation' do source = <<-SOURCE.gsub(/^ {6}/,'') $name = 'fred' node "macbook-owned-by$name" { } SOURCE expect { parser.parse_string(source, nil) }.to raise_error(/An interpolated expression is not allowed in a hostname of a node at line 2:23/) end end context 'does not leak variables' do it 'local variables are gone when lambda ends' do source = <<-SOURCE [1,2,3].each |$x| { $y = $x} $a = $y SOURCE expect do parser.evaluate_string(scope, source) end.to raise_error(/Unknown variable: 'y'/) end it 'lambda parameters are gone when lambda ends' do source = <<-SOURCE [1,2,3].each |$x| { $y = $x} $a = $x SOURCE expect do parser.evaluate_string(scope, source) end.to raise_error(/Unknown variable: 'x'/) end it 'does not leak match variables' do source = <<-SOURCE if 'xyz' =~ /(x)(y)(z)/ { notice $2 } case 'abc' { /(a)(b)(c)/ : { $x = $2 } } "-$x-$2-" SOURCE expect(parser.evaluate_string(scope, source)).to eq('-b--') end end matcher :have_relationship do |expected| calc = Puppet::Pops::Types::TypeCalculator.new match do |compiler| op_name = {'->' => :relationship, '~>' => :subscription} compiler.relationships.any? do | relation | relation.source.type == expected[0] && relation.source.title == expected[1] && relation.type == op_name[expected[2]] && relation.target.type == expected[3] && relation.target.title == expected[4] end end failure_message_for_should do |actual| "Relationship #{expected[0]}[#{expected[1]}] #{expected[2]} #{expected[3]}[#{expected[4]}] but was unknown to compiler" end end end