diff --git a/lib/puppet/dsl.rb b/lib/puppet/dsl.rb index abdb78f67..97a310436 100644 --- a/lib/puppet/dsl.rb +++ b/lib/puppet/dsl.rb @@ -1,11 +1,7 @@ require 'puppet' module Puppet::DSL end require 'puppet/dsl/resource_type_api' require 'puppet/dsl/resource_api' - -class Object - include Puppet::DSL::ResourceTypeAPI -end diff --git a/lib/puppet/dsl/resource_type_api.rb b/lib/puppet/dsl/resource_type_api.rb index 487aab99d..8810d5368 100644 --- a/lib/puppet/dsl/resource_type_api.rb +++ b/lib/puppet/dsl/resource_type_api.rb @@ -1,40 +1,34 @@ require 'puppet/resource/type' -module Puppet::DSL::ResourceTypeAPI - def define(name, *args, &block) - result = mk_resource_type(:definition, name, Hash.new, block) - result.set_arguments(munge_type_arguments(args)) - result - end - - def hostclass(name, options = {}, &block) - mk_resource_type(:hostclass, name, options, block) - end - - def node(name, options = {}, &block) - mk_resource_type(:node, name, options, block) - end - - private - - def mk_resource_type(type, name, options, code) - klass = Puppet::Resource::Type.new(type, name, options) - - klass.ruby_code = code if code - - Puppet::Node::Environment.new.known_resource_types.add klass - - klass +# Type of the objects inside of which pure ruby manifest files are +# executed. Provides methods for creating defines, hostclasses, and +# nodes. +class Puppet::DSL::ResourceTypeAPI + def initialize + @__created_ast_objects__ = [] end - def munge_type_arguments(args) - args.inject([]) do |result, item| + def define(name, *args, &block) + args = args.inject([]) do |result, item| if item.is_a?(Hash) item.each { |p, v| result << [p, v] } else result << item end result end + @__created_ast_objects__.push Puppet::Parser::AST::Definition.new(name, {:arguments => args}, &block) + nil + end + + def hostclass(name, options = {}, &block) + @__created_ast_objects__.push Puppet::Parser::AST::Hostclass.new(name, options, &block) + nil + end + + def node(name, options = {}, &block) + name = [name] unless name.is_a?(Array) + @__created_ast_objects__.push Puppet::Parser::AST::Node.new(name, options, &block) + nil end end diff --git a/lib/puppet/parser/ast/definition.rb b/lib/puppet/parser/ast/definition.rb index 09f52b519..985f8f286 100644 --- a/lib/puppet/parser/ast/definition.rb +++ b/lib/puppet/parser/ast/definition.rb @@ -1,12 +1,15 @@ require 'puppet/parser/ast/top_level_construct' class Puppet::Parser::AST::Definition < Puppet::Parser::AST::TopLevelConstruct - def initialize(name, context = {}) + def initialize(name, context = {}, &ruby_code) @name = name @context = context + @ruby_code = ruby_code end def instantiate(modname) - return [Puppet::Resource::Type.new(:definition, @name, @context.merge(:module_name => modname))] + new_definition = Puppet::Resource::Type.new(:definition, @name, @context.merge(:module_name => modname)) + new_definition.ruby_code = @ruby_code if @ruby_code + [new_definition] end end diff --git a/lib/puppet/parser/ast/hostclass.rb b/lib/puppet/parser/ast/hostclass.rb index d539e4deb..cab5e4a24 100644 --- a/lib/puppet/parser/ast/hostclass.rb +++ b/lib/puppet/parser/ast/hostclass.rb @@ -1,26 +1,29 @@ require 'puppet/parser/ast/top_level_construct' class Puppet::Parser::AST::Hostclass < Puppet::Parser::AST::TopLevelConstruct attr_accessor :name, :context - def initialize(name, context = {}) + def initialize(name, context = {}, &ruby_code) @context = context @name = name + @ruby_code = ruby_code end def instantiate(modname) - all_types = [Puppet::Resource::Type.new(:hostclass, @name, @context.merge(:module_name => modname))] + new_class = Puppet::Resource::Type.new(:hostclass, @name, @context.merge(:module_name => modname)) + new_class.ruby_code = @ruby_code if @ruby_code + all_types = [new_class] if code code.each do |nested_ast_node| if nested_ast_node.respond_to? :instantiate all_types += nested_ast_node.instantiate(modname) end end end return all_types end def code() @context[:code] end end diff --git a/lib/puppet/parser/ast/node.rb b/lib/puppet/parser/ast/node.rb index c19a24ce0..9767399f7 100644 --- a/lib/puppet/parser/ast/node.rb +++ b/lib/puppet/parser/ast/node.rb @@ -1,17 +1,20 @@ require 'puppet/parser/ast/top_level_construct' class Puppet::Parser::AST::Node < Puppet::Parser::AST::TopLevelConstruct attr_accessor :names - def initialize(names, context = {}) + def initialize(names, context = {}, &ruby_code) raise ArgumentError, "names should be an array" unless names.is_a? Array @names = names @context = context + @ruby_code = ruby_code end def instantiate(modname) @names.collect do |name| - Puppet::Resource::Type.new(:node, name, @context.merge(:module_name => modname)) + new_node = Puppet::Resource::Type.new(:node, name, @context.merge(:module_name => modname)) + new_node.ruby_code = @ruby_code if @ruby_code + new_node end end end diff --git a/lib/puppet/parser/parser_support.rb b/lib/puppet/parser/parser_support.rb index 859897a16..a9df33f8b 100644 --- a/lib/puppet/parser/parser_support.rb +++ b/lib/puppet/parser/parser_support.rb @@ -1,216 +1,221 @@ # I pulled this into a separate file, because I got # tired of rebuilding the parser.rb file all the time. class Puppet::Parser::Parser require 'puppet/parser/functions' require 'puppet/parser/files' require 'puppet/resource/type_collection' require 'puppet/resource/type_collection_helper' require 'puppet/resource/type' require 'monitor' AST = Puppet::Parser::AST include Puppet::Resource::TypeCollectionHelper attr_reader :version, :environment attr_accessor :files attr_accessor :lexer # Add context to a message; useful for error messages and such. def addcontext(message, obj = nil) obj ||= @lexer message += " on line #{obj.line}" if file = obj.file message += " in file #{file}" end message end # Create an AST array out of all of the args def aryfy(*args) if args[0].instance_of?(AST::ASTArray) result = args.shift args.each { |arg| result.push arg } else result = ast AST::ASTArray, :children => args end result end # Create an AST object, and automatically add the file and line information if # available. def ast(klass, hash = {}) klass.new ast_context(klass.use_docs).merge(hash) end def ast_context(include_docs = false) result = { :line => lexer.line, :file => lexer.file } result[:doc] = lexer.getcomment(result[:line]) if include_docs result end # The fully qualifed name, with the full namespace. def classname(name) [@lexer.namespace, name].join("::").sub(/^::/, '') end def clear initvars end # Raise a Parse error. def error(message) if brace = @lexer.expected message += "; expected '%s'" end except = Puppet::ParseError.new(message) except.line = @lexer.line except.file = @lexer.file if @lexer.file raise except end def file @lexer.file end def file=(file) unless FileTest.exist?(file) unless file =~ /\.pp$/ file = file + ".pp" end raise Puppet::Error, "Could not find file #{file}" unless FileTest.exist?(file) end raise Puppet::AlreadyImportedError, "Import loop detected" if known_resource_types.watching_file?(file) watch_file(file) @lexer.file = file end [:hostclass, :definition, :node, :nodes?].each do |method| define_method(method) do |*args| known_resource_types.send(method, *args) end end def find_hostclass(namespace, name) known_resource_types.find_hostclass(namespace, name) end def find_definition(namespace, name) known_resource_types.find_definition(namespace, name) end def import(file) known_resource_types.loader.import_if_possible(file, @lexer.file) end def initialize(env) # The environment is needed to know how to find the resource type collection. @environment = env.is_a?(String) ? Puppet::Node::Environment.new(env) : env initvars end # Initialize or reset all of our variables. def initvars @lexer = Puppet::Parser::Lexer.new end # Split an fq name into a namespace and name def namesplit(fullname) ary = fullname.split("::") n = ary.pop || "" ns = ary.join("::") return ns, n end def on_error(token,value,stack) if token == 0 # denotes end of file value = 'end of file' else value = "'#{value[:value]}'" end error = "Syntax error at #{value}" if brace = @lexer.expected error += "; expected '#{brace}'" end except = Puppet::ParseError.new(error) except.line = @lexer.line except.file = @lexer.file if @lexer.file raise except end # how should I do error handling here? def parse(string = nil) if self.file =~ /\.rb$/ - parse_ruby_file - main = nil + main = parse_ruby_file else self.string = string if string begin @yydebug = false main = yyparse(@lexer,:scan) rescue Racc::ParseError => except error = Puppet::ParseError.new(except) error.line = @lexer.line error.file = @lexer.file error.set_backtrace except.backtrace raise error rescue Puppet::ParseError => except except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue Puppet::Error => except # and this is a framework error except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue Puppet::DevError => except except.line ||= @lexer.line except.file ||= @lexer.file raise except rescue => except error = Puppet::DevError.new(except.message) error.line = @lexer.line error.file = @lexer.file error.set_backtrace except.backtrace raise error end end # Store the results as the top-level class. return Puppet::Parser::AST::Hostclass.new('', :code => main) ensure @lexer.clear end def parse_ruby_file - require self.file + # Execute the contents of the file inside its own "main" object so + # that it can call methods in the resource type API. + main_object = Puppet::DSL::ResourceTypeAPI.new + main_object.instance_eval(File.read(self.file)) + + # Then extract any types that were created. + Puppet::Parser::AST::ASTArray.new :children => main_object.instance_eval { @__created_ast_objects__ } end def string=(string) @lexer.string = string end def version known_resource_types.version end # Add a new file to be checked when we're checking to see if we should be # reparsed. This is basically only used by the TemplateWrapper to let the # parser know about templates that should be parsed. def watch_file(filename) known_resource_types.watch_file(filename) end end diff --git a/spec/integration/parser/ruby_manifest_spec.rb b/spec/integration/parser/ruby_manifest_spec.rb new file mode 100644 index 000000000..6627a2456 --- /dev/null +++ b/spec/integration/parser/ruby_manifest_spec.rb @@ -0,0 +1,126 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +require 'puppet_spec/files' + +describe "Pure ruby manifests" do + include PuppetSpec::Files + + before do + @node = Puppet::Node.new "testnode" + + @scope_resource = stub 'scope_resource', :builtin? => true, :finish => nil, :ref => 'Class[main]' + @scope = stub 'scope', :resource => @scope_resource, :source => mock("source") + @test_dir = tmpdir('ruby_manifest_test') + end + + after do + Puppet.settings.clear + end + + def write_file(name, contents) + path = File.join(@test_dir, name) + File.open(path, "w") { |f| f.write(contents) } + path + end + + def compile(contents) + Puppet[:code] = contents + Dir.chdir(@test_dir) do + Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) + end + end + + it "should allow classes" do + write_file('foo.rb', ["hostclass 'one' do notify('one_notify') end", + "hostclass 'two' do notify('two_notify') end"].join("\n")) + catalog = compile("import 'foo'\ninclude one") + catalog.resource("Notify[one_notify]").should_not be_nil + catalog.resource("Notify[two_notify]").should be_nil + end + + it "should allow defines" do + write_file('foo.rb', 'define "foo", :arg do notify("foo_#{@name}_#{@arg}") end') + catalog = compile("import 'foo'\nfoo { instance: arg => 'xyz' }") + catalog.resource("Notify[foo_instance_xyz]").should_not be_nil + catalog.resource("Foo[instance]").should_not be_nil + end + + it "should allow node declarations" do + write_file('foo.rb', ["node 'mynode' do notify('mynode') end", + "node 'theirnode' do notify('theirnode') end"].join("\n")) + catalog = compile("import 'foo'") + catalog.resource("Notify[mynode]").should_not be_nil + catalog.resource("Notify[theirnode]").should be_nil + end + + it "should allow access to the environment" do + write_file('foo.rb', ["hostclass 'foo' do", + " if environment.is_a? Puppet::Node::Environment", + " notify('success')", + " end", + "end"].join("\n")) + compile("import 'foo'\ninclude foo").resource("Notify[success]").should_not be_nil + end + + it "should allow creation of built-in types" do + write_file('foo.rb', "hostclass 'foo' do file 'test_file', :owner => 'root', :mode => '644' end") + catalog = compile("import 'foo'\ninclude foo") + file = catalog.resource("File[test_file]") + file.should be_a Puppet::Resource + file.type.should == 'File' + file.title.should == 'test_file' + file.exported.should_not be + file.virtual.should_not be + file[:owner].should == 'root' + file[:mode].should == '644' + file[:stage].should be_nil # TODO: is this correct behavior? + end + + it "should allow calling user-defined functions" do + write_file('foo.rb', "hostclass 'foo' do user_func :arg => 'xyz' end") + catalog = compile(['define user_func($arg) { notify {"n_$arg": } }', + 'user_func { "one": arg => "two" }'].join("\n")) + catalog.resource("Notify[n_two]").should_not be_nil + catalog.resource("User_func[one]").should_not be_nil + end + + it "should be properly cached for multiple compiles" do + # Note: we can't test this by calling compile() twice, because + # that sets Puppet[:code], which clears out all cached + # environments. + Puppet[:filetimeout] = 1000 + write_file('foo.rb', "hostclass 'foo' do notify('success') end") + Puppet[:code] = "import 'foo'\ninclude foo" + + # Compile the catalog and check it + catalog = Dir.chdir(@test_dir) do + Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) + end + catalog.resource("Notify[success]").should_not be_nil + + # Secretly change the file to make it invalid. This change + # shouldn't be noticed because the we've set a high + # Puppet[:filetimeout]. + write_file('foo.rb', "raise 'should not be executed'") + + # Compile the catalog a second time and make sure it's still ok. + catalog = Dir.chdir(@test_dir) do + Puppet::Parser::Compiler.compile(Puppet::Node.new("mynode")) + end + catalog.resource("Notify[success]").should_not be_nil + end + + it "should be properly reloaded when stale" do + Puppet[:filetimeout] = -1 # force stale check to happen all the time + write_file('foo.rb', "hostclass 'foo' do notify('version1') end") + catalog = compile("import 'foo'\ninclude foo") + catalog.resource("Notify[version1]").should_not be_nil + sleep 1 # so that timestamp will change forcing file reload + write_file('foo.rb', "hostclass 'foo' do notify('version2') end") + catalog = compile("import 'foo'\ninclude foo") + catalog.resource("Notify[version1]").should be_nil + catalog.resource("Notify[version2]").should_not be_nil + end +end diff --git a/spec/unit/dsl/resource_type_api_spec.rb b/spec/unit/dsl/resource_type_api_spec.rb index 4b8ccf5a7..c9a5d272f 100755 --- a/spec/unit/dsl/resource_type_api_spec.rb +++ b/spec/unit/dsl/resource_type_api_spec.rb @@ -1,44 +1,54 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/dsl/resource_type_api' -class DSLAPITester - include Puppet::DSL::ResourceTypeAPI -end - describe Puppet::DSL::ResourceTypeAPI do - before do - @api = DSLAPITester.new + # Verify that the block creates a single AST node through the API, + # instantiate that AST node into a types, and return that type. + def test_api_call(&block) + main_object = Puppet::DSL::ResourceTypeAPI.new + main_object.instance_eval(&block) + created_ast_objects = main_object.instance_eval { @__created_ast_objects__ } + created_ast_objects.length.should == 1 + new_types = created_ast_objects[0].instantiate('') + new_types.length.should == 1 + new_types[0] + ensure + Thread.current[:ruby_file_parse_result] = nil end [:definition, :node, :hostclass].each do |type| method = type == :definition ? "define" : type it "should be able to create a #{type}" do - newtype = @api.send(method, "myname") + newtype = test_api_call { send(method, "myname").should == nil } newtype.should be_a(Puppet::Resource::Type) newtype.type.should == type end it "should use the provided name when creating a #{type}" do - newtype = @api.send(method, "myname") + newtype = test_api_call { send(method, "myname") } newtype.name.should == "myname" end unless type == :definition it "should pass in any provided options when creating a #{type}" do - newtype = @api.send(method, "myname", :line => 200) + newtype = test_api_call { send(method, "myname", :line => 200) } newtype.line.should == 200 end end - it "should set any provided block as the type's ruby code" - - it "should add the type to the current environment's known resource types" + it "should set any provided block as the type's ruby code" do + newtype = test_api_call { send(method, "myname") { 'method_result' } } + newtype.ruby_code.call.should == 'method_result' + end end describe "when creating a definition" do - it "should use the provided options to define valid arguments for the resource type" + it "should use the provided options to define valid arguments for the resource type" do + newtype = test_api_call { define("myname", :arg1, :arg2) } + newtype.arguments.should == { 'arg1' => nil, 'arg2' => nil } + end end end diff --git a/spec/unit/parser/parser_spec.rb b/spec/unit/parser/parser_spec.rb index 0a61e73de..a964315ed 100755 --- a/spec/unit/parser/parser_spec.rb +++ b/spec/unit/parser/parser_spec.rb @@ -1,422 +1,413 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Parser do ast = Puppet::Parser::AST before :each do @known_resource_types = Puppet::Resource::TypeCollection.new("development") @parser = Puppet::Parser::Parser.new "development" @parser.stubs(:known_resource_types).returns @known_resource_types @true_ast = Puppet::Parser::AST::Boolean.new :value => true end it "should require an environment at initialization" do lambda { Puppet::Parser::Parser.new }.should raise_error(ArgumentError) end it "should set the environment" do env = Puppet::Node::Environment.new Puppet::Parser::Parser.new(env).environment.should == env end it "should convert the environment into an environment instance if a string is provided" do env = Puppet::Node::Environment.new("testing") Puppet::Parser::Parser.new("testing").environment.should == env end it "should be able to look up the environment-specific resource type collection" do rtc = Puppet::Node::Environment.new("development").known_resource_types parser = Puppet::Parser::Parser.new "development" parser.known_resource_types.should equal(rtc) end it "should delegate importing to the known resource type loader" do parser = Puppet::Parser::Parser.new "development" parser.known_resource_types.loader.expects(:import).with("newfile", "current_file") parser.lexer.expects(:file).returns "current_file" parser.import("newfile") end describe "when parsing files" do before do FileTest.stubs(:exist?).returns true File.stubs(:read).returns "" @parser.stubs(:watch_file) end it "should treat files ending in 'rb' as ruby files" do @parser.expects(:parse_ruby_file) @parser.file = "/my/file.rb" @parser.parse end - - describe "in ruby" do - it "should use the ruby interpreter to load the file" do - @parser.file = "/my/file.rb" - @parser.expects(:require).with "/my/file.rb" - - @parser.parse_ruby_file - end - end end describe "when parsing append operator" do it "should not raise syntax errors" do lambda { @parser.parse("$var += something") }.should_not raise_error end it "shouldraise syntax error on incomplete syntax " do lambda { @parser.parse("$var += ") }.should raise_error end it "should create ast::VarDef with append=true" do vardef = @parser.parse("$var += 2").code[0] vardef.should be_a(Puppet::Parser::AST::VarDef) vardef.append.should == true end it "should work with arrays too" do vardef = @parser.parse("$var += ['test']").code[0] vardef.should be_a(Puppet::Parser::AST::VarDef) vardef.append.should == true end end describe "when parsing 'if'" do it "not, it should create the correct ast objects" do ast::Not.expects(:new).with { |h| h[:value].is_a?(ast::Boolean) } @parser.parse("if ! true { $var = 1 }") end it "boolean operation, it should create the correct ast objects" do ast::BooleanOperator.expects(:new).with { |h| h[:rval].is_a?(ast::Boolean) and h[:lval].is_a?(ast::Boolean) and h[:operator]=="or" } @parser.parse("if true or true { $var = 1 }") end it "comparison operation, it should create the correct ast objects" do ast::ComparisonOperator.expects(:new).with { |h| h[:lval].is_a?(ast::Name) and h[:rval].is_a?(ast::Name) and h[:operator]=="<" } @parser.parse("if 1 < 2 { $var = 1 }") end end describe "when parsing if complex expressions" do it "should create a correct ast tree" do aststub = stub_everything 'ast' ast::ComparisonOperator.expects(:new).with { |h| h[:rval].is_a?(ast::Name) and h[:lval].is_a?(ast::Name) and h[:operator]==">" }.returns(aststub) ast::ComparisonOperator.expects(:new).with { |h| h[:rval].is_a?(ast::Name) and h[:lval].is_a?(ast::Name) and h[:operator]=="==" }.returns(aststub) ast::BooleanOperator.expects(:new).with { |h| h[:rval]==aststub and h[:lval]==aststub and h[:operator]=="and" } @parser.parse("if (1 > 2) and (1 == 2) { $var = 1 }") end it "should raise an error on incorrect expression" do lambda { @parser.parse("if (1 > 2 > ) or (1 == 2) { $var = 1 }") }.should raise_error end end describe "when parsing resource references" do it "should not raise syntax errors" do lambda { @parser.parse('exec { test: param => File["a"] }') }.should_not raise_error end it "should not raise syntax errors with multiple references" do lambda { @parser.parse('exec { test: param => File["a","b"] }') }.should_not raise_error end it "should create an ast::ResourceReference" do ast::Resource.stubs(:new) ast::ResourceReference.expects(:new).with { |arg| arg[:line]==1 and arg[:type]=="File" and arg[:title].is_a?(ast::ASTArray) } @parser.parse('exec { test: command => File["a","b"] }') end end describe "when parsing resource overrides" do it "should not raise syntax errors" do lambda { @parser.parse('Resource["title"] { param => value }') }.should_not raise_error end it "should not raise syntax errors with multiple overrides" do lambda { @parser.parse('Resource["title1","title2"] { param => value }') }.should_not raise_error end it "should create an ast::ResourceOverride" do #ast::ResourceOverride.expects(:new).with { |arg| # arg[:line]==1 and arg[:object].is_a?(ast::ResourceReference) and arg[:parameters].is_a?(ast::ResourceParam) #} ro = @parser.parse('Resource["title1","title2"] { param => value }').code[0] ro.should be_a(ast::ResourceOverride) ro.line.should == 1 ro.object.should be_a(ast::ResourceReference) ro.parameters[0].should be_a(ast::ResourceParam) end end describe "when parsing if statements" do it "should not raise errors with empty if" do lambda { @parser.parse("if true { }") }.should_not raise_error end it "should not raise errors with empty else" do lambda { @parser.parse("if false { notice('if') } else { }") }.should_not raise_error end it "should not raise errors with empty if and else" do lambda { @parser.parse("if false { } else { }") }.should_not raise_error end it "should create a nop node for empty branch" do ast::Nop.expects(:new) @parser.parse("if true { }") end it "should create a nop node for empty else branch" do ast::Nop.expects(:new) @parser.parse("if true { notice('test') } else { }") end it "should build a chain of 'ifs' if there's an 'elsif'" do ast = @parser.parse(<<-PP) if true { notice('test') } elsif true {} else { } PP end end describe "when parsing function calls" do it "should not raise errors with no arguments" do lambda { @parser.parse("tag()") }.should_not raise_error end it "should not raise errors with rvalue function with no args" do lambda { @parser.parse("$a = template()") }.should_not raise_error end it "should not raise errors with arguments" do lambda { @parser.parse("notice(1)") }.should_not raise_error end it "should not raise errors with multiple arguments" do lambda { @parser.parse("notice(1,2)") }.should_not raise_error end it "should not raise errors with multiple arguments and a trailing comma" do lambda { @parser.parse("notice(1,2,)") }.should_not raise_error end end describe "when parsing arrays with trailing comma" do it "should not raise errors with a trailing comma" do lambda { @parser.parse("$a = [1,2,]") }.should_not raise_error end end describe "when providing AST context" do before do @lexer = stub 'lexer', :line => 50, :file => "/foo/bar", :getcomment => "whev" @parser.stubs(:lexer).returns @lexer end it "should include the lexer's line" do @parser.ast_context[:line].should == 50 end it "should include the lexer's file" do @parser.ast_context[:file].should == "/foo/bar" end it "should include the docs if directed to do so" do @parser.ast_context(true)[:doc].should == "whev" end it "should not include the docs when told not to" do @parser.ast_context(false)[:doc].should be_nil end it "should not include the docs by default" do @parser.ast_context[:doc].should be_nil end end describe "when building ast nodes" do before do @lexer = stub 'lexer', :line => 50, :file => "/foo/bar", :getcomment => "whev" @parser.stubs(:lexer).returns @lexer @class = stub 'class', :use_docs => false end it "should return a new instance of the provided class created with the provided options" do @class.expects(:new).with { |opts| opts[:foo] == "bar" } @parser.ast(@class, :foo => "bar") end it "should merge the ast context into the provided options" do @class.expects(:new).with { |opts| opts[:file] == "/foo" } @parser.expects(:ast_context).returns :file => "/foo" @parser.ast(@class, :foo => "bar") end it "should prefer provided options over AST context" do @class.expects(:new).with { |opts| opts[:file] == "/bar" } @parser.expects(:ast_context).returns :file => "/foo" @parser.ast(@class, :file => "/bar") end it "should include docs when the AST class uses them" do @class.expects(:use_docs).returns true @class.stubs(:new) @parser.expects(:ast_context).with(true).returns({}) @parser.ast(@class, :file => "/bar") end end describe "when retrieving a specific node" do it "should delegate to the known_resource_types node" do @known_resource_types.expects(:node).with("node") @parser.node("node") end end describe "when retrieving a specific class" do it "should delegate to the loaded code" do @known_resource_types.expects(:hostclass).with("class") @parser.hostclass("class") end end describe "when retrieving a specific definitions" do it "should delegate to the loaded code" do @known_resource_types.expects(:definition).with("define") @parser.definition("define") end end describe "when determining the configuration version" do it "should determine it from the resource type collection" do @parser.known_resource_types.expects(:version).returns "foo" @parser.version.should == "foo" end end describe "when looking up definitions" do it "should use the known resource types to check for them by name" do @parser.known_resource_types.stubs(:find_or_load).with("namespace","name",:definition).returns(:this_value) @parser.find_definition("namespace","name").should == :this_value end end describe "when looking up hostclasses" do it "should use the known resource types to check for them by name" do @parser.known_resource_types.stubs(:find_or_load).with("namespace","name",:hostclass).returns(:this_value) @parser.find_hostclass("namespace","name").should == :this_value end end describe "when parsing classes" do before :each do @krt = Puppet::Resource::TypeCollection.new("development") @parser = Puppet::Parser::Parser.new "development" @parser.stubs(:known_resource_types).returns @krt end it "should not create new classes" do @parser.parse("class foobar {}").code[0].should be_a(Puppet::Parser::AST::Hostclass) @krt.hostclass("foobar").should be_nil end it "should correctly set the parent class when one is provided" do @parser.parse("class foobar inherits yayness {}").code[0].instantiate('')[0].parent.should == "yayness" end it "should correctly set the parent class for multiple classes at a time" do statements = @parser.parse("class foobar inherits yayness {}\nclass boo inherits bar {}").code statements[0].instantiate('')[0].parent.should == "yayness" statements[1].instantiate('')[0].parent.should == "bar" end it "should define the code when some is provided" do @parser.parse("class foobar { $var = val }").code[0].code.should_not be_nil end it "should define parameters when provided" do foobar = @parser.parse("class foobar($biz,$baz) {}").code[0].instantiate('')[0] foobar.arguments.should == {"biz" => nil, "baz" => nil} end end describe "when parsing resources" do before :each do @krt = Puppet::Resource::TypeCollection.new("development") @parser = Puppet::Parser::Parser.new "development" @parser.stubs(:known_resource_types).returns @krt end it "should be able to parse class resources" do @krt.add(Puppet::Resource::Type.new(:hostclass, "foobar", :arguments => {"biz" => nil})) lambda { @parser.parse("class { foobar: biz => stuff }") }.should_not raise_error end it "should correctly mark exported resources as exported" do @parser.parse("@@file { '/file': }").code[0][0].exported.should be_true end it "should correctly mark virtual resources as virtual" do @parser.parse("@file { '/file': }").code[0][0].virtual.should be_true end end describe "when parsing nodes" do it "should be able to parse a node with a single name" do node = @parser.parse("node foo { }").code[0] node.should be_a Puppet::Parser::AST::Node node.names.length.should == 1 node.names[0].value.should == "foo" end it "should be able to parse a node with two names" do node = @parser.parse("node foo, bar { }").code[0] node.should be_a Puppet::Parser::AST::Node node.names.length.should == 2 node.names[0].value.should == "foo" node.names[1].value.should == "bar" end it "should be able to parse a node with three names" do node = @parser.parse("node foo, bar, baz { }").code[0] node.should be_a Puppet::Parser::AST::Node node.names.length.should == 3 node.names[0].value.should == "foo" node.names[1].value.should == "bar" node.names[2].value.should == "baz" end end end