diff --git a/lib/puppet/parser/ast/resourcedef.rb b/lib/puppet/parser/ast/resourcedef.rb index 1c9333030..dfd7c447e 100644 --- a/lib/puppet/parser/ast/resourcedef.rb +++ b/lib/puppet/parser/ast/resourcedef.rb @@ -1,215 +1,222 @@ require 'puppet/parser/ast/branch' # Any normal puppet object declaration. Can result in a class or a # component, in addition to builtin types. class Puppet::Parser::AST class ResourceDef < AST::Branch attr_accessor :title, :type, :exported, :virtual attr_reader :params # probably not used at all def []=(index,obj) @params[index] = obj end # probably not used at all def [](index) return @params[index] end # Iterate across all of our children. def each [@type,@title,@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] @scope = scope hash = {} # Get our type and name. objtype = @type # Disable definition inheritance, for now. 8/27/06, luke #if objtype == "super" # objtype = supertype() # @subtype = true #else @subtype = false #end # Evaluate all of the specified params. paramobjects = @params.collect { |param| param.safeevaluate(:scope => scope) } # Now collect info from our parent. parentname = nil if @subtype parentname = supersetup(hash) end objtitles = nil # Determine our name if we have one. if self.title objtitles = @title.safeevaluate(:scope => scope) # it's easier to always use an array, even for only one name unless objtitles.is_a?(Array) objtitles = [objtitles] end else if parentname objtitles = [parentname] else # See if they specified the name as a parameter instead of # as a normal name (i.e., before the colon). unless object # we're a builtin if objclass = Puppet::Type.type(objtype) namevar = objclass.namevar tmp = hash["name"] || hash[namevar.to_s] if tmp objtitles = [tmp] end else # This isn't grammatically legal. raise Puppet::ParseError, "Got a resource with no title" end end end end # This is where our implicit iteration takes place; if someone # passed an array as the name, then we act just like the called us # many times. objtitles.collect { |objtitle| exceptwrap :type => Puppet::ParseError do + exp = self.exported || scope.exported + # We want virtual to be true if exported is true. We can't + # just set :virtual => self.virtual in the initialization, + # because sometimes the :virtual attribute is set *after* + # :exported, in which case it clobbers :exported if :exported + # is true. Argh, this was a very tough one to track down. + virt = self.virtual || exported obj = Puppet::Parser::Resource.new( :type => objtype, :title => objtitle, :params => paramobjects, :file => @file, :line => @line, - :exported => self.exported || scope.exported, - :virtual => self.virtual, + :exported => exp, + :virtual => virt, :source => scope.source, :scope => scope ) # And then store the resource in the scope. # XXX At some point, we need to switch all of this to return # objects instead of storing them like this. scope.setresource(obj) obj end }.reject { |obj| obj.nil? } end # Create our ResourceDef. Handles type checking for us. def initialize(hash) @checked = false super #self.typecheck(@type.value) end # Set the parameters for our object. def params=(params) if params.is_a?(AST::ASTArray) @params = params else @params = AST::ASTArray.new( :line => params.line, :file => params.file, :children => [params] ) end end def supercomp unless defined? @supercomp if @scope and comp = @scope.inside @supercomp = comp else error = Puppet::ParseError.new( "'super' is only valid within definitions" ) error.line = self.line error.file = self.file raise error end end @supercomp end # Take all of the arguments of our parent and add them into our own, # without overriding anything. def supersetup(hash) comp = supercomp() # Now check each of the arguments from the parent. comp.arguments.each do |name, value| unless hash.has_key? name hash[name] = value end end # Return the parent name, so it can be used if appropriate. return comp.name end # Retrieve our supertype. def supertype unless defined? @supertype if parent = supercomp.parentclass @supertype = parent else error = Puppet::ParseError.new( "%s does not have a parent class" % comp.type ) error.line = self.line error.file = self.file raise error end end @supertype end # Print this object out. def tree(indent = 0) return [ @type.tree(indent + 1), @title.tree(indent + 1), ((@@indline * indent) + self.typewrap(self.pin)), @params.collect { |param| begin param.tree(indent + 1) rescue NoMethodError => detail Puppet.err @params.inspect error = Puppet::DevError.new( "failed to tree a %s" % self.class ) error.set_backtrace detail.backtrace raise error end }.join("\n") ].join("\n") end def to_s return "%s => { %s }" % [@title, @params.collect { |param| param.to_s }.join("\n") ] end end end # $Id$ diff --git a/test/language/parser.rb b/test/language/parser.rb index fe45d5adb..44b1f8fd1 100755 --- a/test/language/parser.rb +++ b/test/language/parser.rb @@ -1,614 +1,614 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppet/parser/parser' require 'puppettest' class TestParser < Test::Unit::TestCase include PuppetTest::ParserTesting def setup super Puppet[:parseonly] = true #@lexer = Puppet::Parser::Lexer.new() end def test_each_file textfiles { |file| parser = mkparser Puppet.debug("parsing %s" % file) if __FILE__ == $0 assert_nothing_raised() { parser.file = file parser.parse } Puppet::Type.eachtype { |type| type.each { |obj| assert(obj.file) assert(obj.name) assert(obj.line) } } Puppet::Type.allclear } end def test_failers failers { |file| parser = mkparser Puppet.debug("parsing failer %s" % file) if __FILE__ == $0 assert_raise(Puppet::ParseError) { parser.file = file ast = parser.parse scope = mkscope :interp => parser.interp ast.evaluate :scope => scope } Puppet::Type.allclear } end def test_arrayrvalues parser = mkparser ret = nil file = tempfile() assert_nothing_raised { parser.string = "file { \"#{file}\": mode => [755, 640] }" } assert_nothing_raised { ret = parser.parse } end def mkmanifest(file) name = File.join(tmpdir, "file%s" % rand(100)) @@tmpfiles << name File.open(file, "w") { |f| f.puts "file { \"%s\": ensure => file, mode => 755 }\n" % name } end def test_importglobbing basedir = File.join(tmpdir(), "importesting") @@tmpfiles << basedir Dir.mkdir(basedir) subdir = "subdir" Dir.mkdir(File.join(basedir, subdir)) manifest = File.join(basedir, "manifest") File.open(manifest, "w") { |f| f.puts "import \"%s/*\"" % subdir } 4.times { |i| path = File.join(basedir, subdir, "subfile%s" % i) mkmanifest(path) } assert_nothing_raised("Could not parse multiple files") { parser = mkparser parser.file = manifest parser.parse } end def test_nonexistent_import basedir = File.join(tmpdir(), "importesting") @@tmpfiles << basedir Dir.mkdir(basedir) manifest = File.join(basedir, "manifest") File.open(manifest, "w") do |f| f.puts "import \" no such file \"" end assert_raise(Puppet::ParseError) { parser = mkparser parser.file = manifest parser.parse } end def test_trailingcomma path = tempfile() str = %{file { "#{path}": ensure => file, } } parser = mkparser parser.string = str assert_nothing_raised("Could not parse trailing comma") { parser.parse } end def test_importedclasses imported = tempfile() importer = tempfile() made = tempfile() File.open(imported, "w") do |f| f.puts %{class foo { file { "#{made}": ensure => file }}} end File.open(importer, "w") do |f| f.puts %{import "#{imported}"\ninclude foo} end parser = mkparser parser.file = importer # Make sure it parses fine assert_nothing_raised { parser.parse } # Now make sure it actually does the work assert_creates(importer, made) end # Make sure fully qualified and unqualified files can be imported def test_fqfilesandlocalfiles dir = tempfile() Dir.mkdir(dir) importer = File.join(dir, "site.pp") fullfile = File.join(dir, "full.pp") localfile = File.join(dir, "local.pp") files = [] File.open(importer, "w") do |f| f.puts %{import "#{fullfile}"\ninclude full\nimport "local.pp"\ninclude local} end fullmaker = tempfile() files << fullmaker File.open(fullfile, "w") do |f| f.puts %{class full { file { "#{fullmaker}": ensure => file }}} end localmaker = tempfile() files << localmaker File.open(localfile, "w") do |f| f.puts %{class local { file { "#{localmaker}": ensure => file }}} end parser = mkparser parser.file = importer # Make sure it parses assert_nothing_raised { parser.parse } # Now make sure it actually does the work assert_creates(importer, *files) end # Make sure the parser adds '.pp' when necessary def test_addingpp dir = tempfile() Dir.mkdir(dir) importer = File.join(dir, "site.pp") localfile = File.join(dir, "local.pp") files = [] File.open(importer, "w") do |f| f.puts %{import "local"\ninclude local} end file = tempfile() files << file File.open(localfile, "w") do |f| f.puts %{class local { file { "#{file}": ensure => file }}} end parser = mkparser parser.file = importer assert_nothing_raised { parser.parse } end # Make sure that file importing changes file relative names. def test_changingrelativenames dir = tempfile() Dir.mkdir(dir) Dir.mkdir(File.join(dir, "subdir")) top = File.join(dir, "site.pp") subone = File.join(dir, "subdir/subone") subtwo = File.join(dir, "subdir/subtwo") files = [] file = tempfile() files << file File.open(subone + ".pp", "w") do |f| f.puts %{class one { file { "#{file}": ensure => file }}} end otherfile = tempfile() files << otherfile File.open(subtwo + ".pp", "w") do |f| f.puts %{import "subone"\n class two inherits one { file { "#{otherfile}": ensure => file } }} end File.open(top, "w") do |f| f.puts %{import "subdir/subtwo"} end parser = mkparser parser.file = top assert_nothing_raised { parser.parse } end # Defaults are purely syntactical, so it doesn't make sense to be able to # collect them. def test_uncollectabledefaults string = "@Port { protocols => tcp }" assert_raise(Puppet::ParseError) { mkparser.parse(string) } end # Verify that we can parse collections def test_collecting Puppet[:storeconfigs] = true text = "Port <| |>" parser = mkparser parser.string = text ret = nil assert_nothing_raised { ret = parser.parse } assert_instance_of(AST::ASTArray, ret) ret.each do |obj| assert_instance_of(AST::Collection, obj) end end def test_emptyfile file = tempfile() File.open(file, "w") do |f| f.puts %{} end parser = mkparser parser.file = file assert_nothing_raised { parser.parse } end def test_multiple_nodes_named file = tempfile() other = tempfile() File.open(file, "w") do |f| f.puts %{ node nodeA, nodeB { file { "#{other}": ensure => file } } } end parser = mkparser parser.file = file ast = nil assert_nothing_raised { ast = parser.parse } end def test_emptyarrays str = %{$var = []\n} parser = mkparser parser.string = str # Make sure it parses fine assert_nothing_raised { parser.parse } end # Make sure function names aren't reserved words. def test_functionnamecollision str = %{tag yayness tag(rahness) file { "/tmp/yayness": tag => "rahness", ensure => exists } } parser = mkparser parser.string = str # Make sure it parses fine assert_nothing_raised { parser.parse } end def test_metaparams_in_definition_prototypes parser = mkparser assert_raise(Puppet::ParseError) { parser.parse %{define mydef($schedule) {}} } assert_nothing_raised { parser.parse %{define adef($schedule = false) {}} parser.parse %{define mydef($schedule = daily) {}} } end def test_parsingif parser = mkparser exec = proc do |val| %{exec { "/bin/echo #{val}": logoutput => true }} end str1 = %{if true { #{exec.call("true")} }} ret = nil assert_nothing_raised { ret = parser.parse(str1)[0] } assert_instance_of(Puppet::Parser::AST::IfStatement, ret) str2 = %{if true { #{exec.call("true")} } else { #{exec.call("false")} }} assert_nothing_raised { ret = parser.parse(str2)[0] } assert_instance_of(Puppet::Parser::AST::IfStatement, ret) assert_instance_of(Puppet::Parser::AST::Else, ret.else) end def test_hostclass parser = mkparser interp = parser.interp assert_nothing_raised { parser.parse %{class myclass { class other {} }} } assert(interp.findclass("", "myclass"), "Could not find myclass") assert(interp.findclass("", "myclass::other"), "Could not find myclass::other") assert_nothing_raised { parser.parse "class base {} class container { class deep::sub inherits base {} }" } sub = interp.findclass("", "container::deep::sub") assert(sub, "Could not find sub") assert_equal("base", sub.parentclass.type) end def test_topnamespace parser = mkparser parser.interp.clear # Make sure we put the top-level code into a class called "" in # the "" namespace assert_nothing_raised do out = parser.parse "" assert_nil(out) assert_nil(parser.interp.findclass("", "")) end # Now try something a touch more complicated parser.interp.clear assert_nothing_raised do out = parser.parse "Exec { path => '/usr/bin:/usr/sbin' }" assert_instance_of(AST::ASTArray, out) assert_equal("", parser.interp.findclass("", "").type) assert_equal("", parser.interp.findclass("", "").namespace) assert_equal(out.object_id, parser.interp.findclass("", "").code.object_id) end end # Make sure virtual and exported resources work appropriately. def test_virtualresources Puppet[:storeconfigs] = true [:virtual, :exported].each do |form| parser = mkparser if form == :virtual at = "@" else at = "@@" end - check = proc do |res| + check = proc do |res, msg| if res.is_a?(Puppet::Parser::Resource) txt = res.ref else txt = res.class end # Real resources get marked virtual when exported if form == :virtual or res.is_a?(Puppet::Parser::Resource) - assert(res.virtual, "Resource #{txt} is not virtual") + assert(res.virtual, "#{msg} #{at}#{txt} is not virtual") end if form == :virtual - assert(! res.exported, "Resource #{txt} is exported") + assert(! res.exported, "#{msg} #{at}#{txt} is exported") else - assert(res.exported, "Resource #{txt} is not exported") + assert(res.exported, "#{msg} #{at}#{txt} is not exported") end end ret = nil assert_nothing_raised do ret = parser.parse("#{at}file { '/tmp/testing': owner => root }") end assert_equal("/tmp/testing", ret[0].title.value) # We always get an astarray back, so... assert_instance_of(AST::ResourceDef, ret[0]) - check.call(ret[0]) + check.call(ret[0], "simple resource") # Now let's try it with multiple resources in the same spec assert_nothing_raised do ret = parser.parse("#{at}file { ['/tmp/1', '/tmp/2']: owner => root }") end assert_instance_of(AST::ASTArray, ret) ret.each do |res| assert_instance_of(AST::ResourceDef, res) - check.call(res) + check.call(res, "multiresource") end # Now evaluate these scope = mkscope klass = scope.interp.newclass "" scope.source = klass assert_nothing_raised do ret.evaluate :scope => scope end # Make sure we can find both of them %w{/tmp/1 /tmp/2}.each do |title| res = scope.findresource("file[#{title}]") assert(res, "Could not find %s" % title) - check.call(res) + check.call(res, "found multiresource") end end end def test_collections Puppet[:storeconfigs] = true [:virtual, :exported].each do |form| parser = mkparser if form == :virtual arrow = "<||>" else arrow = "<<||>>" end check = proc do |coll| assert_instance_of(AST::Collection, coll) assert_equal(form, coll.form) end ret = nil assert_nothing_raised do ret = parser.parse("File #{arrow}") end check.call(ret[0]) end end def test_collectionexpressions %w{== !=}.each do |oper| str = "File <| title #{oper} '/tmp/testing' |>" parser = mkparser res = nil assert_nothing_raised do res = parser.parse(str)[0] end assert_instance_of(AST::Collection, res) query = res.query assert_instance_of(AST::CollExpr, query) assert_equal(:virtual, query.form) assert_equal("title", query.test1.value) assert_equal("/tmp/testing", query.test2.value) assert_equal(oper, query.oper) end end def test_collectionstatements %w{and or}.each do |joiner| str = "File <| title == '/tmp/testing' #{joiner} owner == root |>" parser = mkparser res = nil assert_nothing_raised do res = parser.parse(str)[0] end assert_instance_of(AST::Collection, res) query = res.query assert_instance_of(AST::CollExpr, query) assert_equal(joiner, query.oper) assert_instance_of(AST::CollExpr, query.test1) assert_instance_of(AST::CollExpr, query.test2) end end def test_collectionstatements_with_parens [ "(title == '/tmp/testing' and owner == root) or owner == wheel", "(title == '/tmp/testing')" ].each do |test| str = "File <| #{test} |>" parser = mkparser res = nil assert_nothing_raised("Could not parse '#{test}'") do res = parser.parse(str)[0] end assert_instance_of(AST::Collection, res) query = res.query assert_instance_of(AST::CollExpr, query) #assert_equal(joiner, query.oper) #assert_instance_of(AST::CollExpr, query.test1) #assert_instance_of(AST::CollExpr, query.test2) end end # We've had problems with files other than site.pp importing into main. def test_importing_into_main top = tempfile() other = tempfile() File.open(top, "w") do |f| f.puts "import '#{other}'" end file = tempfile() File.open(other, "w") do |f| f.puts "file { '#{file}': ensure => present }" end interp = mkinterp :Manifest => top, :UseNodes => false code = nil assert_nothing_raised do code = interp.run("hostname.domain.com", {}).flatten end assert(code.length == 1, "Did not get the file") assert_instance_of(Puppet::TransObject, code[0]) end end # $Id$