diff --git a/lib/puppet/parser/ast/definition.rb b/lib/puppet/parser/ast/definition.rb index 2b7506446..0c65c702c 100644 --- a/lib/puppet/parser/ast/definition.rb +++ b/lib/puppet/parser/ast/definition.rb @@ -1,204 +1,201 @@ 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(self.class.name, self.classname) - 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 8d4d01660..7f89f8151 100644 --- a/lib/puppet/parser/ast/hostclass.rb +++ b/lib/puppet/parser/ast/hostclass.rb @@ -1,80 +1,87 @@ 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(self.class.name, parentclass) - resource = parentobj.evaluate(scope) + parent_resource = parentobj.evaluate(scope) + end + + # Do nothing if the resource already exists; this makes sure we don't + # get multiple copies of the class resource, which helps provide the + # singleton nature of classes. + if resource = scope.catalog.resource(self.class.name, self.classname) + return resource 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 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 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/spec/unit/parser/ast/hostclass.rb b/spec/unit/parser/ast/hostclass.rb index a53c3b092..0abc174d9 100755 --- a/spec/unit/parser/ast/hostclass.rb +++ b/spec/unit/parser/ast/hostclass.rb @@ -1,129 +1,135 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' describe Puppet::Parser::AST::HostClass do before :each do @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 describe Puppet::Parser::AST::HostClass, "when evaluating" do before do @top = @parser.newclass "top" @middle = @parser.newclass "middle", :parent => "top" end it "should create a resource that references itself" do @top.evaluate(@scope) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should evaluate the parent class if one exists" do @middle.evaluate(@scope) @compiler.catalog.resource(:class, "top").should be_instance_of(Puppet::Parser::Resource) end it "should fail to evaluate if a parent class is defined but cannot be found" do othertop = @parser.newclass "something", :parent => "yay" 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(:class, "top").returns("something") @compiler.catalog.expects(:add_resource).never @top.evaluate(@scope) end + it "should return the existing resource when not creating a new one" do + @compiler.catalog.expects(:resource).with(:class, "top").returns("something") + @compiler.catalog.expects(:add_resource).never + @top.evaluate(@scope).should == "something" + 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(:class, "top") @middle.evaluate(@scope) @compiler.catalog.resource(:class, "top").should equal(top_resource) end # #795 - tag before evaluation. it "should tag the catalog with the resource tags when it is evaluated" do @middle.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::HostClass, "when evaluating code" do before do @top_resource = stub "top_resource" @top = @parser.newclass "top", :code => @top_resource @middle_resource = stub "middle_resource" @middle = @parser.newclass "top::middle", :parent => "top", :code => @middle_resource end it "should set its namespace to its fully qualified name" do @middle.namespace.should == "top::middle" 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 -end \ No newline at end of file +end