diff --git a/lib/puppet/parser/ast/definition.rb b/lib/puppet/parser/ast/definition.rb index cd59da8af..c350c2cdb 100644 --- a/lib/puppet/parser/ast/definition.rb +++ b/lib/puppet/parser/ast/definition.rb @@ -1,216 +1,224 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # Evaluate the stored parse tree for a given component. This will # receive the arguments passed to the component and also the type and # name of the component. class Definition < AST::Branch include Puppet::Util include Puppet::Util::Warnings include Puppet::Util::MethodHelper class << self attr_accessor :name end # The class name @name = :definition attr_accessor :classname, :arguments, :code, :scope, :keyword attr_accessor :exported, :namespace, :parser, :virtual # These are retrieved when looking up the superclass attr_accessor :name attr_reader :parentclass def child_of?(klass) false end def evaluate(options) origscope = options[:scope] resource = options[:resource] # Create a new scope. - scope = subscope(origscope, resource.title) + scope = subscope(origscope, resource) scope.virtual = true if resource.virtual or origscope.virtual? scope.exported = true if resource.exported or origscope.exported? # Additionally, add a tag for whatever kind of class # we are if @classname != "" and ! @classname.nil? @classname.split(/::/).each { |tag| scope.tag(tag) } end [resource.name, resource.title].each do |str| unless str.nil? or str =~ /[^\w]/ or str == "" scope.tag(str) end end set_resource_parameters(scope, resource) if self.code return self.code.safeevaluate(:scope => scope) else return nil end end def initialize(hash = {}) @arguments = nil @parentclass = nil super # Convert the arguments to a hash for ease of later use. if @arguments unless @arguments.is_a? Array @arguments = [@arguments] end oldargs = @arguments @arguments = {} oldargs.each do |arg, val| @arguments[arg] = val end else @arguments = {} end # Deal with metaparams in the argument list. @arguments.each do |arg, defvalue| next unless Puppet::Type.metaparamclass(arg) if defvalue warnonce "%s is a metaparam; this value will inherit to all contained resources" % arg else raise Puppet::ParseError, "%s is a metaparameter; please choose another name" % name end end end def find_parentclass @parser.findclass(namespace, parentclass) end # Set our parent class, with a little check to avoid some potential # weirdness. def parentclass=(name) if name == self.classname parsefail "Parent classes must have dissimilar names" end @parentclass = name end # Hunt down our class object. def parentobj if @parentclass # Cache our result, since it should never change. unless defined?(@parentobj) unless tmp = find_parentclass parsefail "Could not find %s %s" % [self.class.name, @parentclass] end if tmp == self parsefail "Parent classes must have dissimilar names" end @parentobj = tmp end @parentobj else nil end end # Create a new subscope in which to evaluate our code. - def subscope(scope, name = nil) + def subscope(scope, resource = nil) + if resource + type = resource.type + else + type = self.classname + end args = { - :type => self.classname, + :resource => resource, + :type => type, :keyword => self.keyword, - :namespace => self.namespace + :namespace => self.namespace, + :source => self } - args[:name] = name if name + args[:name] = resource.title if resource + oldscope = scope scope = scope.newscope(args) scope.source = self return scope end def to_s classname end # Check whether a given argument is valid. Searches up through # any parent classes that might exist. def validattr?(param) param = param.to_s if @arguments.include?(param) # It's a valid arg for us return true elsif param == "name" return true # elsif defined? @parentclass and @parentclass # # Else, check any existing parent # if parent = @scope.lookuptype(@parentclass) and parent != [] # return parent.validarg?(param) # elsif builtin = Puppet::Type.type(@parentclass) # return builtin.validattr?(param) # else # raise Puppet::Error, "Could not find parent class %s" % # @parentclass # end elsif Puppet::Type.metaparam?(param) return true else # Or just return false return false end end private # Set any arguments passed by the resource as variables in the scope. def set_resource_parameters(scope, resource) args = symbolize_options(resource.to_hash || {}) # Verify that all required arguments are either present or # have been provided with defaults. if self.arguments self.arguments.each { |arg, default| arg = symbolize(arg) unless args.include?(arg) if defined? default and ! default.nil? default = default.safeevaluate :scope => scope args[arg] = default #Puppet.debug "Got default %s for %s in %s" % # [default.inspect, arg.inspect, @name.inspect] else parsefail "Must pass %s to %s of type %s" % [arg,title,@classname] end end } end # Set each of the provided arguments as variables in the # definition's scope. args.each { |arg,value| unless validattr?(arg) parsefail "%s does not accept attribute %s" % [@classname, arg] end exceptwrap do scope.setvar(arg.to_s, args[arg]) end } scope.setvar("title", resource.title) unless args.include? :title scope.setvar("name", resource.name) unless args.include? :name end end end diff --git a/lib/puppet/parser/ast/hostclass.rb b/lib/puppet/parser/ast/hostclass.rb index 8959dc900..41ca34432 100644 --- a/lib/puppet/parser/ast/hostclass.rb +++ b/lib/puppet/parser/ast/hostclass.rb @@ -1,75 +1,75 @@ require 'puppet/parser/ast/definition' class Puppet::Parser::AST # The code associated with a class. This is different from definitions # in that each class is a singleton -- only one will exist for a given # node. class HostClass < AST::Definition @name = :class # 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 self.parentclass if klass == self.parentobj return true else return self.parentobj.child_of?(klass) end end # Evaluate the code associated with this class. def evaluate(options) scope = options[:scope] # Verify that we haven't already been evaluated. This is # what provides the singleton aspect. - if existing_scope = scope.class_scope(self) + if existing_scope = scope.compile.class_scope(self) Puppet.debug "%s class already evaluated" % @type return nil end pnames = nil if pklass = self.parentobj pklass.safeevaluate :scope => scope scope = parent_scope(scope, pklass) pnames = scope.namespaces end unless options[:nosubscope] - scope = subscope(scope) + scope = subscope(scope, options[:resource]) end if pnames pnames.each do |ns| scope.add_namespace(ns) end end # Set the class before we do anything else, so that it's set # during the evaluation and can be inspected. - scope.setclass(self) + scope.compile.class_set(self.classname, scope) # Now evaluate our code, yo. if self.code return self.code.evaluate(:scope => scope) else return nil end end def initialize(options) @parentclass = nil super end def parent_scope(scope, klass) - if s = scope.class_scope(klass) + if s = scope.compile.class_scope(klass) return s else raise Puppet::DevError, "Could not find scope for %s" % klass.fqname end end end end diff --git a/lib/puppet/parser/ast/node.rb b/lib/puppet/parser/ast/node.rb index 695c15f42..d46df3cff 100644 --- a/lib/puppet/parser/ast/node.rb +++ b/lib/puppet/parser/ast/node.rb @@ -1,64 +1,68 @@ require 'puppet/parser/ast/hostclass' class Puppet::Parser::AST # The specific code associated with a host. Nodes are annoyingly unlike # other objects. That's just the way it is, at least for now. class Node < AST::HostClass @name = :node attr_accessor :name #def evaluate(scope, facts = {}) def evaluate(options) scope = options[:scope] #pscope = if ! Puppet[:lexical] or options[:asparent] # @scope #else # origscope #end # We don't have to worry about the declarativeness of node parentage, # because the entry point is always a single node definition. if parent = self.parentobj scope = parent.safeevaluate :scope => scope end scope = scope.newscope( :type => self.name, :keyword => @keyword, :source => self, :namespace => "" # nodes are always in "" ) # Mark our node name as a class, too, but strip it of the domain # name. Make the mark before we evaluate the code, so that it is # marked within the code itself. - scope.setclass(self) + scope.compile.class_set(self.classname, scope) # And then evaluate our code if we have any if self.code @code.safeevaluate(:scope => scope) end return scope end def initialize(options) @parentclass = nil super # Do some validation on the node name if @name =~ /[^-\w.]/ raise Puppet::ParseError, "Invalid node name %s" % @name end end + # Make sure node scopes are marked as such. + def subscope(*args) + scope = super + scope.nodescope = true + end + private # Search for the object matching our parent class. def find_parentclass @parser.findnode(parentclass) end end end - -# $Id$ diff --git a/lib/puppet/parser/ast/resourceoverride.rb b/lib/puppet/parser/ast/resourceoverride.rb index 26d69ae97..418c9c8e4 100644 --- a/lib/puppet/parser/ast/resourceoverride.rb +++ b/lib/puppet/parser/ast/resourceoverride.rb @@ -1,62 +1,62 @@ require 'puppet/parser/ast/resourcedef' class Puppet::Parser::AST # Set a parameter on a resource specification created somewhere else in the # configuration. The object is responsible for verifying that this is allowed. class ResourceOverride < ResourceDef attr_accessor :object attr_reader :params # Iterate across all of our children. def each [@object,@params].flatten.each { |param| #Puppet.debug("yielding param %s" % param) yield param } end # Does not actually return an object; instead sets an object # in the current scope. def evaluate(hash) scope = hash[:scope] # Get our object reference. object = @object.safeevaluate(:scope => scope) hash = {} # Evaluate all of the specified params. params = @params.collect { |param| param.safeevaluate(:scope => scope) } # Now we just create a normal resource, but we call a very different # method on the scope. obj = Puppet::Parser::Resource.new( :type => object.type, :title => object.title, :params => params, :file => @file, :line => @line, :source => scope.source, :scope => scope ) # Now we tell the scope that it's an override, and it behaves as # necessary. - scope.setoverride(obj) + scope.compile.store_override(obj) obj end # Create our ResourceDef. Handles type checking for us. def initialize(hash) @checked = false super #self.typecheck(@type.value) end end end # $Id$ diff --git a/lib/puppet/parser/compile.rb b/lib/puppet/parser/compile.rb index 7159947bf..0ae712e57 100644 --- a/lib/puppet/parser/compile.rb +++ b/lib/puppet/parser/compile.rb @@ -1,565 +1,569 @@ # Created by Luke A. Kanies on 2007-08-13. # Copyright (c) 2007. All rights reserved. require 'puppet/external/gratr/digraph' require 'puppet/external/gratr/import' require 'puppet/external/gratr/dot' require 'puppet/node' require 'puppet/util/errors' # Maintain a graph of scopes, along with a bunch of data # about the individual configuration we're compiling. class Puppet::Parser::Compile include Puppet::Util include Puppet::Util::Errors attr_reader :topscope, :parser, :node, :facts, :collections attr_accessor :extraction_format attr_writer :ast_nodes # Add a collection to the global list. def add_collection(coll) @collections << coll end # Do we use nodes found in the code, vs. the external node sources? def ast_nodes? defined?(@ast_nodes) and @ast_nodes 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) if existing = @class_scopes[name] if existing.nodescope? or scope.nodescope? raise Puppet::ParseError, "Cannot have classes, nodes, or definitions with the same name" else raise Puppet::DevError, "Somehow evaluated the same class twice" end end @class_scopes[name] = scope tag(name) 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 if klass.respond_to?(:classname) @class_scopes[klass.classname] else @class_scopes[klass] end end # Return a list of all of the defined classes. def classlist return @class_scopes.keys.reject { |k| k == "" } end # Compile our configuration. This mostly revolves around finding and evaluating classes. # This is the main entry into our configuration. def compile # Set the client's parameters into the top scope. set_node_parameters() evaluate_main() evaluate_ast_node() evaluate_node_classes() evaluate_generators() fail_on_unevaluated() finish() if Puppet[:storeconfigs] store() end return extract() end # FIXME There are no tests for this. def delete_collection(coll) @collections.delete(coll) if @collections.include?(coll) end # FIXME There are no tests for this. def delete_resource(resource) @resource_table.delete(resource.ref) if @resource_table.include?(resource.ref) @resource_graph.remove_vertex!(resource) if @resource_graph.vertex?(resource) 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, @parser.findclass("", "")) + evaluate_classes(@node.classes, @topscope) end # Evaluate each specified class in turn. If there are any classes we can't # find, just tag the configuration 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, source) + def evaluate_classes(classes, scope) found = [] classes.each do |name| + # If we can find the class, then make a resource that will evaluate it. if klass = @parser.findclass("", name) # This will result in class_set getting called, which # will in turn result in tags. Yay. - klass.safeevaluate(:scope => topscope) + klass.safeevaluate(:scope => scope) found << name else Puppet.info "Could not find class %s for %s" % [name, node.name] tag(name) end end found end # Make sure we support the requested extraction format. def extraction_format=(value) unless respond_to?("extract_to_%s" % value) raise ArgumentError, "Invalid extraction format %s" % value end @extraction_format = value end # Return a resource by either its ref or its type and title. def findresource(string, name = nil) if name string = "%s[%s]" % [string.capitalize, name] end @resource_table[string] end - # Set up our configuration. We require a parser + # 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 = {}) @node = node @parser = parser options.each do |param, value| begin send(param.to_s + "=", value) rescue NoMethodError raise ArgumentError, "Compile objects do not accept %s" % param end end @extraction_format ||= :transportable initvars() end # Create a new scope, with either a specified parent scope or # using the top scope. Adds an edge between the scope and # its parent to the graph. def newscope(parent, options = {}) parent ||= @topscope options[:compile] = self options[:parser] ||= self.parser scope = Puppet::Parser::Scope.new(options) @scope_graph.add_edge!(parent, scope) scope end # Find the parent of a given scope. Assumes scopes only ever have # one in edge, which will always be true. def parent(scope) if ary = @scope_graph.adjacent(scope, :direction => :in) and ary.length > 0 ary[0] else nil end end # Return any overrides for the given resource. def resource_overrides(resource) @resource_overrides[resource.ref] end # Return a list of all resources. def resources @resource_table.values end # Store a resource override. def store_override(override) override.override = true # If possible, merge the override in immediately. if resource = @resource_table[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 store_resource(scope, resource) # This might throw an exception verify_uniqueness(resource) # Store it in the global table. @resource_table[resource.ref] = 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. @resource_graph.add_edge!(scope, resource) 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.nodes[name.to_s.downcase] end unless astnode astnode = @parser.nodes["default"] end unless astnode raise Puppet::ParseError, "Could not find default node or by name with '%s'" % node.names.join(", ") end astnode.safeevaluate :scope => topscope 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 @collections.each do |collection| if collection.evaluate found_something = true end 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 ary.each do |resource| resource.evaluate end # If we evaluated, let the loop know. return true 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 if count > 1000 raise Puppet::ParseError, "Somehow looped more than 1000 times while evaluating host configuration" end end end # Find and evaluate our main object, if possible. def evaluate_main if klass = @parser.findclass("", "") # Set the source, so objects can tell where they were defined. topscope.source = klass klass.safeevaluate :scope => topscope, :nosubscope => true end end # Turn our configuration graph into whatever the client is expecting. def extract send("extract_to_%s" % extraction_format) end # Create the traditional TransBuckets and TransObjects from our configuration # graph. This will hopefully be deprecated soon. def extract_to_transportable top = nil current = nil buckets = {} # I'm *sure* there's a simple way to do this using a breadth-first search # or something, but I couldn't come up with, and this is both fast # and simple, so I'm not going to worry about it too much. @scope_graph.vertices.each do |scope| # For each scope, we need to create a TransBucket, and then # put all of the scope's resources into that bucket, translating # each resource into a TransObject. # Unless the bucket's already been created, make it now and add # it to the cache. unless bucket = buckets[scope] bucket = buckets[scope] = scope.to_trans end # First add any contained scopes @scope_graph.adjacent(scope, :direction => :out).each do |vertex| # If there's not already a bucket, then create and cache it. unless child_bucket = buckets[vertex] child_bucket = buckets[vertex] = vertex.to_trans end bucket.push child_bucket end # Then add the resources. if @resource_graph.vertex?(scope) @resource_graph.adjacent(scope, :direction => :out).each do |vertex| # Some resources don't get translated, e.g., virtual resources. if obj = vertex.to_trans bucket.push obj end end end end # Retrive the bucket for the top-level scope and set the appropriate metadata. result = buckets[topscope] result.copy_type_and_name(topscope) unless classlist.empty? result.classes = classlist end # Clear the cache to encourage the GC buckets.clear return result end # Make sure the entire configuration 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 object(s) %s" % 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 @resource_table.each { |name, resource| resource.finish if resource.respond_to?(:finish) } end # Set up all of our internal variables. def initvars # The table for storing class singletons. This will only actually # be used by top scopes and node scopes. @class_scopes = {} # The table for all defined resources. @resource_table = {} # 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 = [] # A list of tags we've generated; most class names. @tags = [] # Create our initial scope, our scope graph, and add the initial scope to the graph. @topscope = Puppet::Parser::Scope.new(:compile => self, :type => "main", :name => "top", :parser => self.parser) + #@main = @parser.findclass("", "") + #@main_resource = Puppet::Parser::Resource.new(:type => "class", :title => :main, :scope => @topscope, :source => @main) + #@topscope.resource = @main_resource # For maintaining scope relationships. @scope_graph = GRATR::Digraph.new @scope_graph.add_vertex!(@topscope) # For maintaining the relationship between scopes and their resources. @resource_graph = GRATR::Digraph.new 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 end # Store the configuration into the database. def store unless Puppet.features.rails? raise Puppet::Error, "storeconfigs is enabled but rails is unavailable" end unless ActiveRecord::Base.connected? Puppet::Rails.connect end # We used to have hooks here for forking and saving, but I don't # think it's worth retaining at this point. store_to_active_record(@node, @resource_table.values) end # Do the actual storage. def store_to_active_record(node, resources) begin # We store all of the objects, even the collectable ones benchmark(:info, "Stored configuration for #{node.name}") do Puppet::Rails::Host.transaction do Puppet::Rails::Host.store(node, resources) end end rescue => detail if Puppet[:trace] puts detail.backtrace end Puppet.err "Could not store configs: %s" % detail.to_s end end # Add a tag. def tag(*names) names.each do |name| name = name.to_s @tags << name unless @tags.include?(name) end nil end # Return the list of tags. def tags @tags.dup 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 = @resource_table.find_all do |name, object| ! object.builtin? and ! object.evaluated? end.collect { |name, object| object } if ary.empty? return nil else return ary end end # Verify that the given resource isn't defined elsewhere. def verify_uniqueness(resource) # Short-curcuit the common case, unless existing_resource = @resource_table[resource.ref] return true end if typeclass = Puppet::Type.type(resource.type) and ! typeclass.isomorphic? Puppet.info "Allowing duplicate %s" % typeclass.name return true end # Either it's a defined type, which are never # isomorphic, or it's a non-isomorphic type, so # we should throw an exception. msg = "Duplicate definition: %s is already defined" % resource.ref if existing_resource.file and existing_resource.line msg << " in file %s at line %s" % [existing_resource.file, existing_resource.line] end if resource.line or resource.file msg << "; cannot redefine" end raise Puppet::ParseError.new(msg) end end diff --git a/lib/puppet/parser/scope.rb b/lib/puppet/parser/scope.rb index 635a471df..5f6f15ba6 100644 --- a/lib/puppet/parser/scope.rb +++ b/lib/puppet/parser/scope.rb @@ -1,442 +1,411 @@ # 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' class Puppet::Parser::Scope require 'puppet/parser/resource' AST = Puppet::Parser::AST Puppet::Util.logmethods(self) include Enumerable include Puppet::Util::Errors - attr_accessor :parent, :level, :parser, :source - attr_accessor :name, :type, :base, :keyword - attr_accessor :top, :translated, :exported, :virtual, :compile + attr_accessor :parent, :level, :parser, :source, :resource + attr_accessor :base, :keyword, :nodescope + attr_accessor :top, :translated, :compile + + # Temporary accessors. + attr_accessor :name, :type, :title, :exported, :virtual + def exported? + exported + end + def virtual? + virtual + end + #[:name, :type, :title, :exported?, :virtual].each do |method| + # define_method(method) do + # @resource.send(method) + # end + #end # Proxy accessors def host @compile.node.name end + def interpreter @compile.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 # 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 # Is the type a builtin type? def builtintype?(type) if typeklass = Puppet::Type.type(type) return typeklass else return false end end - # Retrieve a given class scope from the compile. - def class_scope(klass) - compile.class_scope(klass) - end - # Are we the top scope? def topscope? @level == 1 end - def exported? - self.exported - end - def findclass(name) @namespaces.each do |namespace| if r = parser.findclass(namespace, name) return r end end return nil end def finddefine(name) @namespaces.each do |namespace| if r = parser.finddefine(namespace, name) return r end end return nil end def findresource(string, name = nil) compile.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 = {} # 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] = {} } 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) finddefine(name) || findclass(name) end def lookup_qualified_var(name, usestring) parts = name.split(/::/) shortname = parts.pop klassname = parts.join("::") klass = findclass(klassname) unless klass raise Puppet::ParseError, "Could not find class %s" % klassname end - unless kscope = class_scope(klass) + unless kscope = compile.class_scope(klass) raise Puppet::ParseError, "Class %s has not been evaluated so its variables cannot be referenced" % klass.classname 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) # 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 @symtable[name]" here because the value might be false if @symtable.include?(name) if usestring and @symtable[name] == :undef return "" else return @symtable[name] end elsif self.parent return parent.lookupvar(name, usestring) elsif usestring return "" else return :undefined end end def namespaces @namespaces.dup end # Create a new scope and set these options. def newscope(options = {}) compile.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? - defined?(@nodescope) and @nodescope + self.nodescope end # We probably shouldn't cache this value... But it's a lot faster # than doing lots of queries. def parent unless defined?(@parent) @parent = compile.parent(self) end @parent 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 - def resources - @definedtable.values - end - - # Store the fact that we've evaluated a given class. We use a hash - # that gets inherited from the top scope down, rather than a global - # hash. We store the object ID, not class name, so that we - # can support multiple unrelated classes with the same name. - def setclass(klass) - if klass.is_a?(AST::HostClass) - unless name = klass.classname - raise Puppet::DevError, "Got a %s with no fully qualified name" % - klass.class - end - @compile.class_set(name, self) - else - raise Puppet::DevError, "Invalid class %s" % klass.inspect - end - if klass.is_a?(AST::Node) - @nodescope = true - end - nil - end - - # Override a parameter in an existing object. If the object does not yet - # exist, then cache the override in a global table, so it can be flushed - # at the end. - def setoverride(resource) - @compile.store_override(resource) - 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, file = nil, line = nil) #Puppet.debug "Setting %s to '%s' at level %s" % # [name.inspect,value,self.level] if @symtable.include?(name) error = Puppet::ParseError.new("Cannot reassign variable %s" % name) if file error.file = file end if line error.line = line end raise error end @symtable[name] = value 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+)\}|^\$((\w*::)*\w+)/) # If it matches the backslash, then just retun the dollar sign. if ss.matched == '\\$' out << '$' else # look the variable up out << lookupvar(ss[1] || ss[3]).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 # Add a tag to our current list. These tags will be added to all # of the objects contained in this scope. def tag(*ary) ary.each { |tag| if tag.nil? or tag == "" puts caller Puppet.debug "got told to tag with %s" % tag.inspect next end unless tag =~ /^\w[-\w]*$/ fail Puppet::ParseError, "Invalid tag %s" % tag.inspect end tag = tag.to_s unless @tags.include?(tag) #Puppet.info "Tagging scope %s with %s" % [self.object_id, tag] @tags << tag end } 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 tmp = [] + @tags unless ! defined? @type or @type.nil? or @type == "" tmp << @type.to_s end if parent #info "Looking for tags in %s" % parent.type parent.tags.each { |tag| if tag.nil? or tag == "" Puppet.debug "parent returned tag %s" % tag.inspect next end unless tmp.include?(tag) tmp << tag end } end return tmp.sort.uniq end # Used mainly for logging def to_s if self.name return "%s[%s]" % [@type, @name] else return self.type.to_s end end # Convert our resource to a TransBucket. def to_trans bucket = Puppet::TransBucket.new([]) case self.type when "": bucket.type = "main" when nil: devfail "A Scope with no type" else bucket.type = @type end if self.name bucket.name = self.name end return bucket end # Undefine a variable; only used for testing. def unsetvar(var) if @symtable.include?(var) @symtable.delete(var) end end - - def virtual? - self.virtual || self.exported? - end end - -# $Id$ diff --git a/test/language/ast.rb b/test/language/ast.rb index 847d24660..000a56a89 100755 --- a/test/language/ast.rb +++ b/test/language/ast.rb @@ -1,190 +1,190 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppet/parser/interpreter' require 'puppet/parser/parser' require 'puppettest/resourcetesting' require 'puppettest/parsertesting' require 'puppettest/support/collection' class TestAST < Test::Unit::TestCase include PuppetTest::ParserTesting include PuppetTest::ResourceTesting include PuppetTest::Support::Collection def test_if astif = nil astelse = nil fakeelse = FakeAST.new(:else) faketest = FakeAST.new(true) fakeif = FakeAST.new(:if) assert_nothing_raised { astelse = AST::Else.new(:statements => fakeelse) } assert_nothing_raised { astif = AST::IfStatement.new( :test => faketest, :statements => fakeif, :else => astelse ) } # We initialized it to true, so we should get that first ret = nil assert_nothing_raised { ret = astif.evaluate(:scope => "yay") } assert_equal(:if, ret) # Now set it to false and check that faketest.evaluate = false assert_nothing_raised { ret = astif.evaluate(:scope => "yay") } assert_equal(:else, ret) end # Make sure our override object behaves "correctly" def test_override scope = mkscope ref = nil assert_nothing_raised do ref = resourceoverride("file", "/yayness", "owner" => "blah", "group" => "boo") end Puppet::Parser::Resource.expects(:new).with { |o| o.is_a?(Hash) }.returns(:override) - scope.expects(:setoverride).with(:override) + scope.compile.expects(:store_override).with(:override) ret = nil assert_nothing_raised do ret = ref.evaluate :scope => scope end assert_equal(:override, ret, "Did not return override") end # make sure our resourcedefaults ast object works correctly. def test_resourcedefaults interp, scope, source = mkclassframing # Now make some defaults for files args = {:source => "/yay/ness", :group => "yayness"} assert_nothing_raised do obj = defaultobj "file", args obj.evaluate :scope => scope end hash = nil assert_nothing_raised do hash = scope.lookupdefaults("file") end hash.each do |name, value| assert_instance_of(Symbol, name) # params always convert assert_instance_of(Puppet::Parser::Resource::Param, value) end args.each do |name, value| assert(hash[name], "Did not get default %s" % name) assert_equal(value, hash[name].value) end end def test_node scope = mkscope parser = scope.compile.parser # Define a base node basenode = parser.newnode "basenode", :code => AST::ASTArray.new(:children => [ resourcedef("file", "/tmp/base", "owner" => "root") ]) # Now define a subnode nodes = parser.newnode ["mynode", "othernode"], :code => AST::ASTArray.new(:children => [ resourcedef("file", "/tmp/mynode", "owner" => "root"), resourcedef("file", "/tmp/basenode", "owner" => "daemon") ]) assert_instance_of(Array, nodes) # Make sure we can find them all. %w{mynode othernode}.each do |node| assert(parser.nodes[node], "Could not find %s" % node) end mynode = parser.nodes["mynode"] # Now try evaluating the node assert_nothing_raised do mynode.evaluate :scope => scope end # Make sure that we can find each of the files myfile = scope.findresource "File[/tmp/mynode]" assert(myfile, "Could not find file from node") assert_equal("root", myfile[:owner]) basefile = scope.findresource "File[/tmp/basenode]" assert(basefile, "Could not find file from base node") assert_equal("daemon", basefile[:owner]) # Now make sure we can evaluate nodes with parents child = parser.newnode(%w{child}, :parent => "basenode").shift newscope = mkscope :parser => parser assert_nothing_raised do child.evaluate :scope => newscope end assert(newscope.findresource("File[/tmp/base]"), "Could not find base resource") end def test_collection scope = mkscope coll = nil assert_nothing_raised do coll = AST::Collection.new(:type => "file", :form => :virtual) end assert_instance_of(AST::Collection, coll) ret = nil assert_nothing_raised do ret = coll.evaluate :scope => scope end assert_instance_of(Puppet::Parser::Collector, ret) # Now make sure we get it back from the scope colls = scope.compile.instance_variable_get("@collections") assert_equal([ret], colls, "Did not store collector in config's collection list") end def test_virtual_collexp @interp, @scope, @source = mkclassframing # make a resource resource = mkresource(:type => "file", :title => "/tmp/testing", :params => {:owner => "root", :group => "bin", :mode => "644"}) run_collection_queries(:virtual) do |string, result, query| code = nil assert_nothing_raised do str, code = query.evaluate :scope => @scope end assert_instance_of(Proc, code) assert_nothing_raised do assert_equal(result, code.call(resource), "'#{string}' failed") end end end end # $Id$ diff --git a/test/language/ast/definition.rb b/test/language/ast/definition.rb index 5a2e6ffea..51948b01f 100755 --- a/test/language/ast/definition.rb +++ b/test/language/ast/definition.rb @@ -1,157 +1,157 @@ #!/usr/bin/env ruby # # Created by Luke A. Kanies on 2006-02-20. # Copyright (c) 2006. All rights reserved. $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'mocha' require 'puppettest/parsertesting' require 'puppettest/resourcetesting' class TestASTDefinition < Test::Unit::TestCase include PuppetTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting AST = Puppet::Parser::AST def test_initialize parser = mkparser # Create a new definition klass = parser.newdefine "yayness", :arguments => [["owner", stringobj("nobody")], %w{mode}], :code => AST::ASTArray.new( :children => [resourcedef("file", "/tmp/$name", "owner" => varref("owner"), "mode" => varref("mode"))] ) # Test validattr? a couple different ways [:owner, "owner", :schedule, "schedule"].each do |var| assert(klass.validattr?(var), "%s was not considered valid" % var.inspect) end [:random, "random"].each do |var| assert(! klass.validattr?(var), "%s was considered valid" % var.inspect) end end def test_evaluate parser = mkparser config = mkconfig scope = config.topscope klass = parser.newdefine "yayness", :arguments => [["owner", stringobj("nobody")], %w{mode}], :code => AST::ASTArray.new( :children => [resourcedef("file", "/tmp/$name", "owner" => varref("owner"), "mode" => varref("mode"))] ) resource = stub 'resource', :title => "first", :name => "first", :type => "yayness", :to_hash => {"mode" => "755"}, :exported => false, :virtual => false resource.stubs(:title) assert_nothing_raised do klass.evaluate(:scope => scope, :resource => resource) end firstobj = config.findresource("File[/tmp/first]") assert(firstobj, "Did not create /tmp/first obj") assert_equal("file", firstobj.type) assert_equal("/tmp/first", firstobj.title) assert_equal("nobody", firstobj[:owner]) assert_equal("755", firstobj[:mode]) # Make sure we can't evaluate it with the same args assert_raise(Puppet::ParseError) do klass.evaluate(:scope => scope, :resource => resource) end # Now create another with different args resource2 = stub 'resource', :title => "second", :name => "second", :type => "yayness", :to_hash => {"mode" => "755", "owner" => "daemon"}, :exported => false, :virtual => false assert_nothing_raised do klass.evaluate(:scope => scope, :resource => resource2) end secondobj = config.findresource("File[/tmp/second]") assert(secondobj, "Did not create /tmp/second obj") assert_equal("file", secondobj.type) assert_equal("/tmp/second", secondobj.title) assert_equal("daemon", secondobj[:owner]) assert_equal("755", secondobj[:mode]) end # #539 - definitions should support both names and titles def test_names_and_titles parser = mkparser scope = mkscope :parser => parser [ {:name => "one", :title => "two"}, {:title => "mytitle"} ].each_with_index do |hash, i| # Create a definition that uses both name and title. Put this # inside the loop so the subscope expectations work. klass = parser.newdefine "yayness%s" % i - subscope = klass.subscope(scope, "yayness%s" % i) - - klass.expects(:subscope).returns(subscope) - resource = stub 'resource', :title => hash[:title], :name => hash[:name] || hash[:title], :type => "yayness%s" % i, :to_hash => {}, :exported => false, :virtual => false + subscope = klass.subscope(scope, resource) + + klass.expects(:subscope).returns(subscope) + if hash[:name] resource.stubs(:to_hash).returns({:name => hash[:name]}) end assert_nothing_raised("Could not evaluate definition with %s" % hash.inspect) do klass.evaluate(:scope => scope, :resource => resource) end name = hash[:name] || hash[:title] title = hash[:title] assert_equal(name, subscope.lookupvar("name"), "Name did not get set correctly") assert_equal(title, subscope.lookupvar("title"), "title did not get set correctly") [:name, :title].each do |param| val = resource.send(param) assert(subscope.tags.include?(val), "Scope was not tagged with %s '%s'" % [param, val]) end end end # Testing the root cause of #615. We should be using the fqname for the type, instead # of just the short name. def test_fully_qualified_types parser = mkparser klass = parser.newclass("one::two") assert_equal("one::two", klass.classname, "Class did not get fully qualified class name") end end diff --git a/test/language/ast/hostclass.rb b/test/language/ast/hostclass.rb index 62483730b..c88152913 100755 --- a/test/language/ast/hostclass.rb +++ b/test/language/ast/hostclass.rb @@ -1,165 +1,165 @@ #!/usr/bin/env ruby # # Created by Luke A. Kanies on 2006-02-20. # Copyright (c) 2006. All rights reserved. $:.unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppettest' require 'puppettest/parsertesting' require 'puppettest/resourcetesting' require 'mocha' class TestASTHostClass < Test::Unit::TestCase include PuppetTest include PuppetTest::ParserTesting include PuppetTest::ResourceTesting AST = Puppet::Parser::AST def test_hostclass scope = mkscope parser = scope.compile.parser # Create the class we're testing, first with no parent klass = parser.newclass "first", :code => AST::ASTArray.new( :children => [resourcedef("file", "/tmp", "owner" => "nobody", "mode" => "755")] ) assert_nothing_raised do klass.evaluate(:scope => scope) end # Then try it again assert_nothing_raised do klass.evaluate(:scope => scope) end - assert(scope.class_scope(klass), "Class was not considered evaluated") + assert(scope.compile.class_scope(klass), "Class was not considered evaluated") tmp = scope.findresource("File[/tmp]") assert(tmp, "Could not find file /tmp") assert_equal("nobody", tmp[:owner]) assert_equal("755", tmp[:mode]) # Now create a couple more classes. newbase = parser.newclass "newbase", :code => AST::ASTArray.new( :children => [resourcedef("file", "/tmp/other", "owner" => "nobody", "mode" => "644")] ) newsub = parser.newclass "newsub", :parent => "newbase", :code => AST::ASTArray.new( :children => [resourcedef("file", "/tmp/yay", "owner" => "nobody", "mode" => "755"), resourceoverride("file", "/tmp/other", "owner" => "daemon") ] ) # Override a different variable in the top scope. moresub = parser.newclass "moresub", :parent => "newbase", :code => AST::ASTArray.new( :children => [resourceoverride("file", "/tmp/other", "mode" => "755")] ) assert_nothing_raised do newsub.evaluate(:scope => scope) end assert_nothing_raised do moresub.evaluate(:scope => scope) end - assert(scope.class_scope(newbase), "Did not eval newbase") - assert(scope.class_scope(newsub), "Did not eval newsub") + assert(scope.compile.class_scope(newbase), "Did not eval newbase") + assert(scope.compile.class_scope(newsub), "Did not eval newsub") yay = scope.findresource("File[/tmp/yay]") assert(yay, "Did not find file /tmp/yay") assert_equal("nobody", yay[:owner]) assert_equal("755", yay[:mode]) other = scope.findresource("File[/tmp/other]") assert(other, "Did not find file /tmp/other") assert_equal("daemon", other[:owner]) assert_equal("755", other[:mode]) end # Make sure that classes set their namespaces to themselves. This # way they start looking for definitions in their own namespace. def test_hostclass_namespace scope = mkscope parser = scope.compile.parser # Create a new class klass = nil assert_nothing_raised do klass = parser.newclass "funtest" end # Now define a definition in that namespace define = nil assert_nothing_raised do define = parser.newdefine "funtest::mydefine" end assert_equal("funtest", klass.namespace, "component namespace was not set in the class") assert_equal("funtest", define.namespace, "component namespace was not set in the definition") newscope = klass.subscope(scope) assert_equal(["funtest"], newscope.namespaces, "Scope did not inherit namespace") # Now make sure we can find the define assert(newscope.finddefine("mydefine"), "Could not find definition in my enclosing class") end # Make sure that our scope is a subscope of the parentclass's scope. # At the same time, make sure definitions in the parent class can be # found within the subclass (#517). def test_parent_scope_from_parentclass scope = mkscope parser = scope.compile.parser source = parser.newclass "" parser.newclass("base") fun = parser.newdefine("base::fun") parser.newclass("middle", :parent => "base") parser.newclass("sub", :parent => "middle") scope = mkscope :parser => parser ret = nil assert_nothing_raised do - ret = scope.compile.evaluate_classes(["sub"], source) + ret = scope.compile.evaluate_classes(["sub"], scope) end - subscope = scope.class_scope(scope.findclass("sub")) + subscope = scope.compile.class_scope(scope.findclass("sub")) assert(subscope, "could not find sub scope") - mscope = scope.class_scope(scope.findclass("middle")) + mscope = scope.compile.class_scope(scope.findclass("middle")) assert(mscope, "could not find middle scope") - pscope = scope.class_scope(scope.findclass("base")) + pscope = scope.compile.class_scope(scope.findclass("base")) assert(pscope, "could not find parent scope") assert(pscope == mscope.parent, "parent scope of middle was not set correctly") assert(mscope == subscope.parent, "parent scope of sub was not set correctly") result = mscope.finddefine("fun") assert(result, "could not find parent-defined definition from middle") assert(fun == result, "found incorrect parent-defined definition from middle") result = subscope.finddefine("fun") assert(result, "could not find parent-defined definition from sub") assert(fun == result, "found incorrect parent-defined definition from sub") end end diff --git a/test/language/compile.rb b/test/language/compile.rb index 5fde2500e..3128b8e64 100755 --- a/test/language/compile.rb +++ b/test/language/compile.rb @@ -1,765 +1,763 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'mocha' require 'puppettest' require 'puppettest/parsertesting' require 'puppet/parser/compile' # Test our compile object. class TestCompile < Test::Unit::TestCase include PuppetTest include PuppetTest::ParserTesting Compile = Puppet::Parser::Compile Scope = Puppet::Parser::Scope Node = Puppet::Network::Handler.handler(:node) SimpleNode = Puppet::Node def mknode(name = "foo") @node = SimpleNode.new(name) end def mkparser # This should mock an interpreter @parser = mock 'parser' end def mkconfig(options = {}) if node = options[:node] options.delete(:node) else node = mknode end @config = Compile.new(node, mkparser, options) end def test_initialize config = nil assert_nothing_raised("Could not init config with all required options") do config = Compile.new("foo", "parser") end assert_equal("foo", config.node, "Did not set node correctly") assert_equal("parser", config.parser, "Did not set parser correctly") # We're not testing here whether we call initvars, because it's too difficult to # mock. # Now try it with some options assert_nothing_raised("Could not init config with extra options") do config = Compile.new("foo", "parser", :ast_nodes => false) end assert_equal(false, config.ast_nodes?, "Did not set ast_nodes? correctly") end def test_initvars config = mkconfig [:class_scopes, :resource_table, :exported_resources, :resource_overrides].each do |table| assert_instance_of(Hash, config.send(:instance_variable_get, "@#{table}"), "Did not set %s table correctly" % table) end assert_instance_of(Scope, config.topscope, "Did not create a topscope") graph = config.instance_variable_get("@scope_graph") assert_instance_of(GRATR::Digraph, graph, "Did not create scope graph") assert(graph.vertex?(config.topscope), "Did not add top scope as a vertex in the graph") end # Make sure we store and can retrieve references to classes and their scopes. def test_class_set_and_class_scope klass = mock 'ast_class' klass.expects(:classname).returns("myname") config = mkconfig config.expects(:tag).with("myname") assert_nothing_raised("Could not set class") do config.class_set "myname", "myscope" end # First try to retrieve it by name. assert_equal("myscope", config.class_scope("myname"), "Could not retrieve class scope by name") # Then by object assert_equal("myscope", config.class_scope(klass), "Could not retrieve class scope by object") end def test_classlist config = mkconfig config.class_set "", "empty" config.class_set "one", "yep" config.class_set "two", "nope" # Make sure our class list is correct assert_equal(%w{one two}.sort, config.classlist.sort, "Did not get correct class list") end # Make sure collections get added to our internal array def test_add_collection config = mkconfig assert_nothing_raised("Could not add collection") do config.add_collection "nope" end assert_equal(%w{nope}, config.instance_variable_get("@collections"), "Did not add collection") end # Make sure we create a graph of scopes. def test_newscope config = mkconfig graph = config.instance_variable_get("@scope_graph") assert_instance_of(Scope, config.topscope, "Did not create top scope") assert_instance_of(GRATR::Digraph, graph, "Did not create graph") assert(graph.vertex?(config.topscope), "The top scope is not a vertex in the graph") # Now that we've got the top scope, create a new, subscope subscope = nil assert_nothing_raised("Could not create subscope") do subscope = config.newscope(config.topscope) end assert_instance_of(Scope, subscope, "Did not create subscope") assert(graph.edge?(config.topscope, subscope), "An edge between top scope and subscope was not added") # Make sure a scope can find its parent. assert(config.parent(subscope), "Could not look up parent scope on compile") assert_equal(config.topscope.object_id, config.parent(subscope).object_id, "Did not get correct parent scope from compile") assert_equal(config.topscope.object_id, subscope.parent.object_id, "Scope did not correctly retrieve its parent scope") # Now create another, this time specifying options another = nil assert_nothing_raised("Could not create subscope") do another = config.newscope(subscope, :name => "testing") end assert_equal("testing", another.name, "did not set scope option correctly") assert_instance_of(Scope, another, "Did not create second subscope") assert(graph.edge?(subscope, another), "An edge between parent scope and second subscope was not added") # Make sure it can find its parent. assert(config.parent(another), "Could not look up parent scope of second subscope on compile") assert_equal(subscope.object_id, config.parent(another).object_id, "Did not get correct parent scope of second subscope from compile") assert_equal(subscope.object_id, another.parent.object_id, "Second subscope did not correctly retrieve its parent scope") # And make sure both scopes show up in the right order in the search path assert_equal([another.object_id, subscope.object_id, config.topscope.object_id], another.scope_path.collect { |p| p.object_id }, "Did not get correct scope path") end # The heart of the action. def test_compile config = mkconfig [:set_node_parameters, :evaluate_main, :evaluate_ast_node, :evaluate_node_classes, :evaluate_generators, :fail_on_unevaluated, :finish].each do |method| config.expects(method) end config.expects(:extract).returns(:config) assert_equal(:config, config.compile, "Did not return the results of the extraction") end # Test setting the node's parameters into the top scope. def test_set_node_parameters config = mkconfig @node.parameters = {"a" => "b", "c" => "d"} scope = config.topscope @node.parameters.each do |param, value| scope.expects(:setvar).with(param, value) end assert_nothing_raised("Could not call 'set_node_parameters'") do config.send(:set_node_parameters) end end # Test that we can evaluate the main class, which is the one named "" in namespace # "". def test_evaluate_main config = mkconfig main = mock 'main_class' config.topscope.expects(:source=).with(main) main.expects(:safeevaluate).with(:scope => config.topscope, :nosubscope => true) @parser.expects(:findclass).with("", "").returns(main) assert_nothing_raised("Could not call evaluate_main") do config.send(:evaluate_main) end end # Make sure we either don't look for nodes, or that we find and evaluate the right object. def test_evaluate_ast_node # First try it with ast_nodes disabled config = mkconfig :ast_nodes => false config.expects(:ast_nodes?).returns(false) config.parser.expects(:nodes).never assert_nothing_raised("Could not call evaluate_ast_node when ast nodes are disabled") do config.send(:evaluate_ast_node) end # Now try it with them enabled, but no node found. nodes = mock 'node_hash' config = mkconfig :ast_nodes => true config.expects(:ast_nodes?).returns(true) config.parser.expects(:nodes).returns(nodes).times(4) # Set some names for our test @node.names = %w{a b c} nodes.expects(:[]).with("a").returns(nil) nodes.expects(:[]).with("b").returns(nil) nodes.expects(:[]).with("c").returns(nil) # It should check this last, of course. nodes.expects(:[]).with("default").returns(nil) # And make sure the lack of a node throws an exception assert_raise(Puppet::ParseError, "Did not fail when we couldn't find an ast node") do config.send(:evaluate_ast_node) end # Finally, make sure it works dandily when we have a node nodes = mock 'hash' config = mkconfig :ast_nodes => true config.expects(:ast_nodes?).returns(true) config.parser.expects(:nodes).returns(nodes).times(3) node = mock 'node' node.expects(:safeevaluate).with(:scope => config.topscope) # Set some names for our test @node.names = %w{a b c} nodes.expects(:[]).with("a").returns(nil) nodes.expects(:[]).with("b").returns(nil) nodes.expects(:[]).with("c").returns(node) nodes.expects(:[]).with("default").never # And make sure the lack of a node throws an exception assert_nothing_raised("Failed when a node was found") do config.send(:evaluate_ast_node) end # Lastly, check when we actually find the default. nodes = mock 'hash' config = mkconfig :ast_nodes => true config.expects(:ast_nodes?).returns(true) config.parser.expects(:nodes).returns(nodes).times(4) node = mock 'node' node.expects(:safeevaluate).with(:scope => config.topscope) # Set some names for our test @node.names = %w{a b c} nodes.expects(:[]).with("a").returns(nil) nodes.expects(:[]).with("b").returns(nil) nodes.expects(:[]).with("c").returns(nil) nodes.expects(:[]).with("default").returns(node) # And make sure the lack of a node throws an exception assert_nothing_raised("Failed when a node was found") do config.send(:evaluate_ast_node) end end # Make sure our config object handles tags appropriately. def test_tags config = mkconfig config.send(:tag, "one") assert_equal(%w{one}, config.send(:tags), "Did not add tag") config.send(:tag, "two", "three") assert_equal(%w{one two three}, config.send(:tags), "Did not add new tags") config.send(:tag, "two") assert_equal(%w{one two three}, config.send(:tags), "Allowed duplicate tag") end def test_evaluate_node_classes config = mkconfig - main = mock 'main' - config.parser.expects(:findclass).with("", "").returns(main) @node.classes = %w{one two three four} - config.expects(:evaluate_classes).with(%w{one two three four}, main) + config.expects(:evaluate_classes).with(%w{one two three four}, config.topscope) assert_nothing_raised("could not call evaluate_node_classes") do config.send(:evaluate_node_classes) end end def test_evaluate_classes config = mkconfig classes = { "one" => mock('class one'), "three" => mock('class three') } classes.each do |name, obj| config.parser.expects(:findclass).with("", name).returns(obj) obj.expects(:safeevaluate).with(:scope => config.topscope) end %w{two four}.each do |name| config.parser.expects(:findclass).with("", name).returns(nil) end config.expects(:tag).with("two") config.expects(:tag).with("four") result = nil assert_nothing_raised("could not call evaluate_node_classes") do result = config.send(:evaluate_classes, %w{one two three four}, config.topscope) end assert_equal(%w{one three}, result, "Did not return the list of evaluated classes") end def test_evaluate_collections config = mkconfig colls = [] # Make sure we return false when there's nothing there. assert(! config.send(:evaluate_collections), "Returned true when there were no collections") # And when the collections fail to evaluate. colls << mock("coll1-false") colls << mock("coll2-false") colls.each { |c| c.expects(:evaluate).returns(false) } config.instance_variable_set("@collections", colls) assert(! config.send(:evaluate_collections), "Returned true when collections both evaluated nothing") # Now have one of the colls evaluate colls.clear colls << mock("coll1-one-true") colls << mock("coll2-one-true") colls[0].expects(:evaluate).returns(true) colls[1].expects(:evaluate).returns(false) assert(config.send(:evaluate_collections), "Did not return true when one collection evaluated true") # And have them both eval true colls.clear colls << mock("coll1-both-true") colls << mock("coll2-both-true") colls[0].expects(:evaluate).returns(true) colls[1].expects(:evaluate).returns(true) assert(config.send(:evaluate_collections), "Did not return true when both collections evaluated true") end def test_unevaluated_resources config = mkconfig resources = {} config.instance_variable_set("@resource_table", resources) # First test it when the table is empty assert_nil(config.send(:unevaluated_resources), "Somehow found unevaluated resources in an empty table") # Then add a builtin resources resources["one"] = mock("builtin only") resources["one"].expects(:builtin?).returns(true) assert_nil(config.send(:unevaluated_resources), "Considered a builtin resource unevaluated") # And do both builtin and non-builtin but already evaluated resources.clear resources["one"] = mock("builtin (with eval)") resources["one"].expects(:builtin?).returns(true) resources["two"] = mock("evaled (with builtin)") resources["two"].expects(:builtin?).returns(false) resources["two"].expects(:evaluated?).returns(true) assert_nil(config.send(:unevaluated_resources), "Considered either a builtin or evaluated resource unevaluated") # Now a single unevaluated resource. resources.clear resources["one"] = mock("unevaluated") resources["one"].expects(:builtin?).returns(false) resources["one"].expects(:evaluated?).returns(false) assert_equal([resources["one"]], config.send(:unevaluated_resources), "Did not find unevaluated resource") # With two uneval'ed resources, and an eval'ed one thrown in resources.clear resources["one"] = mock("unevaluated one") resources["one"].expects(:builtin?).returns(false) resources["one"].expects(:evaluated?).returns(false) resources["two"] = mock("unevaluated two") resources["two"].expects(:builtin?).returns(false) resources["two"].expects(:evaluated?).returns(false) resources["three"] = mock("evaluated") resources["three"].expects(:builtin?).returns(false) resources["three"].expects(:evaluated?).returns(true) result = config.send(:unevaluated_resources) %w{one two}.each do |name| assert(result.include?(resources[name]), "Did not find %s in the unevaluated list" % name) end end def test_evaluate_definitions # First try the case where there's nothing to return config = mkconfig config.expects(:unevaluated_resources).returns(nil) assert_nothing_raised("Could not test for unevaluated resources") do assert(! config.send(:evaluate_definitions), "evaluate_definitions returned true when no resources were evaluated") end # Now try it with resources left to evaluate resources = [] res1 = mock("resource1") res1.expects(:evaluate) res2 = mock("resource2") res2.expects(:evaluate) resources << res1 << res2 config = mkconfig config.expects(:unevaluated_resources).returns(resources) assert_nothing_raised("Could not test for unevaluated resources") do assert(config.send(:evaluate_definitions), "evaluate_definitions returned false when resources were evaluated") end end def test_evaluate_generators # First try the case where we have nothing to do config = mkconfig config.expects(:evaluate_definitions).returns(false) config.expects(:evaluate_collections).returns(false) assert_nothing_raised("Could not call :eval_iterate") do config.send(:evaluate_generators) end # FIXME I could not get this test to work, but the code is short # enough that I'm ok with it. # It's important that collections are evaluated before definitions, # so make sure that's the case by verifying that collections get tested # twice but definitions only once. #config = mkconfig #config.expects(:evaluate_collections).returns(true).returns(false) #config.expects(:evaluate_definitions).returns(false) #config.send(:eval_iterate) end def test_store config = mkconfig Puppet.features.expects(:rails?).returns(true) Puppet::Rails.expects(:connect) node = mock 'node' resource_table = mock 'resources' resource_table.expects(:values).returns(:resources) config.instance_variable_set("@node", node) config.instance_variable_set("@resource_table", resource_table) config.expects(:store_to_active_record).with(node, :resources) config.send(:store) end def test_store_to_active_record config = mkconfig node = mock 'node' node.expects(:name).returns("myname") Puppet::Rails::Host.stubs(:transaction).yields Puppet::Rails::Host.expects(:store).with(node, :resources) config.send(:store_to_active_record, node, :resources) end # Make sure that 'finish' gets called on all of our resources. def test_finish config = mkconfig table = config.instance_variable_get("@resource_table") # Add a resource that does respond to :finish yep = mock("finisher") yep.expects(:respond_to?).with(:finish).returns(true) yep.expects(:finish) table["yep"] = yep # And one that does not dnf = mock("dnf") dnf.expects(:respond_to?).with(:finish).returns(false) table["dnf"] = dnf config.send(:finish) end def test_extract config = mkconfig config.expects(:extraction_format).returns(:whatever) config.expects(:extract_to_whatever).returns(:result) assert_equal(:result, config.send(:extract), "Did not return extraction result as the method result") end # We want to make sure that the scope and resource graphs translate correctly def test_extract_to_transportable_simple # Start with a really simple graph -- one scope, one resource. config = mkconfig resources = config.instance_variable_get("@resource_graph") scopes = config.instance_variable_get("@scope_graph") # Get rid of the topscope scopes.vertices.each { |v| scopes.remove_vertex!(v) } bucket = [] scope = mock("scope") bucket.expects(:copy_type_and_name).with(scope) scope.expects(:to_trans).returns(bucket) scopes.add_vertex! scope # The topscope is the key to picking out the top of the graph. config.instance_variable_set("@topscope", scope) resource = mock "resource" resource.expects(:to_trans).returns(:resource) resources.add_edge! scope, resource result = nil assert_nothing_raised("Could not extract transportable compile") do result = config.send :extract_to_transportable end assert_equal([:resource], result, "Did not translate simple compile correctly") end def test_extract_to_transportable_complex # Now try it with a more complicated graph -- a three tier graph, each tier # having a scope and a resource. config = mkconfig resources = config.instance_variable_get("@resource_graph") scopes = config.instance_variable_get("@scope_graph") # Get rid of the topscope scopes.vertices.each { |v| scopes.remove_vertex!(v) } fakebucket = Class.new(Array) do attr_accessor :name def initialize(n) @name = n end end # Create our scopes. top = mock("top") topbucket = fakebucket.new "top" topbucket.expects(:copy_type_and_name).with(top) top.stubs(:copy_type_and_name) top.expects(:to_trans).returns(topbucket) # The topscope is the key to picking out the top of the graph. config.instance_variable_set("@topscope", top) middle = mock("middle") middle.expects(:to_trans).returns(fakebucket.new("middle")) scopes.add_edge! top, middle bottom = mock("bottom") bottom.expects(:to_trans).returns(fakebucket.new("bottom")) scopes.add_edge! middle, bottom topres = mock "topres" topres.expects(:to_trans).returns(:topres) resources.add_edge! top, topres midres = mock "midres" midres.expects(:to_trans).returns(:midres) resources.add_edge! middle, midres botres = mock "botres" botres.expects(:to_trans).returns(:botres) resources.add_edge! bottom, botres result = nil assert_nothing_raised("Could not extract transportable compile") do result = config.send :extract_to_transportable end assert_equal([[[:botres], :midres], :topres], result, "Did not translate medium compile correctly") end def test_verify_uniqueness config = mkconfig resources = config.instance_variable_get("@resource_table") resource = mock("noconflict") resource.expects(:ref).returns("File[yay]") assert_nothing_raised("Raised an exception when there should have been no conflict") do config.send(:verify_uniqueness, resource) end # Now try the case where our type is isomorphic resources["thing"] = true isoconflict = mock("isoconflict") isoconflict.expects(:ref).returns("thing") isoconflict.expects(:type).returns("testtype") faketype = mock("faketype") faketype.expects(:isomorphic?).returns(false) faketype.expects(:name).returns("whatever") Puppet::Type.expects(:type).with("testtype").returns(faketype) assert_nothing_raised("Raised an exception when was a conflict in non-isomorphic types") do config.send(:verify_uniqueness, isoconflict) end # Now test for when we actually have an exception initial = mock("initial") resources["thing"] = initial initial.expects(:file).returns(false) conflict = mock("conflict") conflict.expects(:ref).returns("thing").times(2) conflict.expects(:type).returns("conflict") conflict.expects(:file).returns(false) conflict.expects(:line).returns(false) faketype = mock("faketype") faketype.expects(:isomorphic?).returns(true) Puppet::Type.expects(:type).with("conflict").returns(faketype) assert_raise(Puppet::ParseError, "Did not fail when two isomorphic resources conflicted") do config.send(:verify_uniqueness, conflict) end end def test_store_resource # Run once when there's no conflict config = mkconfig table = config.instance_variable_get("@resource_table") resource = mock("resource") resource.expects(:ref).returns("yay") config.expects(:verify_uniqueness).with(resource) scope = mock("scope") graph = config.instance_variable_get("@resource_graph") graph.expects(:add_edge!).with(scope, resource) assert_nothing_raised("Could not store resource") do config.store_resource(scope, resource) end assert_equal(resource, table["yay"], "Did not store resource in table") # Now for conflicts config = mkconfig table = config.instance_variable_get("@resource_table") resource = mock("resource") config.expects(:verify_uniqueness).with(resource).raises(ArgumentError) assert_raise(ArgumentError, "Did not raise uniqueness exception") do config.store_resource(scope, resource) end assert(table.empty?, "Conflicting resource was stored in table") end def test_fail_on_unevaluated config = mkconfig config.expects(:fail_on_unevaluated_overrides) config.expects(:fail_on_unevaluated_resource_collections) config.send :fail_on_unevaluated end def test_store_override # First test the case when the resource is not present. config = mkconfig overrides = config.instance_variable_get("@resource_overrides") override = Object.new override.expects(:ref).returns(:myref).times(2) override.expects(:override=).with(true) assert_nothing_raised("Could not call store_override") do config.store_override(override) end assert_instance_of(Array, overrides[:myref], "Overrides table is not a hash of arrays") assert_equal(override, overrides[:myref][0], "Did not store override in appropriately named array") # And when the resource already exists. resource = mock 'resource' resources = config.instance_variable_get("@resource_table") resources[:resref] = resource override = mock 'override' resource.expects(:merge).with(override) override.expects(:override=).with(true) override.expects(:ref).returns(:resref) assert_nothing_raised("Could not call store_override when the resource already exists.") do config.store_override(override) end end def test_resource_overrides config = mkconfig overrides = config.instance_variable_get("@resource_overrides") overrides[:test] = :yay resource = mock 'resource' resource.expects(:ref).returns(:test) assert_equal(:yay, config.resource_overrides(resource), "Did not return overrides from table") end def test_fail_on_unevaluated_resource_collections config = mkconfig collections = config.instance_variable_get("@collections") # Make sure we're fine when the list is empty assert_nothing_raised("Failed when no collections were present") do config.send :fail_on_unevaluated_resource_collections end # And that we're fine when we've got collections but with no resources collections << mock('coll') collections[0].expects(:resources).returns(nil) assert_nothing_raised("Failed when no resource collections were present") do config.send :fail_on_unevaluated_resource_collections end # But that we do fail when we've got resource collections left. collections.clear # return both an array and a string, because that's tested internally collections << mock('coll returns one') collections[0].expects(:resources).returns(:something) collections << mock('coll returns many') collections[1].expects(:resources).returns([:one, :two]) assert_raise(Puppet::ParseError, "Did not fail on unevaluated resource collections") do config.send :fail_on_unevaluated_resource_collections end end def test_fail_on_unevaluated_overrides config = mkconfig overrides = config.instance_variable_get("@resource_overrides") # Make sure we're fine when the list is empty assert_nothing_raised("Failed when no collections were present") do config.send :fail_on_unevaluated_overrides end # But that we fail if there are any overrides left in the table. overrides[:yay] = [] overrides[:foo] = [] overrides[:bar] = [mock("override")] overrides[:bar][0].expects(:ref).returns("yay") assert_raise(Puppet::ParseError, "Failed to fail when overrides remain") do config.send :fail_on_unevaluated_overrides end end def test_find_resource config = mkconfig resources = config.instance_variable_get("@resource_table") assert_nothing_raised("Could not call findresource when the resource table was empty") do assert_nil(config.findresource("yay", "foo"), "Returned a non-existent resource") assert_nil(config.findresource("yay[foo]"), "Returned a non-existent resource") end resources["Foo[bar]"] = :yay assert_nothing_raised("Could not call findresource when the resource table was not empty") do assert_equal(:yay, config.findresource("foo", "bar"), "Returned a non-existent resource") assert_equal(:yay, config.findresource("Foo[bar]"), "Returned a non-existent resource") end end # #620 - Nodes and classes should conflict, else classes don't get evaluated def test_nodes_and_classes_name_conflict # Test node then class config = mkconfig node = stub :nodescope? => true klass = stub :nodescope? => false config.class_set("one", node) assert_raise(Puppet::ParseError, "Did not fail when replacing node with class") do config.class_set("one", klass) end # and class then node config = mkconfig node = stub :nodescope? => true klass = stub :nodescope? => false config.class_set("two", klass) assert_raise(Puppet::ParseError, "Did not fail when replacing node with class") do config.class_set("two", node) end end end diff --git a/test/language/scope.rb b/test/language/scope.rb index fc5e085d4..43cbfd47c 100755 --- a/test/language/scope.rb +++ b/test/language/scope.rb @@ -1,637 +1,598 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'mocha' require 'puppettest' require 'puppettest/parsertesting' require 'puppettest/resourcetesting' # so, what kind of things do we want to test? # we don't need to test function, since we're confident in the # library tests. We do, however, need to test how things are actually # working in the language. # so really, we want to do things like test that our ast is correct # and test whether we've got things in the right scopes class TestScope < Test::Unit::TestCase include PuppetTest::ParserTesting include PuppetTest::ResourceTesting def to_ary(hash) hash.collect { |key,value| [key,value] } end def test_variables config = mkconfig topscope = config.topscope midscope = config.newscope(topscope) botscope = config.newscope(midscope) scopes = {:top => topscope, :mid => midscope, :bot => botscope} # Set a variable in the top and make sure all three can get it topscope.setvar("first", "topval") scopes.each do |name, scope| assert_equal("topval", scope.lookupvar("first", false), "Could not find var in %s" % name) end # Now set a var in the midscope and make sure the mid and bottom can see it but not the top midscope.setvar("second", "midval") assert_equal(:undefined, scopes[:top].lookupvar("second", false), "Found child var in top scope") [:mid, :bot].each do |name| assert_equal("midval", scopes[name].lookupvar("second", false), "Could not find var in %s" % name) end # And set something in the bottom, and make sure we only find it there. botscope.setvar("third", "botval") [:top, :mid].each do |name| assert_equal(:undefined, scopes[name].lookupvar("third", false), "Found child var in top scope") end assert_equal("botval", scopes[:bot].lookupvar("third", false), "Could not find var in bottom scope") end def test_lookupvar parser = mkparser scope = mkscope :parser => parser # first do the plain lookups assert_equal("", scope.lookupvar("var"), "scope did not default to string") assert_equal("", scope.lookupvar("var", true), "scope ignored usestring setting") assert_equal(:undefined, scope.lookupvar("var", false), "scope ignored usestring setting when false") # Now set the var scope.setvar("var", "yep") assert_equal("yep", scope.lookupvar("var"), "did not retrieve value correctly") # Now test the parent lookups subscope = mkscope :parser => parser subscope.parent = scope assert_equal("", subscope.lookupvar("nope"), "scope did not default to string with parent") assert_equal("", subscope.lookupvar("nope", true), "scope ignored usestring setting with parent") assert_equal(:undefined, subscope.lookupvar("nope", false), "scope ignored usestring setting when false with parent") assert_equal("yep", subscope.lookupvar("var"), "did not retrieve value correctly from parent") # Now override the value in the subscope subscope.setvar("var", "sub") assert_equal("sub", subscope.lookupvar("var"), "did not retrieve overridden value correctly") # Make sure we punt when the var is qualified. Specify the usestring value, so we know it propagates. scope.expects(:lookup_qualified_var).with("one::two", false).returns(:punted) assert_equal(:punted, scope.lookupvar("one::two", false), "did not return the value of lookup_qualified_var") end def test_lookup_qualified_var parser = mkparser scope = mkscope :parser => parser scopes = {} classes = ["", "one", "one::two", "one::two::three"].each do |name| klass = parser.newclass(name) klass.evaluate(:scope => scope) - scopes[name] = scope.class_scope(klass) + scopes[name] = scope.compile.class_scope(klass) end classes.each do |name| var = [name, "var"].join("::") scopes[name].expects(:lookupvar).with("var", false).returns(name) assert_equal(name, scope.send(:lookup_qualified_var, var, false), "did not get correct value from lookupvar") end end def test_declarative # set to declarative top = mkscope sub = mkscope(:parent => top) assert_nothing_raised { top.setvar("test","value") } assert_raise(Puppet::ParseError) { top.setvar("test","other") } assert_nothing_raised { sub.setvar("test","later") } assert_raise(Puppet::ParseError) { top.setvar("test","yeehaw") } end def test_setdefaults config = mkconfig scope = config.topscope defaults = scope.instance_variable_get("@defaults") # First the case where there are no defaults and we pass a single param param = stub :name => "myparam", :file => "f", :line => "l" scope.setdefaults(:mytype, param) assert_equal({"myparam" => param}, defaults[:mytype], "Did not set default correctly") # Now the case where we pass in multiple parameters param1 = stub :name => "one", :file => "f", :line => "l" param2 = stub :name => "two", :file => "f", :line => "l" scope.setdefaults(:newtype, [param1, param2]) assert_equal({"one" => param1, "two" => param2}, defaults[:newtype], "Did not set multiple defaults correctly") # And the case where there's actually a conflict. Use the first default for this. newparam = stub :name => "myparam", :file => "f", :line => "l" assert_raise(Puppet::ParseError, "Allowed resetting of defaults") do scope.setdefaults(:mytype, param) end assert_equal({"myparam" => param}, defaults[:mytype], "Replaced default even though there was a failure") end def test_lookupdefaults config = mkconfig top = config.topscope # Make a subscope sub = config.newscope(top) topdefs = top.instance_variable_get("@defaults") subdefs = sub.instance_variable_get("@defaults") # First add some defaults to our top scope topdefs[:t1] = {:p1 => :p2, :p3 => :p4} topdefs[:t2] = {:p5 => :p6} # Then the sub scope subdefs[:t1] = {:p1 => :p7, :p8 => :p9} subdefs[:t2] = {:p5 => :p10, :p11 => :p12} # Now make sure we get the correct list back result = nil assert_nothing_raised("Could not get defaults") do result = sub.lookupdefaults(:t1) end assert_equal(:p9, result[:p8], "Did not get child defaults") assert_equal(:p4, result[:p3], "Did not override parent defaults with child default") assert_equal(:p7, result[:p1], "Did not get parent defaults") end def test_parent config = mkconfig top = config.topscope # Make a subscope sub = config.newscope(top) assert_equal(top, sub.parent, "Did not find parent scope correctly") assert_equal(top, sub.parent, "Did not find parent scope on second call") end - - def test_class_scope - config = mkconfig - scope = config.topscope - config.expects(:class_scope).with(:testing).returns(:myscope) - assert_equal(:myscope, scope.class_scope(:testing), "Did not pass back the results of config.class_scope") - end def test_strinterp # Make and evaluate our classes so the qualified lookups work parser = mkparser klass = parser.newclass("") scope = mkscope(:parser => parser) klass.evaluate(:scope => scope) klass = parser.newclass("one") klass.evaluate(:scope => scope) klass = parser.newclass("one::two") klass.evaluate(:scope => scope) - scope = scope.class_scope("") + scope = scope.compile.class_scope("") assert_nothing_raised { scope.setvar("test","value") } scopes = {"" => scope} %w{one one::two one::two::three}.each do |name| klass = parser.newclass(name) klass.evaluate(:scope => scope) - scopes[name] = scope.class_scope(klass) + scopes[name] = scope.compile.class_scope(klass) scopes[name].setvar("test", "value-%s" % name.sub(/.+::/,'')) end assert_equal("value", scope.lookupvar("::test"), "did not look up qualified value correctly") tests = { "string ${test}" => "string value", "string ${one::two::three::test}" => "string value-three", "string $one::two::three::test" => "string value-three", "string ${one::two::test}" => "string value-two", "string $one::two::test" => "string value-two", "string ${one::test}" => "string value-one", "string $one::test" => "string value-one", "string ${::test}" => "string value", "string $::test" => "string value", "string ${test} ${test} ${test}" => "string value value value", "string $test ${test} $test" => "string value value value", "string \\$test" => "string $test", '\\$test string' => "$test string", '$test string' => "value string", 'a testing $' => "a testing $", 'a testing \$' => "a testing $", "an escaped \\\n carriage return" => "an escaped carriage return", '\$' => "$", '\s' => "\s", '\t' => "\t", '\n' => "\n" } tests.each do |input, output| assert_nothing_raised("Failed to scan %s" % input.inspect) do assert_equal(output, scope.strinterp(input), 'did not parserret %s correctly' % input.inspect) end end logs = [] Puppet::Util::Log.close Puppet::Util::Log.newdestination(logs) # #523 %w{d f h l w z}.each do |l| string = "\\" + l assert_nothing_raised do assert_equal(string, scope.strinterp(string), 'did not parserret %s correctly' % string) end assert(logs.detect { |m| m.message =~ /Unrecognised escape/ }, "Did not get warning about escape sequence with %s" % string) logs.clear end end - def test_setclass - # Run through it when we're a normal class - config = mkconfig - scope = config.topscope - klass = mock("class") - klass.expects(:classname).returns(:myclass) - klass.expects(:is_a?).with(AST::HostClass).returns(true) - klass.expects(:is_a?).with(AST::Node).returns(false) - config.expects(:class_set).with(:myclass, scope) - scope.setclass(klass) - - # And when we're a node - config = mkconfig - scope = config.topscope - klass = mock("class2") - klass.expects(:classname).returns(:myclass) - klass.expects(:is_a?).with(AST::HostClass).returns(true) - klass.expects(:is_a?).with(AST::Node).returns(true) - config.expects(:class_set).with(:myclass, scope) - scope.setclass(klass) - assert(scope.nodescope?, "Did not set the scope as a node scope when evaluating a node") - - # And when we're invalid - config = mkconfig - scope = config.topscope - klass = mock("class3") - klass.expects(:is_a?).with(AST::HostClass).returns(false) - assert_raise(Puppet::DevError, "Did not fail when scope got passed a non-component") do - scope.setclass(klass) - end - end - def test_validtags scope = mkscope() ["a class", "a.class"].each do |bad| assert_raise(Puppet::ParseError, "Incorrectly allowed %s" % bad.inspect) do scope.tag(bad) end end ["a-class", "a_class", "Class", "class", "yayNess"].each do |good| assert_nothing_raised("Incorrectly banned %s" % good.inspect) do scope.tag(good) end end end def test_tagfunction scope = mkscope assert_nothing_raised { scope.function_tag(["yayness", "booness"]) } assert(scope.tags.include?("yayness"), "tag 'yayness' did not get set") assert(scope.tags.include?("booness"), "tag 'booness' did not get set") # Now verify that the 'tagged' function works correctly assert(scope.function_tagged("yayness"), "tagged function incorrectly returned false") assert(scope.function_tagged("booness"), "tagged function incorrectly returned false") assert(! scope.function_tagged("funtest"), "tagged function incorrectly returned true") end def test_includefunction parser = mkparser scope = mkscope :parser => parser myclass = parser.newclass "myclass" otherclass = parser.newclass "otherclass" function = Puppet::Parser::AST::Function.new( :name => "include", :ftype => :statement, :arguments => AST::ASTArray.new( :children => [nameobj("myclass"), nameobj("otherclass")] ) ) assert_nothing_raised do function.evaluate :scope => scope end [myclass, otherclass].each do |klass| - assert(scope.class_scope(klass), + assert(scope.compile.class_scope(klass), "%s was not set" % klass.classname) end end def test_definedfunction parser = mkparser %w{one two}.each do |name| parser.newdefine name end scope = mkscope :parser => parser assert_nothing_raised { %w{one two file user}.each do |type| assert(scope.function_defined([type]), "Class #{type} was not considered defined") end assert(!scope.function_defined(["nopeness"]), "Class 'nopeness' was incorrectly considered defined") } end # Make sure we know what we consider to be truth. def test_truth assert_equal(true, Puppet::Parser::Scope.true?("a string"), "Strings not considered true") assert_equal(true, Puppet::Parser::Scope.true?(true), "True considered true") assert_equal(false, Puppet::Parser::Scope.true?(""), "Empty strings considered true") assert_equal(false, Puppet::Parser::Scope.true?(false), "false considered true") assert_equal(false, Puppet::Parser::Scope.true?(:undef), "undef considered true") end if defined? ActiveRecord # Verify that we recursively mark as exported the results of collectable # components. def test_exportedcomponents config = mkconfig parser = config.parser # Create a default source config.topscope.source = parser.newclass "", "" args = AST::ASTArray.new( :file => tempfile(), :line => rand(100), :children => [nameobj("arg")] ) # Create a top-level component parser.newdefine "one", :arguments => [%w{arg}], :code => AST::ASTArray.new( :children => [ resourcedef("file", "/tmp", {"owner" => varref("arg")}) ] ) # And a component that calls it parser.newdefine "two", :arguments => [%w{arg}], :code => AST::ASTArray.new( :children => [ resourcedef("one", "ptest", {"arg" => varref("arg")}) ] ) # And then a third component that calls the second parser.newdefine "three", :arguments => [%w{arg}], :code => AST::ASTArray.new( :children => [ resourcedef("two", "yay", {"arg" => varref("arg")}) ] ) # lastly, create an object that calls our third component obj = resourcedef("three", "boo", {"arg" => "parentfoo"}) # And mark it as exported obj.exported = true # And then evaluate it obj.evaluate :scope => config.topscope # And run the loop. config.send(:evaluate_generators) %w{file}.each do |type| objects = config.resources.find_all { |r| r.type == type and r.exported } assert(!objects.empty?, "Did not get an exported %s" % type) end end # Verify that we can both store and collect an object in the same # run, whether it's in the same scope as a collection or a different # scope. def test_storeandcollect Puppet[:storeconfigs] = true Puppet::Rails.init sleep 1 children = [] file = tempfile() File.open(file, "w") { |f| f.puts " class yay { @@host { myhost: ip => \"192.168.0.2\" } } include yay @@host { puppet: ip => \"192.168.0.3\" } Host <<||>>" } interp = nil assert_nothing_raised { interp = Puppet::Parser::Interpreter.new( :Manifest => file, :UseNodes => false, :ForkSave => false ) } objects = nil # We run it twice because we want to make sure there's no conflict # if we pull it up from the database. node = mknode node.parameters = {"hostname" => node.name} 2.times { |i| assert_nothing_raised { objects = interp.compile(node) } flat = objects.flatten %w{puppet myhost}.each do |name| assert(flat.find{|o| o.name == name }, "Did not find #{name}") end } end else $stderr.puts "No ActiveRecord -- skipping collection tests" end # Make sure tags behave appropriately. def test_tags parser, scope, source = mkclassframing # First make sure we can only set legal tags ["an invalid tag", "-anotherinvalid", "bad*tag"].each do |tag| assert_raise(Puppet::ParseError, "Tag #{tag} was considered valid") do scope.tag tag end end # Now make sure good tags make it through. tags = %w{good-tag yaytag GoodTag another_tag a ab A} tags.each do |tag| assert_nothing_raised("Tag #{tag} was considered invalid") do scope.tag tag end end # And make sure we get each of them. ptags = scope.tags tags.each do |tag| assert(ptags.include?(tag), "missing #{tag}") end # Now create a subscope and set some tags there newscope = scope.newscope(:type => 'subscope') # set some tags newscope.tag "onemore", "yaytag" # And make sure we get them plus our parent tags assert_equal((ptags + %w{onemore subscope}).sort, newscope.tags.sort) end # FIXME This isn't a great test, but I need to move on. def test_to_trans bucket = mock("transbucket") Puppet::TransBucket.expects(:new).with([]).returns(bucket) scope = mkscope scope.type = "mytype" scope.name = "myname" bucket.expects(:name=).with("myname") bucket.expects(:type=).with("mytype") scope.to_trans end def test_namespaces parser, scope, source = mkclassframing assert_equal([""], scope.namespaces, "Started out with incorrect namespaces") assert_nothing_raised { scope.add_namespace("fun::test") } assert_equal(["fun::test"], scope.namespaces, "Did not add namespace correctly") assert_nothing_raised { scope.add_namespace("yay::test") } assert_equal(["fun::test", "yay::test"], scope.namespaces, "Did not add extra namespace correctly") end def test_findclass_and_finddefine parser = mkparser # Make sure our scope calls the parser findclass method with # the right namespaces scope = mkscope :parser => parser parser.metaclass.send(:attr_accessor, :last) methods = [:findclass, :finddefine] methods.each do |m| parser.meta_def(m) do |namespace, name| @checked ||= [] @checked << [namespace, name] # Only return a value on the last call. if @last == namespace ret = @checked.dup @checked.clear return ret else return nil end end end test = proc do |should| parser.last = scope.namespaces[-1] methods.each do |method| result = scope.send(method, "testing") assert_equal(should, result, "did not get correct value from %s with namespaces %s" % [method, scope.namespaces.inspect]) end end # Start with the empty namespace assert_nothing_raised { test.call([["", "testing"]]) } # Now add a namespace scope.add_namespace("a") assert_nothing_raised { test.call([["a", "testing"]]) } # And another scope.add_namespace("b") assert_nothing_raised { test.call([["a", "testing"], ["b", "testing"]]) } end # #629 - undef should be "" or :undef def test_lookupvar_with_undef scope = mkscope scope.setvar("testing", :undef) assert_equal(:undef, scope.lookupvar("testing", false), "undef was not returned as :undef when not string") assert_equal("", scope.lookupvar("testing", true), "undef was not returned as '' when string") end end # $Id$