diff --git a/lib/puppet/node.rb b/lib/puppet/node.rb index ad5a40b54..721c9f586 100644 --- a/lib/puppet/node.rb +++ b/lib/puppet/node.rb @@ -1,127 +1,113 @@ require 'puppet/indirector' # A class for managing nodes, including their facts and environment. class Puppet::Node require 'puppet/node/facts' require 'puppet/node/environment' # Set up indirection, so that nodes can be looked for in # the node sources. extend Puppet::Indirector # Use the node source as the indirection terminus. indirects :node, :terminus_setting => :node_terminus, :doc => "Where to find node information. A node is composed of its name, its facts, and its environment." - attr_accessor :name, :classes, :parameters, :source, :ipaddress - attr_reader :time - attr_writer :environment - - # Do not return environments that are the empty string, and use - # explicitly set environments, then facts, then a central env - # value. - def environment - unless @environment - if env = parameters["environment"] - @environment = env - else - @environment = Puppet::Node::Environment.new.name.to_s - end - end - @environment - end + attr_accessor :name, :classes, :source, :ipaddress, :environment + attr_reader :time, :parameters def initialize(name, options = {}) unless name raise ArgumentError, "Node names cannot be nil" end @name = name if classes = options[:classes] if classes.is_a?(String) @classes = [classes] else @classes = classes end else @classes = [] end @parameters = options[:parameters] || {} - self.environment = options[:environment] if options[:environment] + env = options[:environment] || @parameters[:environment] || @parameters["environment"] || Puppet::Node::Environment.new + @environment = env.is_a?(String) ? Puppet::Node::Environment.new(env) : env @time = Time.now end # Merge the node facts with parameters from the node source. def fact_merge begin if facts = Puppet::Node::Facts.find(name) merge(facts.values) end rescue => detail error = Puppet::Error.new("Could not retrieve facts for %s: %s" % [name, detail]) error.set_backtrace(detail.backtrace) raise error end end # Merge any random parameters into our parameter list. def merge(params) params.each do |name, value| @parameters[name] = value unless @parameters.include?(name) end - @parameters["environment"] ||= self.environment if self.environment + @parameters["environment"] ||= self.environment.name.to_s if self.environment end # Calculate the list of names we might use for looking # up our node. This is only used for AST nodes. def names if Puppet.settings[:strict_hostname_checking] return [name] end names = [] if name.include?(".") names += split_name(name) end # First, get the fqdn unless fqdn = parameters["fqdn"] if parameters["hostname"] and parameters["domain"] fqdn = parameters["hostname"] + "." + parameters["domain"] else Puppet.warning "Host is missing hostname and/or domain: %s" % name end end # Now that we (might) have the fqdn, add each piece to the name # list to search, in order of longest to shortest. if fqdn names += split_name(fqdn) end # And make sure the node name is first, since that's the most # likely usage. # The name is usually the Certificate CN, but it can be # set to the 'facter' hostname instead. if Puppet[:node_name] == 'cert' names.unshift name else names.unshift parameters["hostname"] end names.uniq end def split_name(name) list = name.split(".") tmp = [] list.each_with_index do |short, i| tmp << list[0..i].join(".") end tmp.reverse end end diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb index 51df86ec6..f9c8f70ae 100644 --- a/lib/puppet/parser/compiler.rb +++ b/lib/puppet/parser/compiler.rb @@ -1,405 +1,403 @@ # Created by Luke A. Kanies on 2007-08-13. # Copyright (c) 2007. All rights reserved. require 'puppet/node' require 'puppet/resource/catalog' require 'puppet/util/errors' +require 'puppet/parser/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 include Puppet::Util include Puppet::Util::Errors - attr_reader :parser, :node, :facts, :collections, :catalog, :node_scope, :resources + include Puppet::Parser::ResourceTypeCollectionHelper + + attr_reader :node, :facts, :collections, :catalog, :node_scope, :resources # Add a collection to the global list. def add_collection(coll) @collections << coll end # 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 # Store a resource in our resource table. def add_resource(scope, resource) @resources << resource # Note that this will fail if the resource is not unique. @catalog.add_resource(resource) # And in the resource graph. At some point, this might supercede # the global resource table, but the table is a lot faster # so it makes sense to maintain for now. if resource.type.to_s.downcase == "class" and main = @catalog.resource(:class, :main) @catalog.add_edge(main, resource) else @catalog.add_edge(scope.resource, resource) end end # Do we use nodes found in the code, vs. the external node sources? def ast_nodes? - parser.nodes? + known_resource_types.nodes? end # 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 classlist return @catalog.classes end # Compiler our catalog. This mostly revolves around finding and evaluating classes. # This is the main entry into our catalog. def compile # Set the client's parameters into the top scope. set_node_parameters() evaluate_main() evaluate_ast_node() evaluate_node_classes() evaluate_generators() finish() fail_on_unevaluated() return @catalog end # LAK:FIXME There are no tests for this. def delete_collection(coll) @collections.delete(coll) if @collections.include?(coll) end # Return the node's environment. def environment unless defined? @environment if node.environment and node.environment != "" @environment = node.environment else @environment = nil end end @environment end # Evaluate all of the classes specified by the node. def evaluate_node_classes evaluate_classes(@node.classes, topscope) end # Evaluate each specified class in turn. If there are any classes we can't # find, just tag the catalog and move on. This method really just # creates resource objects that point back to the classes, and then the # resources are themselves evaluated later in the process. def evaluate_classes(classes, scope, lazy_evaluate = true) unless scope.source raise Puppet::DevError, "No source for scope passed to evaluate_classes" end found = [] classes.each do |name| # If we can find the class, then make a resource that will evaluate it. if klass = scope.find_hostclass(name) found << name and next if scope.class_scope(klass) resource = klass.mk_plain_resource(scope) # If they've disabled lazy evaluation (which the :include function does), # then evaluate our resource immediately. resource.evaluate unless lazy_evaluate found << name else Puppet.info "Could not find class %s for %s" % [name, node.name] @catalog.tag(name) end end found end # Return a resource by either its ref or its type and title. def findresource(*args) @catalog.resource(*args) end - # Set up our compile. We require a parser - # and a node object; the parser is so we can look up classes - # and AST nodes, and the node has all of the client's info, - # like facts and environment. - def initialize(node, parser, options = {}) + def initialize(node, options = {}) @node = node - @parser = parser options.each do |param, value| begin send(param.to_s + "=", value) rescue NoMethodError raise ArgumentError, "Compiler objects do not accept %s" % param end end initvars() init_main() end # Create a new scope, with either a specified parent scope or # using the top scope. def newscope(parent, options = {}) parent ||= topscope options[:compiler] = self - options[:parser] ||= self.parser scope = Puppet::Parser::Scope.new(options) scope.parent = parent scope end # Return any overrides for the given resource. def resource_overrides(resource) @resource_overrides[resource.ref] end # The top scope is usually the top-level scope, but if we're using AST nodes, # then it is instead the node's scope. def topscope node_scope || @topscope end private # If ast nodes are enabled, then see if we can find and evaluate one. def evaluate_ast_node return unless ast_nodes? # Now see if we can find the node. astnode = nil @node.names.each do |name| - break if astnode = @parser.node(name.to_s.downcase) + break if astnode = known_resource_types.node(name.to_s.downcase) end - unless (astnode ||= @parser.node("default")) + unless (astnode ||= known_resource_types.node("default")) raise Puppet::ParseError, "Could not find default node or by name with '%s'" % node.names.join(", ") end # Create a resource to model this node, and then add it to the list # of resources. resource = astnode.mk_plain_resource(topscope) resource.evaluate # Now set the node scope appropriately, so that :topscope can # behave differently. @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? found_something = false 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. @collections.dup.each do |collection| found_something = true if collection.evaluate end end return found_something 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 if ary = unevaluated_resources evaluated = false ary.each do |resource| if not resource.virtual? resource.evaluate evaluated = true end end # If we evaluated, let the loop know. return evaluated else return false 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 # Call collections first, then definitions. done = false if evaluate_collections done = false if evaluate_definitions 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 = @parser.find_hostclass("", "") || @parser.newclass("") + @main = known_resource_types.find_hostclass("", "") || known_resource_types.add(Puppet::Parser::ResourceType.new(:hostclass, "")) @topscope.source = @main @main_resource = Puppet::Parser::Resource.new(:type => "class", :title => :main, :scope => @topscope, :source => @main) @topscope.resource = @main_resource @resources << @main_resource @catalog.add_resource(@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.each do |name, overrides| remaining += overrides end unless remaining.empty? fail Puppet::ParseError, "Could not find resource(s) %s for overriding" % remaining.collect { |o| o.ref }.join(", ") 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.each do |coll| # We're only interested in the 'resource' collections, # which result from direct calls of 'realize'. Anything # else is allowed not to return resources. # Collect all of them, so we have a useful error. if r = coll.resources if r.is_a?(Array) remaining += r else remaining << r end end end unless remaining.empty? raise Puppet::ParseError, "Failed to realize virtual resources %s" % remaining.join(', ') end end # Make sure all of our resources and such have done any last work # necessary. def finish 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 end # Initialize the top-level scope, class, and resource. def init_main # Create our initial scope and a resource that will evaluate main. - @topscope = Puppet::Parser::Scope.new(:compiler => self, :parser => self.parser) + @topscope = Puppet::Parser::Scope.new(:compiler => self) end # Set up all of our internal variables. def initvars # The list of objects that will available for export. @exported_resources = {} # 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 = [] # For maintaining the relationship between scopes and their resources. @catalog = Puppet::Resource::Catalog.new(@node.name) - @catalog.version = @parser.version + @catalog.version = known_resource_types.version # local resource array to maintain resource ordering @resources = [] # Make sure any external node classes are in our class list @catalog.add_class(*@node.classes) end # Set the node's parameters into the top-scope as variables. def set_node_parameters node.parameters.each do |param, value| @topscope.setvar(param, value) end # These might be nil. catalog.client_version = node.parameters["clientversion"] catalog.server_version = node.parameters["serverversion"] 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 ary = resources.reject { |resource| resource.builtin? or resource.evaluated? } if ary.empty? return nil else return ary end end end diff --git a/lib/puppet/parser/interpreter.rb b/lib/puppet/parser/interpreter.rb index 1b158209d..eea9afcad 100644 --- a/lib/puppet/parser/interpreter.rb +++ b/lib/puppet/parser/interpreter.rb @@ -1,70 +1,69 @@ require 'puppet' require 'timeout' require 'puppet/rails' require 'puppet/util/methodhelper' require 'puppet/parser/parser' require 'puppet/parser/compiler' require 'puppet/parser/scope' # The interpreter is a very simple entry-point class that # manages the existence of the parser (e.g., replacing it # when files are reparsed). You can feed it a node and # get the node's catalog back. class Puppet::Parser::Interpreter include Puppet::Util attr_accessor :usenodes include Puppet::Util::Errors # evaluate our whole tree def compile(node) - raise Puppet::ParseError, "Could not parse configuration; cannot compile on node %s" % node.name unless env_parser = parser(node.environment) begin - return Puppet::Parser::Compiler.new(node, env_parser).compile.to_resource + return Puppet::Parser::Compiler.new(node).compile.to_resource rescue => detail puts detail.backtrace if Puppet[:trace] raise Puppet::Error, detail.to_s + " on node %s" % node.name end end # create our interpreter def initialize @parsers = {} end # Return the parser for a specific environment. def parser(environment) if ! @parsers[environment] or @parsers[environment].reparse? # This will throw an exception if it does not succeed. @parsers[environment] = create_parser(environment) end @parsers[environment] end private # Create a new parser object and pre-parse the configuration. def create_parser(environment) begin parser = Puppet::Parser::Parser.new(environment) if code = Puppet.settings.uninterpolated_value(:code, environment) and code != "" parser.string = code else file = Puppet.settings.value(:manifest, environment) parser.file = file end parser.parse return parser rescue => detail msg = "Could not parse" if environment and environment != "" msg += " for environment %s" % environment end msg += ": %s" % detail.to_s error = Puppet::Error.new(msg) error.set_backtrace(detail.backtrace) raise error end end end diff --git a/lib/puppet/parser/resource/reference.rb b/lib/puppet/parser/resource/reference.rb index 3ef981258..bb50efdce 100644 --- a/lib/puppet/parser/resource/reference.rb +++ b/lib/puppet/parser/resource/reference.rb @@ -1,99 +1,103 @@ # A reference to a resource. Mostly just the type and title. require 'puppet/resource/reference' require 'puppet/file_collection/lookup' require 'puppet/parser/yaml_trimmer' +require 'puppet/parser/resource_type_collection_helper' + # A reference to a resource. Mostly just the type and title. class Puppet::Parser::Resource::Reference < Puppet::Resource::Reference include Puppet::Parser::YamlTrimmer include Puppet::FileCollection::Lookup include Puppet::Util::MethodHelper include Puppet::Util::Errors + include Puppet::Parser::ResourceTypeCollectionHelper attr_accessor :builtin, :file, :line, :scope # Are we a builtin type? def builtin? unless defined? @builtin if builtintype() @builtin = true else @builtin = false end end @builtin end def builtintype if t = Puppet::Type.type(self.type.downcase) and t.name != :component t else nil end end # Return the defined type for our obj. This can return classes, # definitions or nodes. def definedtype unless defined? @definedtype case self.type when "Class" # look for host classes - if self.title == :main - tmp = @scope.find_hostclass("") - else - unless tmp = @scope.parser.hostclass(self.title) - fail Puppet::ParseError, "Could not find class '%s'" % self.title - end + name = self.title == :main ? "" : self.title + unless tmp = known_resource_types.find_hostclass("", name) + fail Puppet::ParseError, "Could not find '#{title}' class" end when "Node" # look for node definitions - unless tmp = @scope.parser.node(self.title) + unless tmp = known_resource_types.node(self.title) fail Puppet::ParseError, "Could not find node '%s'" % self.title end else # normal definitions # The resource type is capitalized, so we have to downcase. Really, # we should have a better interface for finding these, but eh. - tmp = @scope.parser.definition(self.type.downcase) + tmp = known_resource_types.definition(self.type.downcase) end if tmp @definedtype = tmp else fail Puppet::ParseError, "Could not find resource type '%s'" % self.type end end @definedtype end + def environment + scope.environment + end + def initialize(hash) set_options(hash) requiredopts(:type, :title) end def skip_for_yaml %w{@typeclass @definedtype} end def to_ref # We have to return different cases to provide backward compatibility # from 0.24.x to 0.23.x. if builtin? return [type.to_s.downcase, title.to_s] else return [type.to_s, title.to_s] end end def typeclass unless defined? @typeclass if tmp = builtintype || definedtype @typeclass = tmp else fail Puppet::ParseError, "Could not find type %s" % self.type end end @typeclass end end diff --git a/lib/puppet/parser/resource_type.rb b/lib/puppet/parser/resource_type.rb index 3dbcbcb62..c0d7f69ed 100644 --- a/lib/puppet/parser/resource_type.rb +++ b/lib/puppet/parser/resource_type.rb @@ -1,236 +1,237 @@ require 'puppet/parser/parser' require 'puppet/util/warnings' require 'puppet/util/errors' require 'puppet/util/inline_docs' require 'puppet/parser/ast/leaf' class Puppet::Parser::ResourceType include Puppet::Util::InlineDocs include Puppet::Util::Warnings include Puppet::Util::Errors RESOURCE_SUPERTYPES = [:hostclass, :node, :definition] attr_accessor :file, :line, :doc, :code, :parent, :code_collection attr_reader :type, :namespace, :arguments, :behaves_like # Are we a child of the passed class? Do a recursive search up our # parentage tree to figure it out. def child_of?(klass) return false unless parent return true if klass == parent_type return parent_type.child_of?(klass) end # Now evaluate the code associated with this class or definition. def evaluate_code(resource) # Create a new scope. scope = subscope(resource.scope, resource) + scope.compiler.class_set(name, scope) set_resource_parameters(resource, scope) return nil unless c = self.code return c.safeevaluate(scope) end def initialize(type, name, options = {}) @type = type.to_s.downcase.to_sym raise ArgumentError, "Invalid resource supertype '#{type}'" unless RESOURCE_SUPERTYPES.include?(@type) name = convert_from_ast(name) if name.is_a?(Puppet::Parser::AST::HostName) set_name_and_namespace(name) [:code, :doc, :line, :file, :parent].each do |param| next unless value = options[param] send(param.to_s + "=", value) end set_arguments(options[:arguments]) end # This is only used for node names, and really only when the node name # is a regexp. def match(string) return string.to_s.downcase == name unless name_is_regex? return @name =~ string end # Add code from a new instance to our code. def merge(other) fail ArgumentError, "#{name} is not a class; cannot add code to it" unless type == :hostclass fail ArgumentError, "#{other.name} is not a class; cannot add code from it" unless other.type == :hostclass if parent and other.parent and parent != other.parent fail ArgumentError, "Cannot merge classes with different parent classes" end # We know they're either equal or only one is set, so keep whichever parent is specified. self.parent ||= other.parent if other.doc self.doc ||= "" self.doc += other.doc end # This might just be an empty, stub class. return unless other.code unless self.code self.code = other.code return end array_class = Puppet::Parser::AST::ASTArray unless self.code.is_a?(array_class) self.code = array_class.new(:children => [self.code]) end if other.code.is_a?(array_class) code.children += other.code.children else code.children << other.code end end # Make an instance of our resource type. This is only possible # for those classes and nodes that don't have any arguments, and is # only useful for things like the 'include' function. def mk_plain_resource(scope) type == :definition and raise ArgumentError, "Cannot create resources for defined resource types" resource_type = type == :hostclass ? :class : :node # Make sure our parent class has been evaluated, if we have one. if parent and ! scope.catalog.resource(resource_type, parent) parent_type.mk_plain_resource(scope) end # Do nothing if the resource already exists; this makes sure we don't # get multiple copies of the class resource, which helps provide the # singleton nature of classes. if resource = scope.catalog.resource(resource_type, name) return resource end resource = Puppet::Parser::Resource.new(:type => resource_type, :title => name, :scope => scope, :source => self) scope.compiler.add_resource(scope, resource) scope.catalog.tag(*resource.tags) resource end def name return @name unless @name.is_a?(Regexp) return @name.source.downcase.gsub(/[^-\w:.]/,'').sub(/^\.+/,'') end def name_is_regex? @name.is_a?(Regexp) end def parent_type return nil unless parent unless @parent_type ||= code_collection.send(type, parent) fail Puppet::ParseError, "Could not find parent resource type '#{parent}'" end @parent_type end # Set any arguments passed by the resource as variables in the scope. def set_resource_parameters(resource, scope) set = {} resource.to_hash.each do |param, value| param = param.to_sym fail Puppet::ParseError, "#{resource.ref} does not accept attribute #{param}" unless validattr?(param) exceptwrap { scope.setvar(param.to_s, value) } set[param] = true end # Verify that all required arguments are either present or # have been provided with defaults. arguments.each do |param, default| param = param.to_sym next if set.include?(param) # Even if 'default' is a false value, it's an AST value, so this works fine fail Puppet::ParseError, "Must pass #{param} to #{resource.ref}" unless default scope.setvar(param.to_s, default.safeevaluate(scope)) end scope.setvar("title", resource.title) unless set.include? :title scope.setvar("name", resource.name) unless set.include? :name scope.class_set(self.name,scope) end # Create a new subscope in which to evaluate our code. def subscope(scope, resource) scope.newscope :resource => resource, :namespace => self.namespace, :source => self end # Check whether a given argument is valid. def validattr?(param) param = param.to_s return true if param == "name" return true if Puppet::Type.metaparam?(param) return false unless defined?(@arguments) return true if arguments.include?(param) return false end def set_arguments(arguments) @arguments = {} return if arguments.nil? arguments.each do |arg, default| arg = arg.to_s warn_if_metaparam(arg, default) @arguments[arg] = default end end private def convert_from_ast(name) value = name.value if value.is_a?(Puppet::Parser::AST::Regex) name = value.value else name = value end end # Split an fq name into a namespace and name def namesplit(fullname) ary = fullname.split("::") n = ary.pop || "" ns = ary.join("::") return ns, n end def set_name_and_namespace(name) if name.is_a?(Regexp) @name = name @namespace = "" else @name = name.to_s.downcase @namespace, ignored_shortname = namesplit(@name) end end def warn_if_metaparam(param, default) return unless Puppet::Type.metaparamclass(param) if default warnonce "#{param} is a metaparam; this value will inherit to all contained resources" else raise Puppet::ParseError, "#{param} is a metaparameter; please choose another parameter name in the #{self.name} definition" end end end diff --git a/lib/puppet/parser/resource_type_collection_helper.rb b/lib/puppet/parser/resource_type_collection_helper.rb index 4f66c773a..51ccdc061 100644 --- a/lib/puppet/parser/resource_type_collection_helper.rb +++ b/lib/puppet/parser/resource_type_collection_helper.rb @@ -1,5 +1,7 @@ +require 'puppet/parser/resource_type_collection' + module Puppet::Parser::ResourceTypeCollectionHelper def known_resource_types environment.known_resource_types end end diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index c32db357f..1f7fd7188 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -1,451 +1,454 @@ # The scope class, which handles storing and retrieving variables and types and # such. require 'puppet/parser/parser' require 'puppet/parser/templatewrapper' require 'puppet/transportable' require 'strscan' +require 'puppet/parser/resource_type_collection_helper' + class Puppet::Parser::Scope + include Puppet::Parser::ResourceTypeCollectionHelper require 'puppet/parser/resource' AST = Puppet::Parser::AST Puppet::Util.logmethods(self) include Enumerable include Puppet::Util::Errors - attr_accessor :level, :parser, :source, :resource + attr_accessor :level, :source, :resource attr_accessor :base, :keyword, :nodescope attr_accessor :top, :translated, :compiler attr_accessor :parent # A demeterific shortcut to the catalog. def catalog compiler.catalog end + def environment + compiler.environment + end + # Proxy accessors def host @compiler.node.name end - def interpreter - @compiler.interpreter - end - # Is the value true? This allows us to control the definition of truth # in one place. def self.true?(value) if value == false or value == "" or value == :undef return false else return true end end # Is the value a number?, return the correct object or nil if not a number def self.number?(value) unless value.is_a?(Fixnum) or value.is_a?(Bignum) or value.is_a?(Float) or value.is_a?(String) return nil end if value.is_a?(String) if value =~ /^-?\d+(:?\.\d+|(:?\.\d+)?e\d+)$/ return value.to_f elsif value =~ /^0x[0-9a-f]+$/i return value.to_i(16) elsif value =~ /^0[0-7]+$/ return value.to_i(8) elsif value =~ /^-?\d+$/ return value.to_i else return nil end end # it is one of Fixnum,Bignum or Float return value end # Add to our list of namespaces. def add_namespace(ns) return false if @namespaces.include?(ns) if @namespaces == [""] @namespaces = [ns] else @namespaces << ns end end # Are we the top scope? def topscope? @level == 1 end def find_hostclass(name) @namespaces.each do |namespace| - if r = parser.find_hostclass(namespace, name) + if r = known_resource_types.find_hostclass(namespace, name) return r end end return nil end def find_definition(name) @namespaces.each do |namespace| - if r = parser.find_definition(namespace, name) + if r = known_resource_types.find_definition(namespace, name) return r end end return nil end def findresource(string, name = nil) compiler.findresource(string, name) end # Initialize our new scope. Defaults to having no parent. def initialize(hash = {}) if hash.include?(:namespace) if n = hash[:namespace] @namespaces = [n] end hash.delete(:namespace) else @namespaces = [""] end hash.each { |name, val| method = name.to_s + "=" if self.respond_to? method self.send(method, val) else raise Puppet::DevError, "Invalid scope argument %s" % name end } @tags = [] # The symbol table for this scope. This is where we store variables. @symtable = {} # the ephemeral symbol tables # those should not persist long, and are used for the moment only # for $0..$xy capture variables of regexes @ephemeral = {} # All of the defaults set for types. It's a hash of hashes, # with the first key being the type, then the second key being # the parameter. @defaults = Hash.new { |dhash,type| dhash[type] = {} } # The table for storing class singletons. This will only actually # be used by top scopes and node scopes. @class_scopes = {} end # Store the fact that we've evaluated a class, and store a reference to # the scope in which it was evaluated, so that we can look it up later. def class_set(name, scope) return parent.class_set(name,scope) if parent if existing = @class_scopes[name] if existing.nodescope? != scope.nodescope? raise Puppet::ParseError, "Cannot have classes, nodes, or definitions with the same name" else raise Puppet::DevError, "Somehow evaluated %s %s twice" % [ existing.nodescope? ? "node" : "class", name] end end @class_scopes[name] = scope end # Return the scope associated with a class. This is just here so # that subclasses can set their parent scopes to be the scope of # their parent class, and it's also used when looking up qualified # variables. def class_scope(klass) # They might pass in either the class or class name k = klass.respond_to?(:name) ? klass.name : klass @class_scopes[k] || (parent && parent.class_scope(k)) end # Collect all of the defaults set at any higher scopes. # This is a different type of lookup because it's additive -- # it collects all of the defaults, with defaults in closer scopes # overriding those in later scopes. def lookupdefaults(type) values = {} # first collect the values from the parents unless parent.nil? parent.lookupdefaults(type).each { |var,value| values[var] = value } end # then override them with any current values # this should probably be done differently if @defaults.include?(type) @defaults[type].each { |var,value| values[var] = value } end #Puppet.debug "Got defaults for %s: %s" % # [type,values.inspect] return values end # Look up a defined type. def lookuptype(name) find_definition(name) || find_hostclass(name) end def lookup_qualified_var(name, usestring) parts = name.split(/::/) shortname = parts.pop klassname = parts.join("::") klass = find_hostclass(klassname) unless klass warning "Could not look up qualified variable '%s'; class %s could not be found" % [name, klassname] return usestring ? "" : :undefined end unless kscope = class_scope(klass) warning "Could not look up qualified variable '%s'; class %s has not been evaluated" % [name, klassname] return usestring ? "" : :undefined end return kscope.lookupvar(shortname, usestring) end private :lookup_qualified_var # Look up a variable. The simplest value search we do. Default to returning # an empty string for missing values, but support returning a constant. def lookupvar(name, usestring = true) table = ephemeral?(name) ? @ephemeral : @symtable # If the variable is qualified, then find the specified scope and look the variable up there instead. if name =~ /::/ return lookup_qualified_var(name, usestring) end # We can't use "if table[name]" here because the value might be false if table.include?(name) if usestring and table[name] == :undef return "" else return table[name] end elsif self.parent return parent.lookupvar(name, usestring) elsif usestring return "" else return :undefined end end # Return a hash containing our variables and their values, optionally (and # by default) including the values defined in our parent. Local values # shadow parent values. def to_hash(recursive = true) if recursive and parent then target = parent.to_hash(recursive) end target ||= Hash.new @symtable.keys.each { |name| value = @symtable[name] if value == :undef then target.delete(name) else target[name] = value end } return target end def namespaces @namespaces.dup end # Create a new scope and set these options. def newscope(options = {}) compiler.newscope(self, options) end # Is this class for a node? This is used to make sure that # nodes and classes with the same name conflict (#620), which # is required because of how often the names are used throughout # the system, including on the client. def nodescope? self.nodescope end # Return the list of scopes up to the top scope, ordered with our own first. # This is used for looking up variables and defaults. def scope_path if parent [self, parent.scope_path].flatten.compact else [self] end end # Set defaults for a type. The typename should already be downcased, # so that the syntax is isolated. We don't do any kind of type-checking # here; instead we let the resource do it when the defaults are used. def setdefaults(type, params) table = @defaults[type] # if we got a single param, it'll be in its own array params = [params] unless params.is_a?(Array) params.each { |param| #Puppet.debug "Default for %s is %s => %s" % # [type,ary[0].inspect,ary[1].inspect] if table.include?(param.name) raise Puppet::ParseError.new("Default already defined for %s { %s }; cannot redefine" % [type, param.name], param.line, param.file) end table[param.name] = param } end # Set a variable in the current scope. This will override settings # in scopes above, but will not allow variables in the current scope # to be reassigned. def setvar(name,value, options = {}) table = options[:ephemeral] ? @ephemeral : @symtable #Puppet.debug "Setting %s to '%s' at level %s mode append %s" % # [name.inspect,value,self.level, append] if table.include?(name) unless options[:append] error = Puppet::ParseError.new("Cannot reassign variable %s" % name) else error = Puppet::ParseError.new("Cannot append, variable %s is defined in this scope" % name) end if options[:file] error.file = options[:file] end if options[:line] error.line = options[:line] end raise error end unless options[:append] table[name] = value else # append case # lookup the value in the scope if it exists and insert the var table[name] = lookupvar(name) # concatenate if string, append if array, nothing for other types case value when Array table[name] += value when Hash raise ArgumentError, "Trying to append to a hash with something which is not a hash is unsupported" unless value.is_a?(Hash) table[name].merge!(value) else table[name] << value end end end # Return an interpolated string. def strinterp(string, file = nil, line = nil) # Most strings won't have variables in them. ss = StringScanner.new(string) out = "" while not ss.eos? if ss.scan(/^\$\{((\w*::)*\w+|[0-9]+)\}|^\$([0-9])|^\$((\w*::)*\w+)/) # If it matches the backslash, then just retun the dollar sign. if ss.matched == '\\$' out << '$' else # look the variable up # make sure $0-$9 are lookupable only if ephemeral var = ss[1] || ss[3] || ss[4] if var and var =~ /^[0-9]+$/ and not ephemeral?(var) next end out << lookupvar(var).to_s || "" end elsif ss.scan(/^\\(.)/) # Puppet.debug("Got escape: pos:%d; m:%s" % [ss.pos, ss.matched]) case ss[1] when 'n' out << "\n" when 't' out << "\t" when 's' out << " " when '\\' out << '\\' when '$' out << '$' else str = "Unrecognised escape sequence '#{ss.matched}'" if file str += " in file %s" % file end if line str += " at line %s" % line end Puppet.warning str out << ss.matched end elsif ss.scan(/^\$/) out << '$' elsif ss.scan(/^\\\n/) # an escaped carriage return next else tmp = ss.scan(/[^\\$]+/) # Puppet.debug("Got other: pos:%d; m:%s" % [ss.pos, tmp]) unless tmp error = Puppet::ParseError.new("Could not parse string %s" % string.inspect) {:file= => file, :line= => line}.each do |m,v| error.send(m, v) if v end raise error end out << tmp end end return out end # Return the tags associated with this scope. It's basically # just our parents' tags, plus our type. We don't cache this value # because our parent tags might change between calls. def tags resource.tags end # Used mainly for logging def to_s "Scope(%s)" % @resource.to_s end # Undefine a variable; only used for testing. def unsetvar(var) table = ephemeral?(var) ? @ephemeral : @symtable if table.include?(var) table.delete(var) end end def unset_ephemeral_var @ephemeral = {} end def ephemeral?(name) @ephemeral.include?(name) end def ephemeral_from(match, file = nil, line = nil) raise(ArgumentError,"Invalid regex match data") unless match.is_a?(MatchData) setvar("0", match[0], :file => file, :line => line, :ephemeral => true) match.captures.each_with_index do |m,i| setvar("#{i+1}", m, :file => file, :line => line, :ephemeral => true) end end end diff --git a/lib/puppet/parser/templatewrapper.rb b/lib/puppet/parser/templatewrapper.rb index b2eb3c422..61c74e970 100644 --- a/lib/puppet/parser/templatewrapper.rb +++ b/lib/puppet/parser/templatewrapper.rb @@ -1,116 +1,114 @@ # A simple wrapper for templates, so they don't have full access to # the scope objects. require 'puppet/parser/files' class Puppet::Parser::TemplateWrapper attr_writer :scope attr_reader :file attr_accessor :string include Puppet::Util Puppet::Util.logmethods(self) def initialize(scope) @__scope__ = scope end def scope @__scope__ end # Should return true if a variable is defined, false if it is not def has_variable?(name) if scope.lookupvar(name.to_s, false) != :undefined true else false end end # Allow templates to access the defined classes def classes return scope.catalog.classes end # Allow templates to access the tags defined in the current scope def tags return scope.tags end # Allow templates to access the all the defined tags def all_tags return scope.catalog.tags end # Ruby treats variables like methods, so we used to expose variables # within scope to the ERB code via method_missing. As per RedMine #1427, # though, this means that conflicts between methods in our inheritance # tree (Kernel#fork) and variable names (fork => "yes/no") could arise. # # Worse, /new/ conflicts could pop up when a new kernel or object method # was added to Ruby, causing templates to suddenly fail mysteriously when # Ruby was upgraded. # # To ensure that legacy templates using unqualified names work we retain # the missing_method definition here until we declare the syntax finally # dead. def method_missing(name, *args) # We have to tell lookupvar to return :undefined to us when # appropriate; otherwise it converts to "". value = scope.lookupvar(name.to_s, false) if value != :undefined return value else # Just throw an error immediately, instead of searching for # other missingmethod things or whatever. raise Puppet::ParseError, "Could not find value for '%s'" % name end end def file=(filename) - unless @file = Puppet::Parser::Files.find_template(filename, scope.compiler.environment) + unless @file = Puppet::Parser::Files.find_template(filename, scope.compiler.environment.to_s) raise Puppet::ParseError, "Could not find template '%s'" % filename end # We'll only ever not have a parser in testing, but, eh. - if scope.parser - scope.parser.watch_file(file) - end + scope.known_resource_types.watch_file(file) @string = File.read(file) end def result(string = nil) if string self.string = string template_source = "inline template" else template_source = file end # Expose all the variables in our scope as instance variables of the # current object, making it possible to access them without conflict # to the regular methods. benchmark(:debug, "Bound template variables for #{template_source}") do scope.to_hash.each { |name, value| if name.kind_of?(String) realname = name.gsub(/[^\w]/, "_") else realname = name end instance_variable_set("@#{realname}", value) } end result = nil benchmark(:debug, "Interpolated template #{template_source}") do template = ERB.new(self.string, 0, "-") result = template.result(binding) end result end def to_s "template[%s]" % (file ? file : "inline") end end diff --git a/spec/integration/parser/compiler.rb b/spec/integration/parser/compiler.rb index 7ce24f558..512924f6f 100755 --- a/spec/integration/parser/compiler.rb +++ b/spec/integration/parser/compiler.rb @@ -1,29 +1,29 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Parser::Compiler do before :each do @node = Puppet::Node.new "testnode" @scope_resource = stub 'scope_resource', :builtin? => true, :finish => nil, :ref => 'Class[main]' @scope = stub 'scope', :resource => @scope_resource, :source => mock("source") end after do Puppet.settings.clear end it "should be able to determine the configuration version from a local version control repository" do # This should always work, because we should always be # in the puppet repo when we run this. version = %x{git rev-parse HEAD}.chomp Puppet.settings[:config_version] = 'git rev-parse HEAD' @parser = Puppet::Parser::Parser.new "development" - @compiler = Puppet::Parser::Compiler.new(@node, @parser) + @compiler = Puppet::Parser::Compiler.new(@node) @compiler.catalog.version.should == version end end diff --git a/spec/unit/node.rb b/spec/unit/node.rb index 025bdc898..c2350da6b 100755 --- a/spec/unit/node.rb +++ b/spec/unit/node.rb @@ -1,200 +1,192 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' describe Puppet::Node, "when initializing" do before do @node = Puppet::Node.new("testnode") end it "should set the node name" do @node.name.should == "testnode" end it "should not allow nil node names" do proc { Puppet::Node.new(nil) }.should raise_error(ArgumentError) end it "should default to an empty parameter hash" do @node.parameters.should == {} end it "should default to an empty class array" do @node.classes.should == [] end it "should note its creation time" do @node.time.should be_instance_of(Time) end it "should accept parameters passed in during initialization" do params = {"a" => "b"} @node = Puppet::Node.new("testing", :parameters => params) @node.parameters.should == params end it "should accept classes passed in during initialization" do classes = %w{one two} @node = Puppet::Node.new("testing", :classes => classes) @node.classes.should == classes end it "should always return classes as an array" do @node = Puppet::Node.new("testing", :classes => "myclass") @node.classes.should == ["myclass"] end - it "should accept an environment value" do - Puppet.settings.stubs(:value).with(:environment).returns("myenv") - @node = Puppet::Node.new("testing", :environment => "myenv") - @node.environment.should == "myenv" + it "should use any specified environment" do + env = Puppet::Node::Environment.new("foo") + + Puppet::Node.new("testnode", :environment => env).environment.should equal(env) end -end -describe Puppet::Node, "when returning the environment" do - before do - Puppet.settings.stubs(:value).with(:environment).returns("one,two") - Puppet.settings.stubs(:value).with(:environment).returns("one") - @node = Puppet::Node.new("testnode") + it "should convert an environment specified as a string into an Environment instance" do + Puppet::Node.new("testnode", :environment => "foo").environment.should be_instance_of(Puppet::Node::Environment) end - it "should return the 'environment' fact if present and there is no explicit environment" do - @node.parameters = {"environment" => "two"} - @node.environment.should == "two" + it "should return the 'environment' parameter if present and there is no explicit environment" do + Puppet::Node.new("testnode", :parameters => {"environment" => "two"}).environment.name.should == Puppet::Node::Environment.new("two").name end it "should use the default environment if there is no environment fact nor explicit environment" do - env = mock 'environment', :name => :myenv - Puppet::Node::Environment.expects(:new).returns(env) - @node.environment.should == "myenv" + @node.environment.name.should == Puppet::Node::Environment.new.name end end describe Puppet::Node, "when merging facts" do before do @node = Puppet::Node.new("testnode") Puppet::Node::Facts.stubs(:find).with(@node.name).returns(Puppet::Node::Facts.new(@node.name, "one" => "c", "two" => "b")) end it "should fail intelligently if it cannot find facts" do Puppet::Node::Facts.expects(:find).with(@node.name).raises "foo" lambda { @node.fact_merge }.should raise_error(Puppet::Error) end it "should prefer parameters already set on the node over facts from the node" do - @node.parameters = {"one" => "a"} + @node = Puppet::Node.new("testnode", :parameters => {"one" => "a"}) @node.fact_merge @node.parameters["one"].should == "a" end it "should add passed parameters to the parameter list" do - @node.parameters = {"one" => "a"} + @node = Puppet::Node.new("testnode", :parameters => {"one" => "a"}) @node.fact_merge @node.parameters["two"].should == "b" end it "should accept arbitrary parameters to merge into its parameters" do - @node.parameters = {"one" => "a"} + @node = Puppet::Node.new("testnode", :parameters => {"one" => "a"}) @node.merge "two" => "three" @node.parameters["two"].should == "three" end it "should add the environment to the list of parameters" do Puppet.settings.stubs(:value).with(:environments).returns("one,two") Puppet.settings.stubs(:value).with(:environment).returns("one") @node = Puppet::Node.new("testnode", :environment => "one") @node.merge "two" => "three" @node.parameters["environment"].should == "one" end it "should not set the environment if it is already set in the parameters" do Puppet.settings.stubs(:value).with(:environments).returns("one,two") Puppet.settings.stubs(:value).with(:environment).returns("one") @node = Puppet::Node.new("testnode", :environment => "one") @node.merge "environment" => "two" @node.parameters["environment"].should == "two" end end describe Puppet::Node, "when indirecting" do it "should redirect to the indirection" do @indirection = stub 'indirection', :name => :node Puppet::Node.stubs(:indirection).returns(@indirection) @indirection.expects(:find) Puppet::Node.find(:my_node.to_s) end it "should default to the 'plain' node terminus" do Puppet::Node.indirection.terminus_class.should == :plain end it "should not have a cache class defined" do Puppet::Node.indirection.cache_class.should be_nil end after do Puppet::Util::Cacher.expire end end describe Puppet::Node, "when generating the list of names to search through" do before do - @node = Puppet::Node.new("foo.domain.com") - @node.parameters = {"hostname" => "yay", "domain" => "domain.com"} + @node = Puppet::Node.new("foo.domain.com", :parameters => {"hostname" => "yay", "domain" => "domain.com"}) end it "should return an array of names" do @node.names.should be_instance_of(Array) end describe "and the node name is fully qualified" do it "should contain an entry for each part of the node name" do @node.names.should be_include("foo.domain.com") @node.names.should be_include("foo.domain") @node.names.should be_include("foo") end end it "should include the node's fqdn" do @node.names.should be_include("yay.domain.com") end it "should combine and include the node's hostname and domain if no fqdn is available" do @node.names.should be_include("yay.domain.com") end it "should contain an entry for each name available by stripping a segment of the fqdn" do @node.parameters["fqdn"] = "foo.deep.sub.domain.com" @node.names.should be_include("foo.deep.sub.domain") @node.names.should be_include("foo.deep.sub") end describe "and :node_name is set to 'cert'" do before do Puppet.settings.stubs(:value).with(:strict_hostname_checking).returns false Puppet.settings.stubs(:value).with(:node_name).returns "cert" end it "should use the passed-in key as the first value" do @node.names[0].should == "foo.domain.com" end describe "and strict hostname checking is enabled" do it "should only use the passed-in key" do Puppet.settings.expects(:value).with(:strict_hostname_checking).returns true @node.names.should == ["foo.domain.com"] end end end describe "and :node_name is set to 'facter'" do before do Puppet.settings.stubs(:value).with(:strict_hostname_checking).returns false Puppet.settings.stubs(:value).with(:node_name).returns "facter" end it "should use the node's 'hostname' fact as the first value" do @node.names[0].should == "yay" end end end diff --git a/spec/unit/parser/compiler.rb b/spec/unit/parser/compiler.rb index a2b2e4d51..cf73f4d3b 100755 --- a/spec/unit/parser/compiler.rb +++ b/spec/unit/parser/compiler.rb @@ -1,579 +1,589 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' class CompilerTestResource attr_accessor :builtin, :virtual, :evaluated, :type, :title def initialize(type, title) @type = type @title = title end def ref "%s[%s]" % [type.to_s.capitalize, title] end def evaluated? @evaluated end def builtin? @builtin end def virtual? @virtual end def evaluate end end describe Puppet::Parser::Compiler do before :each do @node = Puppet::Node.new "testnode" - @parser = Puppet::Parser::Parser.new "development" + @known_resource_types = Puppet::Parser::ResourceTypeCollection.new "development" @scope_resource = stub 'scope_resource', :builtin? => true, :finish => nil, :ref => 'Class[main]', :type => "class" @scope = stub 'scope', :resource => @scope_resource, :source => mock("source") - @compiler = Puppet::Parser::Compiler.new(@node, @parser) + @compiler = Puppet::Parser::Compiler.new(@node) + @compiler.environment.stubs(:known_resource_types).returns @known_resource_types + end + + it "should use the node's environment as its environment" do + @compiler.environment.should equal(@node.environment) + end + + it "should include the resource type collection helper" do + Puppet::Parser::Compiler.ancestors.should be_include(Puppet::Parser::ResourceTypeCollectionHelper) end it "should be able to return a class list containing all added classes" do @compiler.add_class "" @compiler.add_class "one" @compiler.add_class "two" @compiler.classlist.sort.should == %w{one two}.sort end describe "when initializing" do it "should set its node attribute" do @compiler.node.should equal(@node) end - - it "should set its parser attribute" do - @compiler.parser.should equal(@parser) - end - it "should detect when ast nodes are absent" do @compiler.ast_nodes?.should be_false end it "should detect when ast nodes are present" do - @parser.expects(:nodes?).returns true + @known_resource_types.expects(:nodes?).returns true @compiler.ast_nodes?.should be_true end - it "should copy the parser version to the catalog" do - @compiler.catalog.version.should == @parser.version + it "should copy the known_resource_types version to the catalog" do + @compiler.catalog.version.should == @known_resource_types.version end it "should copy any node classes into the class list" do node = Puppet::Node.new("mynode") node.classes = %w{foo bar} - compiler = Puppet::Parser::Compiler.new(node, @parser) + compiler = Puppet::Parser::Compiler.new(node) compiler.classlist.should include("foo") compiler.classlist.should include("bar") end end describe "when managing scopes" do it "should create a top scope" do @compiler.topscope.should be_instance_of(Puppet::Parser::Scope) end it "should be able to create new scopes" do @compiler.newscope(@compiler.topscope).should be_instance_of(Puppet::Parser::Scope) end it "should correctly set the level of newly created scopes" do @compiler.newscope(@compiler.topscope, :level => 5).level.should == 5 end it "should set the parent scope of the new scope to be the passed-in parent" do scope = mock 'scope' newscope = @compiler.newscope(scope) newscope.parent.should equal(scope) end it "should set the parent scope of the new scope to its topscope if the parent passed in is nil" do scope = mock 'scope' newscope = @compiler.newscope(nil) newscope.parent.should equal(@compiler.topscope) end end describe "when compiling" do def compile_methods [:set_node_parameters, :evaluate_main, :evaluate_ast_node, :evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated, :finish, :store, :extract] end # Stub all of the main compile methods except the ones we're specifically interested in. def compile_stub(*except) (compile_methods - except).each { |m| @compiler.stubs(m) } end it "should set node parameters as variables in the top scope" do params = {"a" => "b", "c" => "d"} @node.stubs(:parameters).returns(params) compile_stub(:set_node_parameters) @compiler.compile @compiler.topscope.lookupvar("a").should == "b" @compiler.topscope.lookupvar("c").should == "d" end it "should set the client and server versions on the catalog" do params = {"clientversion" => "2", "serverversion" => "3"} @node.stubs(:parameters).returns(params) compile_stub(:set_node_parameters) @compiler.compile @compiler.catalog.client_version.should == "2" @compiler.catalog.server_version.should == "3" end it "should evaluate any existing classes named in the node" do classes = %w{one two three four} main = stub 'main' one = stub 'one', :name => "one" three = stub 'three', :name => "three" @node.stubs(:name).returns("whatever") @node.stubs(:classes).returns(classes) @compiler.expects(:evaluate_classes).with(classes, @compiler.topscope) @compiler.class.publicize_methods(:evaluate_node_classes) { @compiler.evaluate_node_classes } end it "should evaluate the main class if it exists" do compile_stub(:evaluate_main) main_class = mock 'main_class' main_class.expects(:evaluate_code).with { |r| r.is_a?(Puppet::Parser::Resource) } @compiler.topscope.expects(:source=).with(main_class) - @parser.stubs(:find_hostclass).with("", "").returns(main_class) + @known_resource_types.stubs(:find_hostclass).with("", "").returns(main_class) @compiler.compile end + it "should create a new, empty 'main' if no main class exists" do + compile_stub(:evaluate_main) + @compiler.compile + @known_resource_types.find_hostclass("", "").should be_instance_of(Puppet::Parser::ResourceType) + end + it "should evaluate any node classes" do @node.stubs(:classes).returns(%w{one two three four}) @compiler.expects(:evaluate_classes).with(%w{one two three four}, @compiler.topscope) @compiler.send(:evaluate_node_classes) end it "should evaluate all added collections" do colls = [] # And when the collections fail to evaluate. colls << mock("coll1-false") colls << mock("coll2-false") colls.each { |c| c.expects(:evaluate).returns(false) } @compiler.add_collection(colls[0]) @compiler.add_collection(colls[1]) compile_stub(:evaluate_generators) @compiler.compile end it "should ignore builtin resources" do resource = stub 'builtin', :ref => "File[testing]", :builtin? => true, :type => "file" @compiler.add_resource(@scope, resource) resource.expects(:evaluate).never @compiler.compile end it "should evaluate unevaluated resources" do resource = CompilerTestResource.new(:file, "testing") @compiler.add_resource(@scope, resource) # We have to now mark the resource as evaluated resource.expects(:evaluate).with { |*whatever| resource.evaluated = true } @compiler.compile end it "should not evaluate already-evaluated resources" do resource = stub 'already_evaluated', :ref => "File[testing]", :builtin? => false, :evaluated? => true, :virtual? => false, :type => "file" @compiler.add_resource(@scope, resource) resource.expects(:evaluate).never @compiler.compile end it "should evaluate unevaluated resources created by evaluating other resources" do resource = CompilerTestResource.new(:file, "testing") @compiler.add_resource(@scope, resource) resource2 = CompilerTestResource.new(:file, "other") # We have to now mark the resource as evaluated resource.expects(:evaluate).with { |*whatever| resource.evaluated = true; @compiler.add_resource(@scope, resource2) } resource2.expects(:evaluate).with { |*whatever| resource2.evaluated = true } @compiler.compile end it "should call finish() on all resources" do # Add a resource that does respond to :finish resource = Puppet::Parser::Resource.new :scope => @scope, :type => "file", :title => "finish" resource.expects(:finish) @compiler.add_resource(@scope, resource) # And one that does not dnf = stub "dnf", :ref => "File[dnf]", :type => "file" @compiler.add_resource(@scope, dnf) @compiler.send(:finish) end it "should call finish() in add_resource order" do resources = sequence('resources') resource1 = Puppet::Parser::Resource.new :scope => @scope, :type => "file", :title => "finish1" resource1.expects(:finish).in_sequence(resources) @compiler.add_resource(@scope, resource1) resource2 = Puppet::Parser::Resource.new :scope => @scope, :type => "file", :title => "finish2" resource2.expects(:finish).in_sequence(resources) @compiler.add_resource(@scope, resource2) @compiler.send(:finish) end it "should return added resources in add order" do resource1 = stub "1", :ref => "File[yay]", :type => "file" @compiler.add_resource(@scope, resource1) resource2 = stub "2", :ref => "File[youpi]", :type => "file" @compiler.add_resource(@scope, resource2) @compiler.resources.should == [resource1, resource2] end it "should add resources that do not conflict with existing resources" do resource = CompilerTestResource.new(:file, "yay") @compiler.add_resource(@scope, resource) @compiler.catalog.should be_vertex(resource) end it "should fail to add resources that conflict with existing resources" do file1 = Puppet::Type.type(:file).new :path => "/foo" file2 = Puppet::Type.type(:file).new :path => "/foo" @compiler.add_resource(@scope, file1) lambda { @compiler.add_resource(@scope, file2) }.should raise_error(Puppet::Resource::Catalog::DuplicateResourceError) end it "should add an edge from the scope resource to the added resource" do resource = stub "noconflict", :ref => "File[yay]", :type => "file" @compiler.add_resource(@scope, resource) @compiler.catalog.should be_edge(@scope.resource, resource) end it "should add edges from the class resources to the main class" do main = CompilerTestResource.new(:class, :main) @compiler.add_resource(@scope, main) resource = CompilerTestResource.new(:class, "foo") @compiler.add_resource(@scope, resource) @compiler.catalog.should be_edge(main, resource) end it "should just add edges to the scope resource for the class resources when no main class can be found" do resource = CompilerTestResource.new(:class, "foo") @compiler.add_resource(@scope, resource) @compiler.catalog.should be_edge(@scope.resource, resource) end it "should have a method for looking up resources" do resource = stub 'resource', :ref => "Yay[foo]", :type => "file" @compiler.add_resource(@scope, resource) @compiler.findresource("Yay[foo]").should equal(resource) end it "should be able to look resources up by type and title" do resource = stub 'resource', :ref => "Yay[foo]", :type => "file" @compiler.add_resource(@scope, resource) @compiler.findresource("Yay", "foo").should equal(resource) end it "should not evaluate virtual defined resources" do resource = stub 'notevaluated', :ref => "File[testing]", :builtin? => false, :evaluated? => false, :virtual? => true, :type => "file" @compiler.add_resource(@scope, resource) resource.expects(:evaluate).never @compiler.compile end end describe "when evaluating collections" do it "should evaluate each collection" do 2.times { |i| coll = mock 'coll%s' % i @compiler.add_collection(coll) # This is the hard part -- we have to emulate the fact that # collections delete themselves if they are done evaluating. coll.expects(:evaluate).with do @compiler.delete_collection(coll) end } @compiler.class.publicize_methods(:evaluate_collections) { @compiler.evaluate_collections } end it "should not fail when there are unevaluated resource collections that do not refer to specific resources" do coll = stub 'coll', :evaluate => false coll.expects(:resources).returns(nil) @compiler.add_collection(coll) lambda { @compiler.compile }.should_not raise_error end it "should fail when there are unevaluated resource collections that refer to a specific resource" do coll = stub 'coll', :evaluate => false coll.expects(:resources).returns(:something) @compiler.add_collection(coll) lambda { @compiler.compile }.should raise_error(Puppet::ParseError) end it "should fail when there are unevaluated resource collections that refer to multiple specific resources" do coll = stub 'coll', :evaluate => false coll.expects(:resources).returns([:one, :two]) @compiler.add_collection(coll) lambda { @compiler.compile }.should raise_error(Puppet::ParseError) end end describe "when told to evaluate missing classes" do it "should fail if there's no source listed for the scope" do scope = stub 'scope', :source => nil proc { @compiler.evaluate_classes(%w{one two}, scope) }.should raise_error(Puppet::DevError) end it "should tag the catalog with the name of each not-found class" do @compiler.catalog.expects(:tag).with("notfound") @scope.expects(:find_hostclass).with("notfound").returns(nil) @compiler.evaluate_classes(%w{notfound}, @scope) end end describe "when evaluating found classes" do before do @class = stub 'class', :name => "my::class" @scope.stubs(:find_hostclass).with("myclass").returns(@class) @resource = stub 'resource', :ref => "Class[myclass]", :type => "file" end it "should evaluate each class" do @compiler.catalog.stubs(:tag) @class.expects(:mk_plain_resource).with(@scope) @scope.stubs(:class_scope).with(@class) @compiler.evaluate_classes(%w{myclass}, @scope) end it "should not evaluate the resources created for found classes unless asked" do @compiler.catalog.stubs(:tag) @resource.expects(:evaluate).never @class.expects(:mk_plain_resource).returns(@resource) @scope.stubs(:class_scope).with(@class) @compiler.evaluate_classes(%w{myclass}, @scope) end it "should immediately evaluate the resources created for found classes when asked" do @compiler.catalog.stubs(:tag) @resource.expects(:evaluate) @class.expects(:mk_plain_resource).returns(@resource) @scope.stubs(:class_scope).with(@class) @compiler.evaluate_classes(%w{myclass}, @scope, false) end it "should skip classes that have already been evaluated" do @compiler.catalog.stubs(:tag) @scope.stubs(:class_scope).with(@class).returns("something") @compiler.expects(:add_resource).never @resource.expects(:evaluate).never Puppet::Parser::Resource.expects(:new).never @compiler.evaluate_classes(%w{myclass}, @scope, false) end it "should skip classes previously evaluated with different capitalization" do @compiler.catalog.stubs(:tag) @scope.stubs(:find_hostclass).with("MyClass").returns(@class) @scope.stubs(:class_scope).with(@class).returns("something") @compiler.expects(:add_resource).never @resource.expects(:evaluate).never Puppet::Parser::Resource.expects(:new).never @compiler.evaluate_classes(%w{MyClass}, @scope, false) end it "should return the list of found classes" do @compiler.catalog.stubs(:tag) @compiler.stubs(:add_resource) @scope.stubs(:find_hostclass).with("notfound").returns(nil) @scope.stubs(:class_scope).with(@class) Puppet::Parser::Resource.stubs(:new).returns(@resource) @class.stubs :mk_plain_resource @compiler.evaluate_classes(%w{myclass notfound}, @scope).should == %w{myclass} end end describe "when evaluating AST nodes with no AST nodes present" do it "should do nothing" do @compiler.expects(:ast_nodes?).returns(false) - @compiler.parser.expects(:nodes).never + @compiler.known_resource_types.expects(:nodes).never Puppet::Parser::Resource.expects(:new).never @compiler.send(:evaluate_ast_node) end end describe "when evaluating AST nodes with AST nodes present" do before do - @compiler.parser.stubs(:nodes?).returns true + @compiler.known_resource_types.stubs(:nodes?).returns true # Set some names for our test @node.stubs(:names).returns(%w{a b c}) - @compiler.parser.stubs(:node).with("a").returns(nil) - @compiler.parser.stubs(:node).with("b").returns(nil) - @compiler.parser.stubs(:node).with("c").returns(nil) + @compiler.known_resource_types.stubs(:node).with("a").returns(nil) + @compiler.known_resource_types.stubs(:node).with("b").returns(nil) + @compiler.known_resource_types.stubs(:node).with("c").returns(nil) # It should check this last, of course. - @compiler.parser.stubs(:node).with("default").returns(nil) + @compiler.known_resource_types.stubs(:node).with("default").returns(nil) end it "should fail if the named node cannot be found" do proc { @compiler.send(:evaluate_ast_node) }.should raise_error(Puppet::ParseError) end it "should evaluate the first node class matching the node name" do node_class = stub 'node', :name => "c", :evaluate_code => nil - @compiler.parser.stubs(:node).with("c").returns(node_class) + @compiler.known_resource_types.stubs(:node).with("c").returns(node_class) node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil, :type => "node" node_class.expects(:mk_plain_resource).returns(node_resource) @compiler.compile end it "should match the default node if no matching node can be found" do node_class = stub 'node', :name => "default", :evaluate_code => nil - @compiler.parser.stubs(:node).with("default").returns(node_class) + @compiler.known_resource_types.stubs(:node).with("default").returns(node_class) node_resource = stub 'node resource', :ref => "Node[default]", :evaluate => nil, :type => "node" node_class.expects(:mk_plain_resource).returns(node_resource) @compiler.compile end it "should evaluate the node resource immediately rather than using lazy evaluation" do node_class = stub 'node', :name => "c" - @compiler.parser.stubs(:node).with("c").returns(node_class) + @compiler.known_resource_types.stubs(:node).with("c").returns(node_class) node_resource = stub 'node resource', :ref => "Node[c]", :type => "node" node_class.expects(:mk_plain_resource).returns(node_resource) node_resource.expects(:evaluate) @compiler.send(:evaluate_ast_node) end it "should set the node's scope as the top scope" do node_resource = stub 'node resource', :ref => "Node[c]", :evaluate => nil, :type => "node" node_class = stub 'node', :name => "c", :mk_plain_resource => node_resource - @compiler.parser.stubs(:node).with("c").returns(node_class) + @compiler.known_resource_types.stubs(:node).with("c").returns(node_class) # The #evaluate method normally does this. scope = stub 'scope', :source => "mysource" @compiler.topscope.expects(:class_scope).with(node_class).returns(scope) node_resource.stubs(:evaluate) @compiler.compile @compiler.topscope.should equal(scope) end end describe "when managing resource overrides" do before do @override = stub 'override', :ref => "My[ref]", :type => "my" @resource = stub 'resource', :ref => "My[ref]", :builtin? => true, :type => "my" end it "should be able to store overrides" do lambda { @compiler.add_override(@override) }.should_not raise_error end it "should apply overrides to the appropriate resources" do @compiler.add_resource(@scope, @resource) @resource.expects(:merge).with(@override) @compiler.add_override(@override) @compiler.compile end it "should accept overrides before the related resource has been created" do @resource.expects(:merge).with(@override) # First store the override @compiler.add_override(@override) # Then the resource @compiler.add_resource(@scope, @resource) # And compile, so they get resolved @compiler.compile end it "should fail if the compile is finished and resource overrides have not been applied" do @compiler.add_override(@override) lambda { @compiler.compile }.should raise_error(Puppet::ParseError) end end end diff --git a/spec/unit/parser/interpreter.rb b/spec/unit/parser/interpreter.rb index 56ad71a4a..99a0a4be8 100755 --- a/spec/unit/parser/interpreter.rb +++ b/spec/unit/parser/interpreter.rb @@ -1,136 +1,128 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Parser::Interpreter do before do @interp = Puppet::Parser::Interpreter.new @parser = mock 'parser' end describe "when creating parser instances" do it "should create a parser with code if there is code defined in the :code setting" do Puppet.settings.stubs(:uninterpolated_value).with(:code, :myenv).returns("mycode") @parser.expects(:string=).with("mycode") @parser.expects(:parse) Puppet::Parser::Parser.expects(:new).with(:myenv).returns(@parser) @interp.send(:create_parser, :myenv).object_id.should equal(@parser.object_id) end it "should create a parser with the main manifest when the code setting is an empty string" do Puppet.settings.stubs(:uninterpolated_value).with(:code, :myenv).returns("") Puppet.settings.stubs(:value).with(:manifest, :myenv).returns("/my/file") @parser.expects(:parse) @parser.expects(:file=).with("/my/file") Puppet::Parser::Parser.expects(:new).with(:myenv).returns(@parser) @interp.send(:create_parser, :myenv).should equal(@parser) end it "should return nothing when new parsers fail" do Puppet::Parser::Parser.expects(:new).with(:myenv).raises(ArgumentError) proc { @interp.send(:create_parser, :myenv) }.should raise_error(Puppet::Error) end it "should create parsers with environment-appropriate manifests" do # Set our per-environment values. We can't just stub :value, because # it's called by too much of the rest of the code. text = "[env1]\nmanifest = /t/env1.pp\n[env2]\nmanifest = /t/env2.pp" FileTest.stubs(:exist?).returns true Puppet.settings.stubs(:read_file).returns(text) Puppet.settings.parse parser1 = mock 'parser1' Puppet::Parser::Parser.expects(:new).with(:env1).returns(parser1) parser1.expects(:file=).with("/t/env1.pp") parser1.expects(:parse) @interp.send(:create_parser, :env1) parser2 = mock 'parser2' Puppet::Parser::Parser.expects(:new).with(:env2).returns(parser2) parser2.expects(:file=).with("/t/env2.pp") parser2.expects(:parse) @interp.send(:create_parser, :env2) end end describe "when managing parser instances" do it "should use the same parser when the parser does not need reparsing" do @interp.expects(:create_parser).with(:myenv).returns(@parser) @interp.send(:parser, :myenv).should equal(@parser) @parser.expects(:reparse?).returns(false) @interp.send(:parser, :myenv).should equal(@parser) end it "should fail intelligently if a parser cannot be created and one does not already exist" do @interp.expects(:create_parser).with(:myenv).raises(ArgumentError) proc { @interp.send(:parser, :myenv) }.should raise_error(ArgumentError) end it "should use different parsers for different environments" do # get one for the first env @interp.expects(:create_parser).with(:first_env).returns(@parser) @interp.send(:parser, :first_env).should equal(@parser) other_parser = mock('otherparser') @interp.expects(:create_parser).with(:second_env).returns(other_parser) @interp.send(:parser, :second_env).should equal(other_parser) end describe "when files need reparsing" do it "should create a new parser" do oldparser = mock('oldparser') newparser = mock('newparser') oldparser.expects(:reparse?).returns(true) @interp.expects(:create_parser).with(:myenv).returns(oldparser) @interp.send(:parser, :myenv).should equal(oldparser) @interp.expects(:create_parser).with(:myenv).returns(newparser) @interp.send(:parser, :myenv).should equal(newparser) end it "should raise an exception if a new parser cannot be created" do # Get the first parser in the hash. @interp.expects(:create_parser).with(:myenv).returns(@parser) @interp.send(:parser, :myenv).should equal(@parser) @parser.expects(:reparse?).returns(true) @interp.expects(:create_parser).with(:myenv).raises(Puppet::Error, "Could not parse") lambda { @interp.parser(:myenv) }.should raise_error(Puppet::Error) end end end describe "when compiling a catalog" do before do - @node = stub 'node', :environment => :myenv + @node = Puppet::Node.new("foo") @compiler = mock 'compile' end - it "should create a compile with the node and parser" do + it "should create a compile with the node" do catalog = stub 'catalog', :to_resource => nil @compiler.expects(:compile).returns(catalog) - @interp.expects(:parser).with(:myenv).returns(@parser) - Puppet::Parser::Compiler.expects(:new).with(@node, @parser).returns(@compiler) + Puppet::Parser::Compiler.expects(:new).with(@node).returns(@compiler) @interp.compile(@node) end - it "should fail intelligently when no parser can be found" do - @node.stubs(:name).returns("whatever") - @interp.expects(:parser).with(:myenv).returns(nil) - proc { @interp.compile(@node) }.should raise_error(Puppet::ParseError) - end - it "should return the results of the compile, converted to a plain resource catalog" do catalog = mock 'catalog' @compiler.expects(:compile).returns(catalog) - @interp.stubs(:parser).returns(@parser) Puppet::Parser::Compiler.stubs(:new).returns(@compiler) catalog.expects(:to_resource).returns "my_resource_catalog" @interp.compile(@node).should == "my_resource_catalog" end end end diff --git a/spec/unit/parser/resource.rb b/spec/unit/parser/resource.rb index 2b57f7898..eb2cd490c 100755 --- a/spec/unit/parser/resource.rb +++ b/spec/unit/parser/resource.rb @@ -1,521 +1,534 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' # LAK: FIXME This is just new tests for resources; I have # not moved all tests over yet. describe Puppet::Parser::Resource do before do - @parser = Puppet::Parser::Parser.new :Code => "" - @source = @parser.newclass "" @node = Puppet::Node.new("yaynode") - @compiler = Puppet::Parser::Compiler.new(@node, @parser) + @known_resource_types = Puppet::Parser::ResourceTypeCollection.new("env") + @compiler = Puppet::Parser::Compiler.new(@node) + @compiler.environment.stubs(:known_resource_types).returns @known_resource_types + @source = newclass "" @scope = @compiler.topscope end def mkresource(args = {}) args[:source] ||= "source" args[:scope] ||= stub('scope', :source => mock('source')) {:type => "resource", :title => "testing", :source => "source", :scope => "scope"}.each do |param, value| args[param] ||= value end params = args[:params] || {:one => "yay", :three => "rah"} if args[:params] == :none args.delete(:params) elsif not args[:params].is_a? Array args[:params] = paramify(args[:source], params) end Puppet::Parser::Resource.new(args) end def param(name, value, source) Puppet::Parser::Resource::Param.new(:name => name, :value => value, :source => source) end def paramify(source, hash) hash.collect do |name, value| Puppet::Parser::Resource::Param.new( :name => name, :value => value, :source => source ) end end + def newclass(name) + @known_resource_types.add Puppet::Parser::ResourceType.new(:hostclass, name) + end + + def newdefine(name) + @known_resource_types.add Puppet::Parser::ResourceType.new(:definition, name) + end + + def newnode(name) + @known_resource_types.add Puppet::Parser::ResourceType.new(:node, name) + end + it "should use the file lookup module" do Puppet::Parser::Resource.ancestors.should be_include(Puppet::FileCollection::Lookup) end it "should be isomorphic if it is builtin and models an isomorphic type" do Puppet::Type.type(:file).expects(:isomorphic?).returns(true) @resource = Puppet::Parser::Resource.new(:type => "file", :title => "whatever", :scope => @scope, :source => @source).isomorphic?.should be_true end it "should not be isomorphic if it is builtin and models a non-isomorphic type" do Puppet::Type.type(:file).expects(:isomorphic?).returns(false) @resource = Puppet::Parser::Resource.new(:type => "file", :title => "whatever", :scope => @scope, :source => @source).isomorphic?.should be_false end it "should be isomorphic if it is not builtin" do - @parser.newdefine "whatever" + newdefine "whatever" @resource = Puppet::Parser::Resource.new(:type => "whatever", :title => "whatever", :scope => @scope, :source => @source).isomorphic?.should be_true end it "should have a array-indexing method for retrieving parameter values" do @resource = mkresource @resource[:one].should == "yay" end it "should use a Puppet::Resource for converting to a ral resource" do trans = mock 'resource', :to_ral => "yay" @resource = mkresource @resource.expects(:to_resource).returns trans @resource.to_ral.should == "yay" end it "should be able to use the indexing operator to access parameters" do resource = Puppet::Parser::Resource.new(:type => "resource", :title => "testing", :source => "source", :scope => "scope") resource["foo"] = "bar" resource["foo"].should == "bar" end it "should return the title when asked for a parameter named 'title'" do Puppet::Parser::Resource.new(:type => "resource", :title => "testing", :source => "source", :scope => "scope")[:title].should == "testing" end describe "when initializing" do before do @arguments = {:type => "resource", :title => "testing", :scope => stub('scope', :source => mock('source'))} end [:type, :title, :scope].each do |name| it "should fail unless #{name.to_s} is specified" do try = @arguments.dup try.delete(name) lambda { Puppet::Parser::Resource.new(try) }.should raise_error(ArgumentError) end end it "should set the reference correctly" do res = Puppet::Parser::Resource.new(@arguments) res.ref.should == "Resource[testing]" end it "should be tagged with user tags" do tags = [ "tag1", "tag2" ] @arguments[:params] = [ param(:tag, tags , :source) ] res = Puppet::Parser::Resource.new(@arguments) (res.tags & tags).should == tags end end describe "when refering to a resource with name canonicalization" do before do @arguments = {:type => "file", :title => "/path/", :scope => stub('scope', :source => mock('source'))} end it "should canonicalize its own name" do res = Puppet::Parser::Resource.new(@arguments) res.ref.should == "File[/path]" end end describe "when evaluating" do before do @type = Puppet::Parser::Resource - @definition = @parser.newdefine "mydefine" - @class = @parser.newclass "myclass" - @nodedef = @parser.newnode("mynode")[0] + @definition = newdefine "mydefine" + @class = newclass "myclass" + @nodedef = newnode("mynode") end it "should evaluate the associated AST definition" do res = @type.new(:type => "mydefine", :title => "whatever", :scope => @scope, :source => @source) @definition.expects(:evaluate_code).with(res) res.evaluate end it "should evaluate the associated AST class" do res = @type.new(:type => "class", :title => "myclass", :scope => @scope, :source => @source) @class.expects(:evaluate_code).with(res) res.evaluate end it "should evaluate the associated AST node" do res = @type.new(:type => "node", :title => "mynode", :scope => @scope, :source => @source) @nodedef.expects(:evaluate_code).with(res) res.evaluate end end describe "when finishing" do before do - @class = @parser.newclass "myclass" - @nodedef = @parser.newnode("mynode")[0] + @class = newclass "myclass" + @nodedef = newnode("mynode") @resource = Puppet::Parser::Resource.new(:type => "file", :title => "whatever", :scope => @scope, :source => @source) end it "should do nothing if it has already been finished" do @resource.finish @resource.expects(:add_metaparams).never @resource.finish end it "should add all defaults available from the scope" do @resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => param(:owner, "default", @resource.source)) @resource.finish @resource[:owner].should == "default" end it "should not replace existing parameters with defaults" do @resource.set_parameter :owner, "oldvalue" @resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => :replaced) @resource.finish @resource[:owner].should == "oldvalue" end it "should add a copy of each default, rather than the actual default parameter instance" do newparam = param(:owner, "default", @resource.source) other = newparam.dup other.value = "other" newparam.expects(:dup).returns(other) @resource.scope.expects(:lookupdefaults).with(@resource.type).returns(:owner => newparam) @resource.finish @resource[:owner].should == "other" end it "should be running in metaparam compatibility mode if running a version below 0.25" do catalog = stub 'catalog', :client_version => "0.24.8" @resource.stubs(:catalog).returns catalog @resource.should be_metaparam_compatibility_mode end it "should be running in metaparam compatibility mode if running no client version is available" do catalog = stub 'catalog', :client_version => nil @resource.stubs(:catalog).returns catalog @resource.should be_metaparam_compatibility_mode end it "should not be running in metaparam compatibility mode if running a version at or above 0.25" do catalog = stub 'catalog', :client_version => "0.25.0" @resource.stubs(:catalog).returns catalog @resource.should_not be_metaparam_compatibility_mode end it "should copy metaparams from its scope" do @scope.setvar("noop", "true") @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["noop"].should == "true" end it "should not copy metaparams that it already has" do @resource.set_parameter("noop", "false") @scope.setvar("noop", "true") @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["noop"].should == "false" end it "should not copy relationship metaparams when not in metaparam compatibility mode" do @scope.setvar("require", "bar") @resource.stubs(:metaparam_compatibility_mode?).returns false @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["require"].should be_nil end it "should copy relationship metaparams when in metaparam compatibility mode" do @scope.setvar("require", "bar") @resource.stubs(:metaparam_compatibility_mode?).returns true @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["require"].should == "bar" end it "should stack relationship metaparams when in metaparam compatibility mode" do @resource.set_parameter("require", "foo") @scope.setvar("require", "bar") @resource.stubs(:metaparam_compatibility_mode?).returns true @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["require"].should == ["foo", "bar"] end it "should copy all metaparams that it finds" do @scope.setvar("noop", "foo") @scope.setvar("schedule", "bar") @resource.class.publicize_methods(:add_metaparams) { @resource.add_metaparams } @resource["noop"].should == "foo" @resource["schedule"].should == "bar" end it "should add any tags from the scope resource" do scope_resource = stub 'scope_resource', :tags => %w{one two} @scope.stubs(:resource).returns(scope_resource) @resource.class.publicize_methods(:add_scope_tags) { @resource.add_scope_tags } @resource.tags.should be_include("one") @resource.tags.should be_include("two") end end describe "when being tagged" do before do @scope_resource = stub 'scope_resource', :tags => %w{srone srtwo} @scope = stub 'scope', :resource => @scope_resource @resource = Puppet::Parser::Resource.new(:type => "file", :title => "yay", :scope => @scope, :source => mock('source')) end it "should get tagged with the resource type" do @resource.tags.should be_include("file") end it "should get tagged with the title" do @resource.tags.should be_include("yay") end it "should get tagged with each name in the title if the title is a qualified class name" do resource = Puppet::Parser::Resource.new(:type => "file", :title => "one::two", :scope => @scope, :source => mock('source')) resource.tags.should be_include("one") resource.tags.should be_include("two") end it "should get tagged with each name in the type if the type is a qualified class name" do resource = Puppet::Parser::Resource.new(:type => "one::two", :title => "whatever", :scope => @scope, :source => mock('source')) resource.tags.should be_include("one") resource.tags.should be_include("two") end it "should not get tagged with non-alphanumeric titles" do resource = Puppet::Parser::Resource.new(:type => "file", :title => "this is a test", :scope => @scope, :source => mock('source')) resource.tags.should_not be_include("this is a test") end it "should fail on tags containing '*' characters" do lambda { @resource.tag("bad*tag") }.should raise_error(Puppet::ParseError) end it "should fail on tags starting with '-' characters" do lambda { @resource.tag("-badtag") }.should raise_error(Puppet::ParseError) end it "should fail on tags containing ' ' characters" do lambda { @resource.tag("bad tag") }.should raise_error(Puppet::ParseError) end it "should allow alpha tags" do lambda { @resource.tag("good_tag") }.should_not raise_error(Puppet::ParseError) end end describe "when merging overrides" do before do @source = "source1" @resource = mkresource :source => @source @override = mkresource :source => @source end it "should fail when the override was not created by a parent class" do @override.source = "source2" @override.source.expects(:child_of?).with("source1").returns(false) lambda { @resource.merge(@override) }.should raise_error(Puppet::ParseError) end it "should succeed when the override was created in the current scope" do @resource.source = "source3" @override.source = @resource.source @override.source.expects(:child_of?).with("source3").never params = {:a => :b, :c => :d} @override.expects(:params).returns(params) @resource.expects(:override_parameter).with(:b) @resource.expects(:override_parameter).with(:d) @resource.merge(@override) end it "should succeed when a parent class created the override" do @resource.source = "source3" @override.source = "source4" @override.source.expects(:child_of?).with("source3").returns(true) params = {:a => :b, :c => :d} @override.expects(:params).returns(params) @resource.expects(:override_parameter).with(:b) @resource.expects(:override_parameter).with(:d) @resource.merge(@override) end it "should add new parameters when the parameter is not set" do @source.stubs(:child_of?).returns true @override.set_parameter(:testing, "value") @resource.merge(@override) @resource[:testing].should == "value" end it "should replace existing parameter values" do @source.stubs(:child_of?).returns true @resource.set_parameter(:testing, "old") @override.set_parameter(:testing, "value") @resource.merge(@override) @resource[:testing].should == "value" end it "should add values to the parameter when the override was created with the '+>' syntax" do @source.stubs(:child_of?).returns true param = Puppet::Parser::Resource::Param.new(:name => :testing, :value => "testing", :source => @resource.source) param.add = true @override.set_parameter(param) @resource.set_parameter(:testing, "other") @resource.merge(@override) @resource[:testing].should == %w{other testing} end it "should promote tag overrides to real tags" do @source.stubs(:child_of?).returns true param = Puppet::Parser::Resource::Param.new(:name => :tag, :value => "testing", :source => @resource.source) @override.set_parameter(param) @resource.merge(@override) @resource.tagged?("testing").should be_true end end it "should be able to be converted to a normal resource" do @source = stub 'scope', :name => "myscope" @resource = mkresource :source => @source @resource.should respond_to(:to_resource) end it "should use its resource converter to convert to a transportable resource" do @source = stub 'scope', :name => "myscope" @resource = mkresource :source => @source newresource = Puppet::Resource.new(:file, "/my") Puppet::Resource.expects(:new).returns(newresource) newresource.expects(:to_trans).returns "mytrans" @resource.to_trans.should == "mytrans" end it "should return nil if converted to a transportable resource and it is virtual" do @source = stub 'scope', :name => "myscope" @resource = mkresource :source => @source @resource.expects(:virtual?).returns true @resource.to_trans.should be_nil end describe "when being converted to a resource" do before do @source = stub 'scope', :name => "myscope" @parser_resource = mkresource :source => @source, :params => {:foo => "bar", :fee => "fum"} end it "should create an instance of Puppet::Resource" do @parser_resource.to_resource.should be_instance_of(Puppet::Resource) end it "should set the type correctly on the Puppet::Resource" do @parser_resource.to_resource.type.should == @parser_resource.type end it "should set the title correctly on the Puppet::Resource" do @parser_resource.to_resource.title.should == @parser_resource.title end it "should copy over all of the parameters" do result = @parser_resource.to_resource.to_hash # The name will be in here, also. result[:foo].should == "bar" result[:fee].should == "fum" end it "should copy over the tags" do @parser_resource.tag "foo" @parser_resource.tag "bar" @parser_resource.to_resource.tags.should == @parser_resource.tags end it "should copy over the line" do @parser_resource.line = 40 @parser_resource.to_resource.line.should == 40 end it "should copy over the file" do @parser_resource.file = "/my/file" @parser_resource.to_resource.file.should == "/my/file" end it "should copy over the 'exported' value" do @parser_resource.exported = true @parser_resource.to_resource.exported.should be_true end it "should copy over the 'virtual' value" do @parser_resource.virtual = true @parser_resource.to_resource.virtual.should be_true end it "should convert any parser resource references to Puppet::Resource::Reference instances" do ref = Puppet::Parser::Resource::Reference.new(:title => "/my/file", :type => "file") @parser_resource = mkresource :source => @source, :params => {:foo => "bar", :fee => ref} result = @parser_resource.to_resource result[:fee].should == Puppet::Resource::Reference.new(:file, "/my/file") end it "should convert any parser resource references to Puppet::Resource::Reference instances even if they are in an array" do ref = Puppet::Parser::Resource::Reference.new(:title => "/my/file", :type => "file") @parser_resource = mkresource :source => @source, :params => {:foo => "bar", :fee => ["a", ref]} result = @parser_resource.to_resource result[:fee].should == ["a", Puppet::Resource::Reference.new(:file, "/my/file")] end it "should convert any parser resource references to Puppet::Resource::Reference instances even if they are in an array of array, and even deeper" do ref1 = Puppet::Parser::Resource::Reference.new(:title => "/my/file1", :type => "file") ref2 = Puppet::Parser::Resource::Reference.new(:title => "/my/file2", :type => "file") @parser_resource = mkresource :source => @source, :params => {:foo => "bar", :fee => ["a", [ref1,ref2]]} result = @parser_resource.to_resource result[:fee].should == ["a", Puppet::Resource::Reference.new(:file, "/my/file1"), Puppet::Resource::Reference.new(:file, "/my/file2")] end it "should fail if the same param is declared twice" do lambda do @parser_resource = mkresource :source => @source, :params => [ Puppet::Parser::Resource::Param.new( :name => :foo, :value => "bar", :source => @source ), Puppet::Parser::Resource::Param.new( :name => :foo, :value => "baz", :source => @source ) ] end.should raise_error(Puppet::ParseError) end end end diff --git a/spec/unit/parser/resource/reference.rb b/spec/unit/parser/resource/reference.rb index a09c436c2..1034dbdda 100755 --- a/spec/unit/parser/resource/reference.rb +++ b/spec/unit/parser/resource/reference.rb @@ -1,111 +1,134 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' describe Puppet::Parser::Resource::Reference do before do @type = Puppet::Parser::Resource::Reference end + it "should get its environment from its scope" do + env = stub 'environment' + scope = stub 'scope', :environment => env + @type.new(:title => "foo", :type => "bar", :scope => scope).environment.should equal(env) + end + + it "should use the resource type collection helper to find its known resource types" do + Puppet::Parser::Resource::Reference.ancestors.should include(Puppet::Parser::ResourceTypeCollectionHelper) + end + it "should use the file lookup module" do Puppet::Parser::Resource::Reference.ancestors.should be_include(Puppet::FileCollection::Lookup) end it "should require a type" do proc { @type.new(:title => "yay") }.should raise_error(Puppet::DevError) end it "should require a title" do proc { @type.new(:type => "file") }.should raise_error(Puppet::DevError) end it "should know when it refers to a builtin type" do ref = @type.new(:type => "file", :title => "/tmp/yay") ref.builtin?.should be_true ref.builtintype.should equal(Puppet::Type.type(:file)) end it "should return a downcased relationship-style resource reference for defined types" do ref = @type.new(:type => "file", :title => "/tmp/yay") ref.to_ref.should == ["file", "/tmp/yay"] end it "should return a capitalized relationship-style resource reference for defined types" do ref = @type.new(:type => "whatever", :title => "/tmp/yay") ref.to_ref.should == ["Whatever", "/tmp/yay"] end it "should return a resource reference string when asked" do ref = @type.new(:type => "file", :title => "/tmp/yay") ref.to_s.should == "File[/tmp/yay]" end it "should canonize resource reference types" do ref = @type.new(:type => "foo::bar", :title => "/tmp/yay") ref.to_s.should == "Foo::Bar[/tmp/yay]" end it "should canonize resource reference values" do ref = @type.new(:type => "file", :title => "/tmp/yay/") ref.to_s.should == "File[/tmp/yay]" end it "should canonize resource reference values without order dependencies" do args = [[:title, "/tmp/yay/"], [:type, "file"]] ref = @type.new(args) ref.to_s.should == "File[/tmp/yay]" end end describe Puppet::Parser::Resource::Reference, " when modeling defined types" do + def newclass(name) + @known_resource_types.add Puppet::Parser::ResourceType.new(:hostclass, name) + end + + def newdefine(name) + @known_resource_types.add Puppet::Parser::ResourceType.new(:definition, name) + end + + def newnode(name) + @known_resource_types.add Puppet::Parser::ResourceType.new(:node, name) + end + before do @type = Puppet::Parser::Resource::Reference - @parser = Puppet::Parser::Parser.new :Code => "" - @definition = @parser.newdefine "mydefine" - @class = @parser.newclass "myclass" - @nodedef = @parser.newnode("mynode")[0] + @known_resource_types = Puppet::Parser::ResourceTypeCollection.new("myenv") + @definition = newdefine("mydefine") + @class = newclass("myclass") + @nodedef = newnode("mynode") @node = Puppet::Node.new("yaynode") - @compiler = Puppet::Parser::Compiler.new(@node, @parser) + @compiler = Puppet::Parser::Compiler.new(@node) + @compiler.environment.stubs(:known_resource_types).returns @known_resource_types end it "should be able to find defined types" do ref = @type.new(:type => "mydefine", :title => "/tmp/yay", :scope => @compiler.topscope) ref.builtin?.should be_false ref.definedtype.should equal(@definition) end it "should be able to find classes" do ref = @type.new(:type => "class", :title => "myclass", :scope => @compiler.topscope) ref.builtin?.should be_false ref.definedtype.should equal(@class) end it "should be able to find nodes" do ref = @type.new(:type => "node", :title => "mynode", :scope => @compiler.topscope) ref.builtin?.should be_false ref.definedtype.object_id.should == @nodedef.object_id end it "should only look for fully qualified classes" do - top = @parser.newclass "top" - sub = @parser.newclass "other::top" + top = newclass "top" + sub = newclass "other::top" - scope = @compiler.topscope.class.new(:parent => @compiler.topscope, :namespace => "other", :parser => @parser) + scope = @compiler.topscope.class.new(:parent => @compiler.topscope, :namespace => "other", :compiler => @compiler) ref = @type.new(:type => "class", :title => "top", :scope => scope) ref.definedtype.name.should equal(top.name) end it "should only look for fully qualified definitions" do - top = @parser.newdefine "top" - sub = @parser.newdefine "other::top" + top = newdefine "top" + sub = newdefine "other::top" - scope = @compiler.topscope.class.new(:parent => @compiler.topscope, :namespace => "other", :parser => @parser) + scope = @compiler.topscope.class.new(:parent => @compiler.topscope, :namespace => "other", :compiler => @compiler) ref = @type.new(:type => "top", :title => "foo", :scope => scope) ref.definedtype.name.should equal(top.name) end end diff --git a/spec/unit/parser/resource_type.rb b/spec/unit/parser/resource_type.rb index 8a8bfb5de..816f86176 100755 --- a/spec/unit/parser/resource_type.rb +++ b/spec/unit/parser/resource_type.rb @@ -1,527 +1,533 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/parser/resource_type' describe Puppet::Parser::ResourceType do it "should have a 'name' attribute" do Puppet::Parser::ResourceType.new(:hostclass, "foo").name.should == "foo" end [:code, :doc, :line, :file, :code_collection].each do |attr| it "should have a '#{attr}' attribute" do type = Puppet::Parser::ResourceType.new(:hostclass, "foo") type.send(attr.to_s + "=", "yay") type.send(attr).should == "yay" end end describe "when a node" do it "should allow a regex as its name" do lambda { Puppet::Parser::ResourceType.new(:node, /foo/) }.should_not raise_error end it "should allow a AST::HostName instance as its name" do regex = Puppet::Parser::AST::Regex.new(:value => /foo/) name = Puppet::Parser::AST::HostName.new(:value => regex) lambda { Puppet::Parser::ResourceType.new(:node, name) }.should_not raise_error end it "should match against the regexp in the AST::HostName when a HostName instance is provided" do regex = Puppet::Parser::AST::Regex.new(:value => /\w/) name = Puppet::Parser::AST::HostName.new(:value => regex) node = Puppet::Parser::ResourceType.new(:node, name) node.match("foo").should be_true end it "should return the value of the hostname if provided a string-form AST::HostName instance as the name" do name = Puppet::Parser::AST::HostName.new(:value => "foo") node = Puppet::Parser::ResourceType.new(:node, name) node.name.should == "foo" end describe "and the name is a regex" do it "should have a method that indicates that this is the case" do Puppet::Parser::ResourceType.new(:node, /w/).should be_name_is_regex end it "should set its namespace to ''" do Puppet::Parser::ResourceType.new(:node, /w/).namespace.should == "" end it "should return the regex converted to a string when asked for its name" do Puppet::Parser::ResourceType.new(:node, /ww/).name.should == "ww" end it "should downcase the regex when returning the name as a string" do Puppet::Parser::ResourceType.new(:node, /W/).name.should == "w" end it "should remove non-alpha characters when returning the name as a string" do Puppet::Parser::ResourceType.new(:node, /w*w/).name.should_not include("*") end it "should remove leading dots when returning the name as a string" do Puppet::Parser::ResourceType.new(:node, /.ww/).name.should_not =~ /^\./ end it "should have a method for matching its regex name against a provided name" do Puppet::Parser::ResourceType.new(:node, /.ww/).should respond_to(:match) end it "should return true when its regex matches the provided name" do Puppet::Parser::ResourceType.new(:node, /\w/).match("foo").should be_true end it "should return false when its regex does not match the provided name" do (!!Puppet::Parser::ResourceType.new(:node, /\d/).match("foo")).should be_false end it "should return true when its name, as a string, is matched against an equal string" do Puppet::Parser::ResourceType.new(:node, "foo").match("foo").should be_true end it "should return false when its name is matched against an unequal string" do Puppet::Parser::ResourceType.new(:node, "foo").match("bar").should be_false end it "should match names insensitive to case" do Puppet::Parser::ResourceType.new(:node, "fOo").match("foO").should be_true end end it "should return the name converted to a string when the name is not a regex" do pending "Need to define ResourceTypeCollection behaviour first" name = Puppet::Parser::AST::HostName.new(:value => "foo") Puppet::Parser::ResourceType.new(:node, name).name.should == "foo" end it "should return the name converted to a string when the name is a regex" do pending "Need to define ResourceTypeCollection behaviour first" name = Puppet::Parser::AST::HostName.new(:value => /regex/) Puppet::Parser::ResourceType.new(:node, name).name.should == /regex/.to_s end it "should mark any created scopes as a node scope" do pending "Need to define ResourceTypeCollection behaviour first" name = Puppet::Parser::AST::HostName.new(:value => /regex/) Puppet::Parser::ResourceType.new(:node, name).name.should == /regex/.to_s end end describe "when initializing" do it "should require a resource super type" do Puppet::Parser::ResourceType.new(:hostclass, "foo").type.should == :hostclass end it "should fail if provided an invalid resource super type" do lambda { Puppet::Parser::ResourceType.new(:nope, "foo") }.should raise_error(ArgumentError) end it "should set its name to the downcased, stringified provided name" do Puppet::Parser::ResourceType.new(:hostclass, "Foo::Bar".intern).name.should == "foo::bar" end it "should set its namespace to the downcased, stringified qualified portion of the name" do Puppet::Parser::ResourceType.new(:hostclass, "Foo::Bar::Baz".intern).namespace.should == "foo::bar" end %w{code line file doc}.each do |arg| it "should set #{arg} if provided" do type = Puppet::Parser::ResourceType.new(:hostclass, "foo", arg.to_sym => "something") type.send(arg).should == "something" end end it "should set any provided arguments with the keys as symbols" do type = Puppet::Parser::ResourceType.new(:hostclass, "foo", :arguments => {:foo => "bar", :baz => "biz"}) type.should be_validattr("foo") type.should be_validattr("baz") end it "should set any provided arguments with they keys as strings" do type = Puppet::Parser::ResourceType.new(:hostclass, "foo", :arguments => {"foo" => "bar", "baz" => "biz"}) type.should be_validattr(:foo) type.should be_validattr(:baz) end it "should function if provided no arguments" do type = Puppet::Parser::ResourceType.new(:hostclass, "foo") type.should_not be_validattr(:foo) end end describe "when testing the validity of an attribute" do it "should return true if the parameter was typed at initialization" do Puppet::Parser::ResourceType.new(:hostclass, "foo", :arguments => {"foo" => "bar"}).should be_validattr("foo") end it "should return true if it is a metaparam" do Puppet::Parser::ResourceType.new(:hostclass, "foo").should be_validattr("require") end it "should return true if the parameter is named 'name'" do Puppet::Parser::ResourceType.new(:hostclass, "foo").should be_validattr("name") end it "should return false if it is not a metaparam and was not provided at initialization" do Puppet::Parser::ResourceType.new(:hostclass, "foo").should_not be_validattr("yayness") end end describe "when creating a subscope" do before do @scope = stub 'scope', :newscope => nil @resource = stub 'resource' @type = Puppet::Parser::ResourceType.new(:hostclass, "foo") end it "should return a new scope created with the provided scope as the parent" do @scope.expects(:newscope).returns "foo" @type.subscope(@scope, @resource).should == "foo" end it "should set the source as itself" do @scope.expects(:newscope).with { |args| args[:source] == @type } @type.subscope(@scope, @resource) end it "should set the scope's namespace to its namespace" do @type.expects(:namespace).returns "yayness" @scope.expects(:newscope).with { |args| args[:namespace] == "yayness" } @type.subscope(@scope, @resource) end it "should set the scope's resource to the provided resource" do @scope.expects(:newscope).with { |args| args[:resource] == @resource } @type.subscope(@scope, @resource) end end describe "when setting its parameters in the scope" do before do @scope = stub 'scope', :newscope => nil, :setvar => nil @resource = stub 'resource', :title => "yay", :name => "yea", :ref => "Foo[bar]" @type = Puppet::Parser::ResourceType.new(:hostclass, "foo") end it "should set each of the resource's parameters as variables in the scope" do @type.set_arguments :foo => nil, :boo => nil @resource.expects(:to_hash).returns(:foo => "bar", :boo => "baz") @scope.expects(:setvar).with("foo", "bar") @scope.expects(:setvar).with("boo", "baz") @scope.stubs(:class_set).with("foo",@scope) @type.set_resource_parameters(@resource, @scope) end it "should set the variables as strings" do @type.set_arguments :foo => nil @resource.expects(:to_hash).returns(:foo => "bar") @scope.expects(:setvar).with("foo", "bar") @scope.stubs(:class_set).with("foo",@scope) @type.set_resource_parameters(@resource, @scope) end it "should fail if any of the resource's parameters are not valid attributes" do @type.set_arguments :foo => nil @resource.expects(:to_hash).returns(:boo => "baz") lambda { @type.set_resource_parameters(@resource, @scope) }.should raise_error(Puppet::ParseError) end it "should evaluate and set its default values as variables for parameters not provided by the resource" do @type.set_arguments :foo => stub("value", :safeevaluate => "something") @resource.expects(:to_hash).returns({}) @scope.expects(:setvar).with("foo", "something") @scope.stubs(:class_set).with("foo",@scope) @type.set_resource_parameters(@resource, @scope) end it "should fail if the resource does not provide a value for a required argument" do @type.set_arguments :foo => nil @resource.expects(:to_hash).returns({}) lambda { @type.set_resource_parameters(@resource, @scope) }.should raise_error(Puppet::ParseError) end it "should set the resource's title as a variable if not otherwise provided" do @resource.expects(:to_hash).returns({}) @resource.expects(:title).returns 'teetle' @scope.expects(:setvar).with("title", "teetle") @scope.stubs(:class_set).with("foo",@scope) @type.set_resource_parameters(@resource, @scope) end it "should set the resource's name as a variable if not otherwise provided" do @resource.expects(:to_hash).returns({}) @resource.expects(:name).returns 'nombre' @scope.expects(:setvar).with("name", "nombre") @scope.stubs(:class_set).with("foo",@scope) @type.set_resource_parameters(@resource, @scope) end end describe "when describing and managing parent classes" do before do @code = Puppet::Parser::ResourceTypeCollection.new("env") @parent = Puppet::Parser::ResourceType.new(:hostclass, "bar") @code.add @parent @child = Puppet::Parser::ResourceType.new(:hostclass, "foo", :parent => "bar") @code.add @child end it "should be able to define a parent" do Puppet::Parser::ResourceType.new(:hostclass, "foo", :parent => "bar") end it "should use the code collection to find the parent resource type" do @child.parent_type.should equal(@parent) end it "should be able to find parent nodes" do parent = Puppet::Parser::ResourceType.new(:node, "bar") @code.add parent child = Puppet::Parser::ResourceType.new(:node, "foo", :parent => "bar") @code.add child child.parent_type.should equal(parent) end it "should cache a reference to the parent type" do @code.expects(:hostclass).once.with("bar").returns @parent @child.parent_type @child.parent_type end it "should correctly state when it is another type's child" do @child.should be_child_of(@parent) end it "should be considered the child of a parent's parent" do @grandchild = Puppet::Parser::ResourceType.new(:hostclass, "baz", :parent => "foo") @code.add @grandchild @grandchild.should be_child_of(@parent) end it "should correctly state when it is not another type's child" do @notchild = Puppet::Parser::ResourceType.new(:hostclass, "baz") @code.add @notchild @notchild.should_not be_child_of(@parent) end end describe "when evaluating its code" do before do - @scope = stub 'scope', :newscope => nil, :setvar => nil + @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) + @scope = Puppet::Parser::Scope.new :compiler => @compiler @resource = stub 'resource', :title => "yay", :name => "yea", :ref => "Foo[bar]", :scope => @scope @type = Puppet::Parser::ResourceType.new(:hostclass, "foo") @type.stubs(:set_resource_parameters) end it "should set all of its parameters in a subscope" do - subscope = stub 'subscope' + subscope = stub 'subscope', :compiler => @compiler @type.expects(:subscope).with(@scope, @resource).returns subscope @type.expects(:set_resource_parameters).with(@resource, subscope) @type.evaluate_code(@resource) end + it "should store the class scope" do + subscope = stub 'subscope', :compiler => @compiler + @type.expects(:subscope).with(@scope, @resource).returns subscope + + @type.evaluate_code(@resource) + @compiler.class_scope(@type).should equal(subscope) + end + it "should evaluate the code if any is provided" do code = stub 'code' - @type.expects(:code).returns code - @type.stubs(:subscope).returns stub("subscope") + @type.stubs(:code).returns code + @type.stubs(:subscope).returns stub("subscope", :compiler => @compiler) code.expects(:safeevaluate).with @type.subscope @type.evaluate_code(@resource) end it "should noop if there is no code" do @type.expects(:code).returns nil - @type.stubs(:subscope).returns stub("subscope") @type.evaluate_code(@resource) end end describe "when creating a resource" do before do - @catalog = Puppet::Resource::Catalog.new - @node = stub 'node', :name => "foo", :classes => [] - @compiler = Puppet::Parser::Compiler.new(@node, @catalog) - @scope = Puppet::Parser::Scope.new - @scope.stubs(:compiler).returns @compiler + @node = Puppet::Node.new("foo") + @compiler = Puppet::Parser::Compiler.new(@node) + @scope = Puppet::Parser::Scope.new(:compiler => @compiler) @top = Puppet::Parser::ResourceType.new :hostclass, "top" @middle = Puppet::Parser::ResourceType.new :hostclass, "middle", :parent => "top" @code = Puppet::Parser::ResourceTypeCollection.new("env") @code.add @top @code.add @middle end it "should create a resource instance" do @top.mk_plain_resource(@scope).should be_instance_of(Puppet::Parser::Resource) end it "should set its resource type to 'class' when it is a hostclass" do Puppet::Parser::ResourceType.new(:hostclass, "top").mk_plain_resource(@scope).type.should == "Class" end it "should set its resource type to 'node' when it is a node" do Puppet::Parser::ResourceType.new(:node, "top").mk_plain_resource(@scope).type.should == "Node" end it "should fail when it is a definition" do lambda { Puppet::Parser::ResourceType.new(:definition, "top").mk_plain_resource(@scope) }.should raise_error(ArgumentError) end it "should add the created resource to the scope's catalog" do @top.mk_plain_resource(@scope) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should evaluate the parent class if one exists" do @middle.mk_plain_resource(@scope) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should fail to evaluate if a parent class is defined but cannot be found" do othertop = Puppet::Parser::ResourceType.new :hostclass, "something", :parent => "yay" @code.add othertop lambda { othertop.mk_plain_resource(@scope) }.should raise_error(Puppet::ParseError) end it "should not create a new resource if one already exists" do @compiler.catalog.expects(:resource).with(:class, "top").returns("something") @compiler.catalog.expects(:add_resource).never @top.mk_plain_resource(@scope) end it "should return the existing resource when not creating a new one" do @compiler.catalog.expects(:resource).with(:class, "top").returns("something") @compiler.catalog.expects(:add_resource).never @top.mk_plain_resource(@scope).should == "something" end it "should not create a new parent resource if one already exists and it has a parent class" do @top.mk_plain_resource(@scope) top_resource = @compiler.catalog.resource(:class, "top") @middle.mk_plain_resource(@scope) @compiler.catalog.resource(:class, "top").should equal(top_resource) end # #795 - tag before evaluation. it "should tag the catalog with the resource tags when it is evaluated" do @middle.mk_plain_resource(@scope) @compiler.catalog.should be_tagged("middle") end it "should tag the catalog with the parent class tags when it is evaluated" do @middle.mk_plain_resource(@scope) @compiler.catalog.should be_tagged("top") end end describe "when merging code from another instance" do def code(str) Puppet::Parser::AST::Leaf.new :value => str end it "should fail unless it is a class" do lambda { Puppet::Parser::ResourceType.new(:node, "bar").merge("foo") }.should raise_error(ArgumentError) end it "should fail unless the source instance is a class" do dest = Puppet::Parser::ResourceType.new(:hostclass, "bar") source = Puppet::Parser::ResourceType.new(:node, "foo") lambda { dest.merge(source) }.should raise_error(ArgumentError) end it "should fail if both classes have different parent classes" do code = Puppet::Parser::ResourceTypeCollection.new("env") {"a" => "b", "c" => "d"}.each do |parent, child| code.add Puppet::Parser::ResourceType.new(:hostclass, parent) code.add Puppet::Parser::ResourceType.new(:hostclass, child, :parent => parent) end lambda { code.hostclass("b").merge(code.hostclass("d")) }.should raise_error(ArgumentError) end it "should copy the other class's parent if it has not parent" do dest = Puppet::Parser::ResourceType.new(:hostclass, "bar") parent = Puppet::Parser::ResourceType.new(:hostclass, "parent") source = Puppet::Parser::ResourceType.new(:hostclass, "foo", :parent => "parent") dest.merge(source) dest.parent.should == "parent" end it "should copy the other class's documentation as its docs if it has no docs" do dest = Puppet::Parser::ResourceType.new(:hostclass, "bar") source = Puppet::Parser::ResourceType.new(:hostclass, "foo", :doc => "yayness") dest.merge(source) dest.doc.should == "yayness" end it "should append the other class's docs to its docs if it has any" do dest = Puppet::Parser::ResourceType.new(:hostclass, "bar", :doc => "fooness") source = Puppet::Parser::ResourceType.new(:hostclass, "foo", :doc => "yayness") dest.merge(source) dest.doc.should == "foonessyayness" end it "should turn its code into an ASTArray if necessary" do dest = Puppet::Parser::ResourceType.new(:hostclass, "bar", :code => code("foo")) source = Puppet::Parser::ResourceType.new(:hostclass, "foo", :code => code("bar")) dest.merge(source) dest.code.should be_instance_of(Puppet::Parser::AST::ASTArray) end it "should set the other class's code as its code if it has none" do dest = Puppet::Parser::ResourceType.new(:hostclass, "bar") source = Puppet::Parser::ResourceType.new(:hostclass, "foo", :code => code("bar")) dest.merge(source) dest.code.value.should == "bar" end it "should append the other class's code to its code if it has any" do dcode = Puppet::Parser::AST::ASTArray.new :children => [code("dest")] dest = Puppet::Parser::ResourceType.new(:hostclass, "bar", :code => dcode) scode = Puppet::Parser::AST::ASTArray.new :children => [code("source")] source = Puppet::Parser::ResourceType.new(:hostclass, "foo", :code => scode) dest.merge(source) dest.code.children.collect { |l| l.value }.should == %w{dest source} end end end diff --git a/spec/unit/parser/scope.rb b/spec/unit/parser/scope.rb index ba5f80670..b48bd1246 100755 --- a/spec/unit/parser/scope.rb +++ b/spec/unit/parser/scope.rb @@ -1,346 +1,360 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Parser::Scope do before :each do @topscope = Puppet::Parser::Scope.new() # This is necessary so we don't try to use the compiler to discover our parent. @topscope.parent = nil @scope = Puppet::Parser::Scope.new() @scope.parent = @topscope end it "should be able to store references to class scopes" do lambda { @scope.class_set "myname", "myscope" }.should_not raise_error end it "should be able to retrieve class scopes by name" do @scope.class_set "myname", "myscope" @scope.class_scope("myname").should == "myscope" end it "should be able to retrieve class scopes by object" do klass = mock 'ast_class' klass.expects(:name).returns("myname") @scope.class_set "myname", "myscope" @scope.class_scope(klass).should == "myscope" end # #620 - Nodes and classes should conflict, else classes don't get evaluated describe "when evaluating nodes and classes with the same name (#620)" do before do @node = stub :nodescope? => true @class = stub :nodescope? => false end it "should fail if a node already exists with the same name as the class being evaluated" do @scope.class_set("one", @node) lambda { @scope.class_set("one", @class) }.should raise_error(Puppet::ParseError) end it "should fail if a class already exists with the same name as the node being evaluated" do @scope.class_set("one", @class) lambda { @scope.class_set("one", @node) }.should raise_error(Puppet::ParseError) end end + it "should get its environment from its compiler" do + env = stub 'environment' + compiler = stub 'compiler', :environment => env + scope = Puppet::Parser::Scope.new :compiler => compiler + scope.environment.should equal(env) + end + + it "should use the resource type collection helper to find its known resource types" do + Puppet::Parser::Scope.ancestors.should include(Puppet::Parser::ResourceTypeCollectionHelper) + end + describe "when looking up a variable" do it "should default to an empty string" do @scope.lookupvar("var").should == "" end it "should return an string when asked for a string" do @scope.lookupvar("var", true).should == "" end it "should return ':undefined' for unset variables when asked not to return a string" do @scope.lookupvar("var", false).should == :undefined end it "should be able to look up values" do @scope.setvar("var", "yep") @scope.lookupvar("var").should == "yep" end it "should be able to look up hashes" do @scope.setvar("var", {"a" => "b"}) @scope.lookupvar("var").should == {"a" => "b"} end it "should be able to look up variables in parent scopes" do @topscope.setvar("var", "parentval") @scope.lookupvar("var").should == "parentval" end it "should prefer its own values to parent values" do @topscope.setvar("var", "parentval") @scope.setvar("var", "childval") @scope.lookupvar("var").should == "childval" end describe "and the variable is qualified" do before do - @parser = Puppet::Parser::Parser.new() - @compiler = Puppet::Parser::Compiler.new(stub("node", :name => "foonode", :classes => []), @parser) + @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("foonode")) @scope.compiler = @compiler - @scope.parser = @parser + @known_resource_types = @scope.known_resource_types + end + + def newclass(name) + @known_resource_types.add Puppet::Parser::ResourceType.new(:hostclass, name) end def create_class_scope(name) - klass = @parser.newclass(name) + klass = newclass(name) Puppet::Parser::Resource.new(:type => "class", :title => name, :scope => @scope, :source => mock('source')).evaluate return @scope.class_scope(klass) end it "should be able to look up explicitly fully qualified variables from main" do other_scope = create_class_scope("") other_scope.setvar("othervar", "otherval") @scope.lookupvar("::othervar").should == "otherval" end it "should be able to look up explicitly fully qualified variables from other scopes" do other_scope = create_class_scope("other") other_scope.setvar("var", "otherval") @scope.lookupvar("::other::var").should == "otherval" end it "should be able to look up deeply qualified variables" do other_scope = create_class_scope("other::deep::klass") other_scope.setvar("var", "otherval") @scope.lookupvar("other::deep::klass::var").should == "otherval" end it "should return an empty string for qualified variables that cannot be found in other classes" do other_scope = create_class_scope("other::deep::klass") @scope.lookupvar("other::deep::klass::var").should == "" end it "should warn and return an empty string for qualified variables whose classes have not been evaluated" do - klass = @parser.newclass("other::deep::klass") + klass = newclass("other::deep::klass") @scope.expects(:warning) @scope.lookupvar("other::deep::klass::var").should == "" end it "should warn and return an empty string for qualified variables whose classes do not exist" do @scope.expects(:warning) @scope.lookupvar("other::deep::klass::var").should == "" end it "should return ':undefined' when asked for a non-string qualified variable from a class that does not exist" do @scope.stubs(:warning) @scope.lookupvar("other::deep::klass::var", false).should == :undefined end it "should return ':undefined' when asked for a non-string qualified variable from a class that has not been evaluated" do @scope.stubs(:warning) - klass = @parser.newclass("other::deep::klass") + klass = newclass("other::deep::klass") @scope.lookupvar("other::deep::klass::var", false).should == :undefined end end end describe "when setvar is called with append=true" do it "should raise error if the variable is already defined in this scope" do @scope.setvar("var","1", :append => false) lambda { @scope.setvar("var","1", :append => true) }.should raise_error(Puppet::ParseError) end it "should lookup current variable value" do @scope.expects(:lookupvar).with("var").returns("2") @scope.setvar("var","1", :append => true) end it "should store the concatenated string '42'" do @topscope.setvar("var","4", :append => false) @scope.setvar("var","2", :append => true) @scope.lookupvar("var").should == "42" end it "should store the concatenated array [4,2]" do @topscope.setvar("var",[4], :append => false) @scope.setvar("var",[2], :append => true) @scope.lookupvar("var").should == [4,2] end it "should store the merged hash {a => b, c => d}" do @topscope.setvar("var",{"a" => "b"}, :append => false) @scope.setvar("var",{"c" => "d"}, :append => true) @scope.lookupvar("var").should == {"a" => "b", "c" => "d"} end it "should raise an error when appending a hash with something other than another hash" do @topscope.setvar("var",{"a" => "b"}, :append => false) lambda { @scope.setvar("var","not a hash", :append => true) }.should raise_error end end describe "when calling number?" do it "should return nil if called with anything not a number" do Puppet::Parser::Scope.number?([2]).should be_nil end it "should return a Fixnum for a Fixnum" do Puppet::Parser::Scope.number?(2).should be_an_instance_of(Fixnum) end it "should return a Float for a Float" do Puppet::Parser::Scope.number?(2.34).should be_an_instance_of(Float) end it "should return 234 for '234'" do Puppet::Parser::Scope.number?("234").should == 234 end it "should return nil for 'not a number'" do Puppet::Parser::Scope.number?("not a number").should be_nil end it "should return 23.4 for '23.4'" do Puppet::Parser::Scope.number?("23.4").should == 23.4 end it "should return 23.4e13 for '23.4e13'" do Puppet::Parser::Scope.number?("23.4e13").should == 23.4e13 end it "should understand negative numbers" do Puppet::Parser::Scope.number?("-234").should == -234 end it "should know how to convert exponential float numbers ala '23e13'" do Puppet::Parser::Scope.number?("23e13").should == 23e13 end it "should understand hexadecimal numbers" do Puppet::Parser::Scope.number?("0x234").should == 0x234 end it "should understand octal numbers" do Puppet::Parser::Scope.number?("0755").should == 0755 end it "should return nil on malformed integers" do Puppet::Parser::Scope.number?("0.24.5").should be_nil end it "should convert strings with leading 0 to integer if they are not octal" do Puppet::Parser::Scope.number?("0788").should == 788 end it "should convert strings of negative integers" do Puppet::Parser::Scope.number?("-0788").should == -788 end it "should return nil on malformed hexadecimal numbers" do Puppet::Parser::Scope.number?("0x89g").should be_nil end end describe "when using ephemeral variables" do it "should store the variable value" do @scope.setvar("1", :value, :ephemeral => true) @scope.lookupvar("1").should == :value end it "should remove the variable value when unset_ephemeral_var is called" do @scope.setvar("1", :value, :ephemeral => true) @scope.stubs(:parent).returns(nil) @scope.unset_ephemeral_var @scope.lookupvar("1", false).should == :undefined end it "should not remove classic variables when unset_ephemeral_var is called" do @scope.setvar("myvar", :value1) @scope.setvar("1", :value2, :ephemeral => true) @scope.stubs(:parent).returns(nil) @scope.unset_ephemeral_var @scope.lookupvar("myvar", false).should == :value1 end end describe "when interpolating string" do (0..9).each do |n| it "should allow $#{n} to match" do @scope.setvar(n.to_s, "value", :ephemeral => true) @scope.strinterp("$#{n}").should == "value" end end (0..9).each do |n| it "should not allow $#{n} to match if not ephemeral" do @scope.setvar(n.to_s, "value", :ephemeral => false) @scope.strinterp("$#{n}").should_not == "value" end end it "should not allow $10 to match" do @scope.setvar("10", "value", :ephemeral => true) @scope.strinterp('==$10==').should_not == "==value==" end it "should not allow ${10} to match" do @scope.setvar("10", "value", :ephemeral => true) @scope.strinterp('==${10}==').should == "==value==" end end describe "when setting ephemeral vars from matches" do before :each do @match = stub 'match', :is_a? => true @match.stubs(:[]).with(0).returns("this is a string") @match.stubs(:captures).returns([]) @scope.stubs(:setvar) end it "should accept only MatchData" do lambda { @scope.ephemeral_from("match") }.should raise_error end it "should set $0 with the full match" do @scope.expects(:setvar).with { |*arg| arg[0] == "0" and arg[1] == "this is a string" and arg[2][:ephemeral] } @scope.ephemeral_from(@match) end it "should set every capture as ephemeral var" do @match.stubs(:captures).returns([:capture1,:capture2]) @scope.expects(:setvar).with { |*arg| arg[0] == "1" and arg[1] == :capture1 and arg[2][:ephemeral] } @scope.expects(:setvar).with { |*arg| arg[0] == "2" and arg[1] == :capture2 and arg[2][:ephemeral] } @scope.ephemeral_from(@match) end end describe "when unsetting variables" do it "should be able to unset normal variables" do @scope.setvar("foo", "bar") @scope.unsetvar("foo") @scope.lookupvar("foo").should == "" end it "should be able to unset ephemeral variables" do @scope.setvar("foo", "bar", :ephemeral => true) @scope.unsetvar("foo") @scope.lookupvar("foo").should == "" end end end diff --git a/spec/unit/parser/templatewrapper.rb b/spec/unit/parser/templatewrapper.rb index a72595279..b1f9d2ad9 100755 --- a/spec/unit/parser/templatewrapper.rb +++ b/spec/unit/parser/templatewrapper.rb @@ -1,134 +1,144 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Parser::TemplateWrapper do before(:each) do - compiler = stub('compiler', :environment => "foo") - parser = stub('parser', :watch_file => true) - @scope = stub('scope', :compiler => compiler, :parser => parser, :to_hash => {}) + @known_resource_types = Puppet::Parser::ResourceTypeCollection.new("env") + @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) + @compiler.environment.stubs(:known_resource_types).returns @known_resource_types + @scope = Puppet::Parser::Scope.new :compiler => @compiler + @file = "fake_template" Puppet::Parser::Files.stubs(:find_template).returns("/tmp/fake_template") FileTest.stubs(:exists?).returns("true") File.stubs(:read).with("/tmp/fake_template").returns("template content") @tw = Puppet::Parser::TemplateWrapper.new(@scope) end it "should create a new object TemplateWrapper from a scope" do tw = Puppet::Parser::TemplateWrapper.new(@scope) tw.should be_a_kind_of(Puppet::Parser::TemplateWrapper) end it "should check template file existance and read its content" do - Puppet::Parser::Files.expects(:find_template).with("fake_template", "foo").returns("/tmp/fake_template") + Puppet::Parser::Files.expects(:find_template).with("fake_template", @scope.environment.to_s).returns("/tmp/fake_template") File.expects(:read).with("/tmp/fake_template").returns("template content") @tw.file = @file end + it "should mark the file for watching" do + Puppet::Parser::Files.expects(:find_template).returns("/tmp/fake_template") + File.stubs(:read) + + @known_resource_types.expects(:watch_file).with("/tmp/fake_template") + @tw.file = @file + end + it "should fail if a template cannot be found" do - Puppet::Parser::Files.expects(:find_template).with("fake_template", "foo").returns nil + Puppet::Parser::Files.expects(:find_template).returns nil lambda { @tw.file = @file }.should raise_error(Puppet::ParseError) end it "should turn into a string like template[name] for file based template" do @tw.file = @file @tw.to_s.should eql("template[/tmp/fake_template]") end it "should turn into a string like template[inline] for string-based template" do @tw.to_s.should eql("template[inline]") end it "should return the processed template contents with a call to result" do template_mock = mock("template", :result => "woot!") File.expects(:read).with("/tmp/fake_template").returns("template contents") ERB.expects(:new).with("template contents", 0, "-").returns(template_mock) @tw.file = @file @tw.result.should eql("woot!") end it "should return the processed template contents with a call to result and a string" do template_mock = mock("template", :result => "woot!") ERB.expects(:new).with("template contents", 0, "-").returns(template_mock) @tw.result("template contents").should eql("woot!") end it "should return the contents of a variable if called via method_missing" do @scope.expects(:lookupvar).with("chicken", false).returns("is good") tw = Puppet::Parser::TemplateWrapper.new(@scope) tw.chicken.should eql("is good") end it "should throw an exception if a variable is called via method_missing and it does not exist" do @scope.expects(:lookupvar).with("chicken", false).returns(:undefined) tw = Puppet::Parser::TemplateWrapper.new(@scope) lambda { tw.chicken }.should raise_error(Puppet::ParseError) end it "should allow you to check whether a variable is defined with has_variable?" do @scope.expects(:lookupvar).with("chicken", false).returns("is good") tw = Puppet::Parser::TemplateWrapper.new(@scope) tw.has_variable?("chicken").should eql(true) end it "should allow you to check whether a variable is not defined with has_variable?" do @scope.expects(:lookupvar).with("chicken", false).returns(:undefined) tw = Puppet::Parser::TemplateWrapper.new(@scope) tw.has_variable?("chicken").should eql(false) end it "should allow you to retrieve the defined classes with classes" do catalog = mock 'catalog', :classes => ["class1", "class2"] @scope.expects(:catalog).returns( catalog ) tw = Puppet::Parser::TemplateWrapper.new(@scope) tw.classes().should == ["class1", "class2"] end it "should allow you to retrieve all the tags with all_tags" do catalog = mock 'catalog', :tags => ["tag1", "tag2"] @scope.expects(:catalog).returns( catalog ) tw = Puppet::Parser::TemplateWrapper.new(@scope) tw.all_tags().should == ["tag1","tag2"] end it "should allow you to retrieve the tags defined in the current scope" do @scope.expects(:tags).returns( ["tag1", "tag2"] ) tw = Puppet::Parser::TemplateWrapper.new(@scope) tw.tags().should == ["tag1","tag2"] end it "should set all of the scope's variables as instance variables" do template_mock = mock("template", :result => "woot!") ERB.expects(:new).with("template contents", 0, "-").returns(template_mock) @scope.expects(:to_hash).returns("one" => "foo") @tw.result("template contents") @tw.instance_variable_get("@one").should == "foo" end it "should not error out if one of the variables is a symbol" do template_mock = mock("template", :result => "woot!") ERB.expects(:new).with("template contents", 0, "-").returns(template_mock) @scope.expects(:to_hash).returns(:_timestamp => "1234") @tw.result("template contents") end %w{! . ; :}.each do |badchar| it "should translate #{badchar} to _ when setting the instance variables" do template_mock = mock("template", :result => "woot!") ERB.expects(:new).with("template contents", 0, "-").returns(template_mock) @scope.expects(:to_hash).returns("one#{badchar}" => "foo") @tw.result("template contents") @tw.instance_variable_get("@one_").should == "foo" end end end