diff --git a/lib/puppet/parser/ast/definition.rb b/lib/puppet/parser/ast/definition.rb index b4a90016a..2b7506446 100644 --- a/lib/puppet/parser/ast/definition.rb +++ b/lib/puppet/parser/ast/definition.rb @@ -1,204 +1,204 @@ require 'puppet/parser/ast/branch' require 'puppet/util/warnings' # The AST class for defined types, which is also the base class # nodes and classes. class Puppet::Parser::AST::Definition < Puppet::Parser::AST::Branch include Puppet::Util::Warnings class << self attr_accessor :name end # The class name @name = :definition attr_accessor :classname, :arguments, :code, :scope, :keyword attr_accessor :exported, :namespace, :parser, :virtual, :name attr_reader :parentclass def child_of?(klass) false end # Create a resource that knows how to evaluate our actual code. def evaluate(scope) # Do nothing if the resource already exists; this provides the singleton nature classes need. - return if scope.catalog.resource(:class, self.classname) + return if scope.catalog.resource(self.class.name, self.classname) - resource = Puppet::Parser::Resource.new(:type => "class", :title => self.classname, :scope => scope, :source => scope.source) + resource = Puppet::Parser::Resource.new(:type => self.class.name, :title => self.classname, :scope => scope, :source => scope.source) scope.catalog.tag(*resource.tags) scope.compiler.add_resource(scope, resource) return resource end # Now evaluate the code associated with this class or definition. def evaluate_code(resource) # Create a new scope. scope = subscope(resource.scope, resource) set_resource_parameters(scope, resource) if self.code return self.code.safeevaluate(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 parameter name in the %s definition" % [arg, self.classname] 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 return nil unless @parentclass # Cache our result, since it should never change. unless defined?(@parentobj) unless tmp = find_parentclass parsefail "Could not find %s parent %s" % [self.class.name, @parentclass] end if tmp == self parsefail "Parent classes must have dissimilar names" end @parentobj = tmp end @parentobj end # Create a new subscope in which to evaluate our code. def subscope(scope, resource) args = { :resource => resource, :keyword => self.keyword, :namespace => self.namespace, :source => self } 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 = arg.to_sym unless args.include?(arg) if defined? default and ! default.nil? default = default.safeevaluate 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, resource.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 diff --git a/lib/puppet/parser/ast/hostclass.rb b/lib/puppet/parser/ast/hostclass.rb index f49016526..8d4d01660 100644 --- a/lib/puppet/parser/ast/hostclass.rb +++ b/lib/puppet/parser/ast/hostclass.rb @@ -1,77 +1,80 @@ require 'puppet/parser/ast/definition' # 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 Puppet::Parser::AST::HostClass < Puppet::Parser::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 # Make sure our parent class has been evaluated, if we have one. def evaluate(scope) - if parentclass and ! scope.catalog.resource(:class, parentclass) + if parentclass and ! scope.catalog.resource(self.class.name, parentclass) resource = parentobj.evaluate(scope) end super end # Evaluate the code associated with this class. def evaluate_code(resource) scope = resource.scope # Verify that we haven't already been evaluated. This is # what provides the singleton aspect. if existing_scope = scope.compiler.class_scope(self) Puppet.debug "Class '%s' already evaluated; not evaluating again" % (classname == "" ? "main" : classname) return nil end pnames = nil if pklass = self.parentobj - pklass.evaluate_code(resource) + parent_resource = resource.scope.compiler.catalog.resource(self.class.name, pklass.classname) + # This shouldn't evaluate if the class has already been evaluated. + pklass.evaluate_code(parent_resource) scope = parent_scope(scope, pklass) pnames = scope.namespaces end # Don't create a subscope for the top-level class, since it already # has its own scope. scope = subscope(scope, resource) unless resource.title == :main + # Add the parent scope namespaces to our own. 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. + # Set the class before we evaluate the code, so that it's set during + # the evaluation and can be inspected. scope.compiler.class_set(self.classname, scope) # Now evaluate our code, yo. if self.code return self.code.safeevaluate(scope) else return nil end end def parent_scope(scope, klass) if s = scope.compiler.class_scope(klass) return s else raise Puppet::DevError, "Could not find scope for %s" % klass.classname end end end diff --git a/lib/puppet/parser/ast/node.rb b/lib/puppet/parser/ast/node.rb index 8cebac8a8..2bf6c1882 100644 --- a/lib/puppet/parser/ast/node.rb +++ b/lib/puppet/parser/ast/node.rb @@ -1,57 +1,35 @@ require 'puppet/parser/ast/hostclass' # 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 Puppet::Parser::AST::Node < Puppet::Parser::AST::HostClass @name = :node - # Evaluate the code associated with our node definition. - def evaluate_code(resource) - scope = resource.scope - - # 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.evaluate_code(resource) - end - - scope = scope.newscope( - :resource => resource, - :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.compiler.class_set(self.classname, scope) - - # And then evaluate our code if we have any - @code.safeevaluate(scope) if self.code - - 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 + def namespace + "" + end + # Make sure node scopes are marked as such. def subscope(*args) scope = super scope.nodescope = true + scope end private + # Search for the object matching our parent class. def find_parentclass @parser.findnode(parentclass) end end diff --git a/lib/puppet/parser/compiler.rb b/lib/puppet/parser/compiler.rb index 26fdd3743..68c06e500 100644 --- a/lib/puppet/parser/compiler.rb +++ b/lib/puppet/parser/compiler.rb @@ -1,467 +1,468 @@ # Created by Luke A. Kanies on 2007-08-13. # Copyright (c) 2007. All rights reserved. require 'puppet/node' require 'puppet/node/catalog' require 'puppet/util/errors' # Maintain a graph of scopes, along with a bunch of data # about the individual catalog we're compiling. class Puppet::Parser::Compiler include Puppet::Util include Puppet::Util::Errors attr_reader :parser, :node, :facts, :collections, :catalog, :node_scope # Add a collection to the global list. def add_collection(coll) @collections << coll end # Store a resource override. def add_override(override) # If possible, merge the override in immediately. if resource = @catalog.resource(override.ref) resource.merge(override) else # Otherwise, store the override for later; these # get evaluated in Resource#finish. @resource_overrides[override.ref] << override end end # Store a resource in our resource table. def add_resource(scope, resource) + # Note that this will fail if the resource is not unique. @catalog.add_resource(resource) # And in the resource graph. At some point, this might supercede # the global resource table, but the table is a lot faster # so it makes sense to maintain for now. @catalog.add_edge(scope.resource, resource) end # Do we use nodes found in the code, vs. the external node sources? def ast_nodes? parser.nodes.length > 0 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? + if existing.nodescope? != 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" + raise Puppet::DevError, "Somehow evaluated %s %s twice" % [ existing.nodescope? ? "node" : "class", name] end end @class_scopes[name] = scope @catalog.add_class(name) unless 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 @catalog.classes end # Compiler our catalog. This mostly revolves around finding and evaluating classes. # This is the main entry into our catalog. def compile # Set the client's parameters into the top scope. set_node_parameters() evaluate_main() evaluate_ast_node() evaluate_node_classes() evaluate_generators() finish() fail_on_unevaluated() if Puppet[:storeconfigs] store() end return @catalog end # LAK:FIXME There are no tests for this. def delete_collection(coll) @collections.delete(coll) if @collections.include?(coll) end # Return the node's environment. def environment unless defined? @environment if node.environment and node.environment != "" @environment = node.environment else @environment = nil end end @environment end # Evaluate all of the classes specified by the node. def evaluate_node_classes evaluate_classes(@node.classes, topscope) end # Evaluate each specified class in turn. If there are any classes we can't # find, just tag the catalog and move on. This method really just # creates resource objects that point back to the classes, and then the # resources are themselves evaluated later in the process. def evaluate_classes(classes, scope, lazy_evaluate = true) unless scope.source raise Puppet::DevError, "No source for scope passed to evaluate_classes" end found = [] classes.each do |name| # If we can find the class, then make a resource that will evaluate it. if klass = scope.findclass(name) found << name and next if class_scope(klass) resource = klass.evaluate(scope) # If they've disabled lazy evaluation (which the :include function does), # then evaluate our resource immediately. resource.evaluate unless lazy_evaluate found << name else Puppet.info "Could not find class %s for %s" % [name, node.name] @catalog.tag(name) end end found end # Return a resource by either its ref or its type and title. def findresource(*args) @catalog.resource(*args) end # Set up our compile. We require a parser # and a node object; the parser is so we can look up classes # and AST nodes, and the node has all of the client's info, # like facts and environment. def initialize(node, parser, options = {}) @node = node @parser = parser options.each do |param, value| begin send(param.to_s + "=", value) rescue NoMethodError raise ArgumentError, "Compiler objects do not accept %s" % param end end initvars() init_main() end # Create a new scope, with either a specified parent scope or # using the top scope. Adds an edge between the scope and # its parent to the graph. def newscope(parent, options = {}) parent ||= topscope options[:compiler] = 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 @catalog.vertices end # The top scope is usually the top-level scope, but if we're using AST nodes, # then it is instead the node's scope. def topscope node_scope || @topscope end private # If ast nodes are enabled, then see if we can find and evaluate one. def evaluate_ast_node return unless ast_nodes? # Now see if we can find the node. astnode = nil @node.names.each do |name| break if astnode = @parser.nodes[name.to_s.downcase] end unless (astnode ||= @parser.nodes["default"]) raise Puppet::ParseError, "Could not find default node or by name with '%s'" % node.names.join(", ") end # Create a resource to model this node, and then add it to the list # of resources. resource = astnode.evaluate(topscope) resource.evaluate # Now set the node scope appropriately, so that :topscope can # behave differently. @node_scope = class_scope(astnode) end # Evaluate our collections and return true if anything returned an object. # The 'true' is used to continue a loop, so it's important. def evaluate_collections return false if @collections.empty? found_something = false exceptwrap do # We have to iterate over a dup of the array because # collections can delete themselves from the list, which # changes its length and causes some collections to get missed. @collections.dup.each do |collection| found_something = true if collection.evaluate end end return found_something end # Make sure all of our resources have been evaluated into native resources. # We return true if any resources have, so that we know to continue the # evaluate_generators loop. def evaluate_definitions exceptwrap do if ary = unevaluated_resources 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 count += 1 if count > 1000 raise Puppet::ParseError, "Somehow looped more than 1000 times while evaluating host catalog" end end end # Find and evaluate our main object, if possible. def evaluate_main @main = @parser.findclass("", "") || @parser.newclass("") @topscope.source = @main @main_resource = Puppet::Parser::Resource.new(:type => "class", :title => :main, :scope => @topscope, :source => @main) @topscope.resource = @main_resource @catalog.add_resource(@main_resource) @main_resource.evaluate end # Make sure the entire catalog is evaluated. def fail_on_unevaluated fail_on_unevaluated_overrides fail_on_unevaluated_resource_collections end # If there are any resource overrides remaining, then we could # not find the resource they were supposed to override, so we # want to throw an exception. def fail_on_unevaluated_overrides remaining = [] @resource_overrides.each do |name, overrides| remaining += overrides end unless remaining.empty? fail Puppet::ParseError, "Could not find 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 @catalog.resources.each do |name| resource = @catalog.resource(name) # Add in any resource overrides. if overrides = resource_overrides(resource) overrides.each do |over| resource.merge(over) end # Remove the overrides, so that the configuration knows there # are none left. overrides.clear end resource.finish if resource.respond_to?(:finish) end end # Initialize the top-level scope, class, and resource. def init_main # Create our initial scope and a resource that will evaluate main. @topscope = Puppet::Parser::Scope.new(:compiler => self, :parser => self.parser) @scope_graph.add_vertex(@topscope) 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 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 graph for maintaining scope relationships. @scope_graph = Puppet::SimpleGraph.new # For maintaining the relationship between scopes and their resources. @catalog = Puppet::Node::Catalog.new(@node.name) @catalog.version = @parser.version 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 catalog 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, @catalog.vertices) 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 catalog 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 # 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 = @catalog.vertices.reject { |resource| resource.builtin? or resource.evaluated? } if ary.empty? return nil else return ary end end end diff --git a/lib/puppet/parser/resource/reference.rb b/lib/puppet/parser/resource/reference.rb index ea53b421a..c59748049 100644 --- a/lib/puppet/parser/resource/reference.rb +++ b/lib/puppet/parser/resource/reference.rb @@ -1,85 +1,89 @@ require 'puppet/resource_reference' # A reference to a resource. Mostly just the type and title. class Puppet::Parser::Resource::Reference < Puppet::ResourceReference include Puppet::Util::MethodHelper include Puppet::Util::Errors attr_accessor :builtin, :file, :line, :scope # Are we a builtin type? def builtin? unless defined? @builtin if builtintype() @builtin = true else @builtin = false end end @builtin end def builtintype if t = Puppet::Type.type(self.type.downcase) and t.name != :component t else nil end end # Return the defined type for our obj. This can return classes, # definitions or nodes. def definedtype unless defined? @definedtype case self.type when "Class": # look for host classes if self.title == :main tmp = @scope.findclass("") else - tmp = @scope.findclass(self.title) + unless tmp = @scope.findclass(self.title) + fail Puppet::ParseError, "Could not find class '%s'" % self.title + end end when "Node": # look for node definitions - tmp = @scope.parser.nodes[self.title] + unless tmp = @scope.parser.nodes[self.title] + fail Puppet::ParseError, "Could not find node '%s'" % self.title + end else # normal definitions # We have to swap these variables around so the errors are right. tmp = @scope.finddefine(self.type) end if tmp @definedtype = tmp else fail Puppet::ParseError, "Could not find resource type '%s'" % self.type end end @definedtype end def initialize(hash) set_options(hash) requiredopts(:type, :title) end def to_ref # We have to return different cases to provide backward compatibility # from 0.24.x to 0.23.x. if builtin? return [type.to_s.downcase, title.to_s] else return [type.to_s, title.to_s] end end def typeclass unless defined? @typeclass if tmp = builtintype || definedtype @typeclass = tmp else fail Puppet::ParseError, "Could not find type %s" % self.type end end @typeclass end end diff --git a/spec/unit/parser/ast/definition.rb b/spec/unit/parser/ast/definition.rb index ba80894e8..f236e23b7 100755 --- a/spec/unit/parser/ast/definition.rb +++ b/spec/unit/parser/ast/definition.rb @@ -1,47 +1,195 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' describe Puppet::Parser::AST::Definition, "when initializing" do end describe Puppet::Parser::AST::Definition, "when evaluating" do before do @type = Puppet::Parser::Resource @parser = Puppet::Parser::Parser.new :Code => "" @source = @parser.newclass "" @definition = @parser.newdefine "mydefine" @node = Puppet::Node.new("yaynode") @compiler = Puppet::Parser::Compiler.new(@node, @parser) @scope = @compiler.topscope @resource = Puppet::Parser::Resource.new(:type => "mydefine", :title => "myresource", :scope => @scope, :source => @source) end it "should create a new scope" do scope = nil code = mock 'code' code.expects(:safeevaluate).with do |scope| scope.object_id.should_not == @scope.object_id true end @definition.stubs(:code).returns(code) @definition.evaluate_code(@resource) end # it "should copy its namespace to the scope" # # it "should mark the scope virtual if the resource is virtual" # # it "should mark the scope exported if the resource is exported" # # it "should set the resource's parameters as variables in the scope" # # it "should set the resource's title as a variable in the scope" # # it "should copy the resource's title in a 'name' variable in the scope" # # it "should not copy the resource's title as the name if 'name' is one of the resource parameters" # # it "should evaluate the associated code with the new scope" + + 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 = mkcompiler + config.send(:evaluate_main) + 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 = Puppet::Parser::Resource.new( + :title => "first", + :type => "yayness", + :exported => false, + :virtual => false, + :scope => scope, + :source => scope.source + ) + resource.send(:set_parameter, "name", "first") + resource.send(:set_parameter, "mode", "755") + + resource.stubs(:title) + assert_nothing_raised do + klass.evaluate_code(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_code(resource) + end + + # Now create another with different args + resource2 = Puppet::Parser::Resource.new( + :title => "second", + :type => "yayness", + :exported => false, + :virtual => false, + :scope => scope, + :source => scope.source + ) + resource2.send(:set_parameter, "name", "second") + resource2.send(:set_parameter, "mode", "755") + resource2.send(:set_parameter, "owner", "daemon") + + assert_nothing_raised do + klass.evaluate_code(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 + + resource = Puppet::Parser::Resource.new( + :title => hash[:title], + :type => "yayness%s" % i, + :exported => false, + :virtual => false, + :scope => scope, + :source => scope.source + ) + + 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_code(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/spec/unit/parser/ast/node.rb b/spec/unit/parser/ast/node.rb new file mode 100755 index 000000000..340630194 --- /dev/null +++ b/spec/unit/parser/ast/node.rb @@ -0,0 +1,127 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +module ASTNodeTesting + def setup + @node = Puppet::Node.new "testnode" + @parser = Puppet::Parser::Parser.new :environment => "development" + @scope_resource = stub 'scope_resource', :builtin? => true + @compiler = Puppet::Parser::Compiler.new(@node, @parser) + + @scope = @compiler.topscope + end +end + +describe Puppet::Parser::AST::Node, "when evaluating" do + include ASTNodeTesting + + before do + @top = @parser.newnode("top").shift + @middle = @parser.newnode("middle", :parent => "top").shift + end + + it "should create a resource that references itself" do + @top.evaluate(@scope) + + @compiler.catalog.resource(:node, "top").should be_an_instance_of(Puppet::Parser::Resource) + end + + it "should evaluate the parent class if one exists" do + @middle.evaluate(@scope) + + @compiler.catalog.resource(:node, "top").should be_an_instance_of(Puppet::Parser::Resource) + end + + it "should fail to evaluate if a parent class is defined but cannot be found" do + othertop = @parser.newnode("something", :parent => "yay").shift + lambda { othertop.evaluate(@scope) }.should raise_error(Puppet::ParseError) + end + + it "should not create a new resource if one already exists" do + @compiler.catalog.expects(:resource).with(:node, "top").returns("something") + @compiler.catalog.expects(:add_resource).never + @top.evaluate(@scope) + end + + it "should not create a new parent resource if one already exists and it has a parent class" do + @top.evaluate(@scope) + + top_resource = @compiler.catalog.resource(:node, "top") + + @middle.evaluate(@scope) + + @compiler.catalog.resource(:node, "top").should equal(top_resource) + end + + # #795 - tag before evaluation. + it "should tag the catalog with the resource tags when it is evaluated" do + @middle.evaluate(@scope) + + @compiler.catalog.should be_tagged("middle") + end + + it "should tag the catalog with the parent class tags when it is evaluated" do + @middle.evaluate(@scope) + + @compiler.catalog.should be_tagged("top") + end +end + +describe Puppet::Parser::AST::Node, "when evaluating code" do + include ASTNodeTesting + + before do + @top_resource = stub "top_resource" + @top = @parser.newnode("top", :code => @top_resource).shift + + @middle_resource = stub "middle_resource" + @middle = @parser.newnode("middle", :parent => "top", :code => @middle_resource).shift + end + + it "should evaluate the code referred to by the class" do + @top_resource.expects(:safeevaluate) + + resource = @top.evaluate(@scope) + + @top.evaluate_code(resource) + end + + it "should evaluate the parent class's code if it has a parent" do + @top_resource.expects(:safeevaluate) + @middle_resource.expects(:safeevaluate) + + resource = @middle.evaluate(@scope) + + @middle.evaluate_code(resource) + end + + it "should not evaluate the parent class's code if the parent has already been evaluated" do + @top_resource.stubs(:safeevaluate) + resource = @top.evaluate(@scope) + @top.evaluate_code(resource) + + @top_resource.expects(:safeevaluate).never + @middle_resource.stubs(:safeevaluate) + resource = @middle.evaluate(@scope) + @middle.evaluate_code(resource) + end + + it "should use the parent class's scope as its parent scope" do + @top_resource.stubs(:safeevaluate) + @middle_resource.stubs(:safeevaluate) + resource = @middle.evaluate(@scope) + @middle.evaluate_code(resource) + + @compiler.class_scope(@middle).parent.should equal(@compiler.class_scope(@top)) + end + + it "should add the parent class's namespace to its namespace search path" do + @top_resource.stubs(:safeevaluate) + @middle_resource.stubs(:safeevaluate) + resource = @middle.evaluate(@scope) + @middle.evaluate_code(resource) + + @compiler.class_scope(@middle).namespaces.should be_include(@top.namespace) + end +end diff --git a/test/language/ast/definition.rb b/test/language/ast/definition.rb deleted file mode 100755 index 1585c5b1d..000000000 --- a/test/language/ast/definition.rb +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env ruby -# -# Created by Luke A. Kanies on 2006-02-20. -# Copyright (c) 2006. All rights reserved. - -require File.dirname(__FILE__) + '/../../lib/puppettest' - -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 = mkcompiler - config.send(:evaluate_main) - 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 = Puppet::Parser::Resource.new( - :title => "first", - :type => "yayness", - :exported => false, - :virtual => false, - :scope => scope, - :source => scope.source - ) - resource.send(:set_parameter, "name", "first") - resource.send(:set_parameter, "mode", "755") - - resource.stubs(:title) - assert_nothing_raised do - klass.evaluate_code(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_code(resource) - end - - # Now create another with different args - resource2 = Puppet::Parser::Resource.new( - :title => "second", - :type => "yayness", - :exported => false, - :virtual => false, - :scope => scope, - :source => scope.source - ) - resource2.send(:set_parameter, "name", "second") - resource2.send(:set_parameter, "mode", "755") - resource2.send(:set_parameter, "owner", "daemon") - - assert_nothing_raised do - klass.evaluate_code(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 - - resource = Puppet::Parser::Resource.new( - :title => hash[:title], - :type => "yayness%s" % i, - :exported => false, - :virtual => false, - :scope => scope, - :source => scope.source - ) - - 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_code(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/node.rb b/test/language/ast/node.rb deleted file mode 100755 index a90f05adf..000000000 --- a/test/language/ast/node.rb +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env ruby -# -# Created by Luke A. Kanies on 2008-02-09. -# Copyright (c) 2008. All rights reserved. - -require File.dirname(__FILE__) + '/../../lib/puppettest' - -require 'puppettest' -require 'mocha' -require 'puppettest/parsertesting' -require 'puppettest/resourcetesting' - -class TestASTNode < Test::Unit::TestCase - include PuppetTest - include PuppetTest::ParserTesting - include PuppetTest::ResourceTesting - AST = Puppet::Parser::AST - - def test_node - scope = mkscope - parser = scope.compiler.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_code scope.resource - 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_code newscope.resource - end - - assert(newscope.findresource("File[/tmp/base]"), - "Could not find base resource") - end -end