From 6b1dd81799a44288287d9ab0cdf46afa3aaf090a Mon Sep 17 00:00:00 2001 From: Paul Berry Date: Thu, 5 Aug 2010 10:34:35 -0700 Subject: [#4472]+[#4483] Moved type-name resolution out of Puppet::Parser::TypeLoader. Moved type-name resolution out of Puppet::Parser::TypeLoader, and into its primary client, Puppet::Resource::TypeCollection. TypeCollection now always passes fully qualified type names to TypeLoader. This avoids duplicate type-name resolution logic between TypeLoader and TypeCollection. That in turn fixes bug 4472, which resulted from flaws in the type-name resolution logic in TypeLoader. In addition, it fixes bug 4483, which resulted from improper interleaving between looking up names using the TypeCollection and the TypeLoader. --- spec/unit/parser/type_loader_spec.rb | 83 ++++++++---------------------------- 1 file changed, 18 insertions(+), 65 deletions(-) (limited to 'spec/unit/parser') diff --git a/spec/unit/parser/type_loader_spec.rb b/spec/unit/parser/type_loader_spec.rb index 8f005d551..b7e174753 100644 --- a/spec/unit/parser/type_loader_spec.rb +++ b/spec/unit/parser/type_loader_spec.rb @@ -28,86 +28,28 @@ describe Puppet::Parser::TypeLoader do describe "when loading names from namespaces" do it "should do nothing if the name to import is an empty string" do @loader.expects(:name2files).never - @loader.load_until(["foo"], "") { |f| false }.should be_nil - end - - it "should turn the provided namespaces and name into a list of files" do - @loader.expects(:name2files).with(["foo"], "bar").returns [] - @loader.load_until(["foo"], "bar") { |f| false } + @loader.try_load_fqname("") { |filename, modname| raise :should_not_occur }.should be_nil end it "should attempt to import each generated name" do - @loader.expects(:name2files).returns %w{foo bar} + @loader.expects(:import).with("foo/bar",nil) @loader.expects(:import).with("foo",nil) - @loader.expects(:import).with("bar",nil) - @loader.load_until(["foo"], "bar") { |f| false } + @loader.try_load_fqname("foo::bar") { |f| false } end it "should yield after each import" do yielded = [] - @loader.expects(:name2files).returns %w{foo bar} - @loader.expects(:import).with("foo",nil) - @loader.expects(:import).with("bar",nil) - @loader.load_until(["foo"], "bar") { |f| yielded << f; false } - yielded.should == %w{foo bar} - end - - it "should stop importing when the yielded block returns true" do - yielded = [] - @loader.expects(:name2files).returns %w{foo bar baz} - @loader.expects(:import).with("foo",nil) - @loader.expects(:import).with("bar",nil) - @loader.expects(:import).with("baz",nil).never - @loader.load_until(["foo"], "bar") { |f| true if f == "bar" } - end - - it "should return the result of the block" do - yielded = [] - @loader.expects(:name2files).returns %w{foo bar baz} + @loader.expects(:import).with("foo/bar",nil) @loader.expects(:import).with("foo",nil) - @loader.expects(:import).with("bar",nil) - @loader.expects(:import).with("baz",nil).never - @loader.load_until(["foo"], "bar") { |f| 10 if f == "bar" }.should == 10 - end - - it "should return nil if the block never returns true" do - @loader.expects(:name2files).returns %w{foo bar} - @loader.expects(:import).with("foo",nil) - @loader.expects(:import).with("bar",nil) - @loader.load_until(["foo"], "bar") { |f| false }.should be_nil + @loader.try_load_fqname("foo::bar") { |filename, modname| yielded << [filename, modname]; false } + yielded.should == [["foo/bar", nil], ["foo", nil]] end it "should know when a given name has been loaded" do - @loader.expects(:name2files).returns %w{file} @loader.expects(:import).with("file",nil) - @loader.load_until(["foo"], "bar") { |f| true } + @loader.try_load_fqname("file") { |f| true } @loader.should be_loaded("file") end - - it "should set the module name on any created resource types" do - type = Puppet::Resource::Type.new(:hostclass, "mytype") - - Puppet::Parser::Files.expects(:find_manifests).returns ["modname", %w{one}] - @loader.stubs(:parse_file) - @loader.load_until(["foo"], "one") { |f| type } - - type.module_name.should == "modname" - end - end - - describe "when mapping names to files" do - { - [["foo"], "::bar::baz"] => %w{bar/baz}, - [[""], "foo::bar"] => %w{foo foo/bar}, - [%w{foo}, "bar"] => %w{foo foo/bar bar}, - [%w{a b}, "bar"] => %w{a a/bar b b/bar bar}, - [%w{a::b::c}, "bar"] => %w{a a/b/c/bar bar}, - [%w{a::b}, "foo::bar"] => %w{a a/b/foo/bar foo/bar} - }.each do |inputs, outputs| - it "should produce #{outputs.inspect} from the #{inputs[0].inspect} namespace and #{inputs[1]} name" do - @loader.name2files(*inputs).should == outputs - end - end end describe "when importing" do @@ -198,4 +140,15 @@ describe Puppet::Parser::TypeLoader do @loader.known_resource_types.hostclass("foo").should be_instance_of(Puppet::Resource::Type) end + + describe "when deciding where to look for files" do + { 'foo' => ['foo'], + 'foo::bar' => ['foo/bar', 'foo'], + 'foo::bar::baz' => ['foo/bar/baz', 'foo/bar', 'foo'] + }.each do |fqname, expected_paths| + it "should look for #{fqname.inspect} in #{expected_paths.inspect}" do + @loader.instance_eval { name2files(fqname) }.should == expected_paths + end + end + end end -- cgit From 4da88fb4cd57871f16649d50572240ac3f7420f0 Mon Sep 17 00:00:00 2001 From: Paul Berry Date: Fri, 13 Aug 2010 15:43:34 -0700 Subject: [#4496]+[#4521]+[#4522] Add structures to the AST to represent type definitions (classes, definitions, and nodes). Previously, type definitions were not represented directly in the AST. Instead, the parser would instantiate types and insert them into known_resource_types as soon as they were parsed. This made it difficult to distinguish which types had come from the file that was just parsed and which types had been loaded previously, which led to bug 4496. A side-effect of this change is that the user is no longer allowed to define types inside of conditional constructs (such as if/else). This was allowed before but had unexpected semantics (bugs 4521 and 4522). It is still possible, however, to place an "include" statement inside a conditional construct, and have that "include" statement trigger the autoloading of a file that instantiates types. --- spec/unit/parser/ast/definition_spec.rb | 17 ++++++ spec/unit/parser/ast/hostclass_spec.rb | 73 +++++++++++++++++++++++++ spec/unit/parser/ast/node_spec.rb | 30 +++++++++++ spec/unit/parser/ast/resource_spec.rb | 6 +-- spec/unit/parser/parser_spec.rb | 96 ++++++++++++++++++--------------- spec/unit/parser/scope_spec.rb | 3 +- spec/unit/parser/type_loader_spec.rb | 32 +++++------ 7 files changed, 189 insertions(+), 68 deletions(-) create mode 100644 spec/unit/parser/ast/definition_spec.rb create mode 100644 spec/unit/parser/ast/hostclass_spec.rb create mode 100644 spec/unit/parser/ast/node_spec.rb (limited to 'spec/unit/parser') diff --git a/spec/unit/parser/ast/definition_spec.rb b/spec/unit/parser/ast/definition_spec.rb new file mode 100644 index 000000000..01ca068ad --- /dev/null +++ b/spec/unit/parser/ast/definition_spec.rb @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +describe Puppet::Parser::AST::Definition do + describe "when instantiated" do + it "should create a definition with the proper type, name, context, and module name" do + definition = Puppet::Parser::AST::Definition.new('foo', :line => 5) + instantiated_definitions = definition.instantiate('modname') + instantiated_definitions.length.should == 1 + instantiated_definitions[0].type.should == :definition + instantiated_definitions[0].name.should == 'foo' + instantiated_definitions[0].line.should == 5 + instantiated_definitions[0].module_name.should == 'modname' + end + end +end diff --git a/spec/unit/parser/ast/hostclass_spec.rb b/spec/unit/parser/ast/hostclass_spec.rb new file mode 100644 index 000000000..b22eba98b --- /dev/null +++ b/spec/unit/parser/ast/hostclass_spec.rb @@ -0,0 +1,73 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +describe Puppet::Parser::AST::Hostclass do + def ast + Puppet::Parser::AST + end + + def newarray(*elems) + ast::ASTArray.new({}).push(*elems) + end + + it "should make its name and context available through accessors" do + hostclass = ast::Hostclass.new('foo', :line => 5) + hostclass.name.should == 'foo' + hostclass.context.should == {:line => 5} + end + + it "should make its code available through an accessor" do + code = newarray + hostclass = ast::Hostclass.new('foo', :code => code) + hostclass.code.should be_equal(code) + end + + describe "when instantiated" do + it "should create a class with the proper type, code, name, context, and module name" do + code = newarray + hostclass = ast::Hostclass.new('foo', :code => code, :line => 5) + instantiated_class = hostclass.instantiate('modname')[0] + instantiated_class.type.should == :hostclass + instantiated_class.name.should == 'foo' + instantiated_class.code.should be_equal(code) + instantiated_class.line.should == 5 + instantiated_class.module_name.should == 'modname' + end + + it "should instantiate all nested classes, defines, and nodes with the same module name." do + nested_objects = newarray(ast::Hostclass.new('foo::child1'), + ast::Definition.new('foo::child2'), + ast::Definition.new('child3')) + hostclass = ast::Hostclass.new('foo', :code => nested_objects) + instantiated_classes = hostclass.instantiate('modname') + instantiated_classes.length.should == 4 + instantiated_classes[0].name.should == 'foo' + instantiated_classes[1].name.should == 'foo::child1' + instantiated_classes[2].name.should == 'foo::child2' + instantiated_classes[3].name.should == 'child3' + instantiated_classes.each { |cls| cls.module_name.should == 'modname' } + end + + it "should handle a nested class that contains its own nested classes." do + foo_bar_baz = ast::Hostclass.new('foo::bar::baz') + foo_bar = ast::Hostclass.new('foo::bar', :code => newarray(foo_bar_baz)) + foo = ast::Hostclass.new('foo', :code => newarray(foo_bar)) + instantiated_classes = foo.instantiate('') + instantiated_classes.length.should == 3 + instantiated_classes[0].name.should == 'foo' + instantiated_classes[1].name.should == 'foo::bar' + instantiated_classes[2].name.should == 'foo::bar::baz' + end + + it "should skip nested elements that are not classes, definitions, or nodes." do + func = ast::Function.new(:name => 'biz', :arguments => newarray(ast::Name.new(:value => 'baz'))) + foo = ast::Hostclass.new('foo', :code => newarray(func)) + instantiated_classes = foo.instantiate('') + instantiated_classes.length.should == 1 + instantiated_classes[0].should be_a(Puppet::Resource::Type) + instantiated_classes[0].name.should == 'foo' + end + end +end + diff --git a/spec/unit/parser/ast/node_spec.rb b/spec/unit/parser/ast/node_spec.rb new file mode 100644 index 000000000..409e877f9 --- /dev/null +++ b/spec/unit/parser/ast/node_spec.rb @@ -0,0 +1,30 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +describe Puppet::Parser::AST::Node do + describe "when instantiated" do + it "should make its names available through an accessor" do + node = Puppet::Parser::AST::Node.new(['foo', 'bar']) + node.names.should == ['foo', 'bar'] + end + + it "should create a node with the proper type, name, context, and module name" do + node = Puppet::Parser::AST::Node.new(['foo'], :line => 5) + instantiated_nodes = node.instantiate('modname') + instantiated_nodes.length.should == 1 + instantiated_nodes[0].type.should == :node + instantiated_nodes[0].name.should == 'foo' + instantiated_nodes[0].line.should == 5 + instantiated_nodes[0].module_name.should == 'modname' + end + + it "should handle multiple names" do + node = Puppet::Parser::AST::Node.new(['foo', 'bar']) + instantiated_nodes = node.instantiate('modname') + instantiated_nodes.length.should == 2 + instantiated_nodes[0].name.should == 'foo' + instantiated_nodes[1].name.should == 'bar' + end + end +end diff --git a/spec/unit/parser/ast/resource_spec.rb b/spec/unit/parser/ast/resource_spec.rb index 58ffae925..4e5549b96 100755 --- a/spec/unit/parser/ast/resource_spec.rb +++ b/spec/unit/parser/ast/resource_spec.rb @@ -89,9 +89,9 @@ describe Puppet::Parser::AST::Resource do before do @scope = Puppet::Parser::Scope.new :compiler => Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) @parser = Puppet::Parser::Parser.new(Puppet::Node::Environment.new) - @parser.newdefine "one" - @parser.newdefine "one::two" - @parser.newdefine "three" + ["one", "one::two", "three"].each do |name| + @parser.environment.known_resource_types.add(Puppet::Resource::Type.new(:definition, name, {})) + end @twoscope = @scope.newscope(:namespace => "one") @twoscope.resource = @scope.resource end diff --git a/spec/unit/parser/parser_spec.rb b/spec/unit/parser/parser_spec.rb index 0657ab37a..0a61e73de 100755 --- a/spec/unit/parser/parser_spec.rb +++ b/spec/unit/parser/parser_spec.rb @@ -73,14 +73,16 @@ describe Puppet::Parser do lambda { @parser.parse("$var += ") }.should raise_error end - it "should call ast::VarDef with append=true" do - ast::VarDef.expects(:new).with { |h| h[:append] == true } - @parser.parse("$var += 2") + 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 - ast::VarDef.expects(:new).with { |h| h[:append] == true } - @parser.parse("$var += ['test']") + vardef = @parser.parse("$var += ['test']").code[0] + vardef.should be_a(Puppet::Parser::AST::VarDef) + vardef.append.should == true end end @@ -160,10 +162,14 @@ describe Puppet::Parser do 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) - } - @parser.parse('Resource["title1","title2"] { param => value }') + #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 @@ -290,24 +296,6 @@ describe Puppet::Parser do end end - describe "when creating a node" do - before :each do - @lexer = stub 'lexer' - @lexer.stubs(:getcomment) - @parser.stubs(:lexer).returns(@lexer) - @node = stub_everything 'node' - @parser.stubs(:ast_context).returns({}) - @parser.stubs(:node).returns(nil) - - @nodename = stub 'nodename', :is_a? => false, :value => "foo" - @nodename.stubs(:is_a?).with(Puppet::Parser::AST::HostName).returns(true) - end - - it "should return an array of nodes" do - @parser.newnode(@nodename).should be_instance_of(Array) - 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") @@ -360,30 +348,28 @@ describe Puppet::Parser do @parser.stubs(:known_resource_types).returns @krt end - it "should create new classes" do - @parser.parse("class foobar {}") - @krt.hostclass("foobar").should be_instance_of(Puppet::Resource::Type) + 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 {}") - @krt.hostclass("foobar").parent.should == "yayness" + @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 - @parser.parse("class foobar inherits yayness {}\nclass boo inherits bar {}") - @krt.hostclass("foobar").parent.should == "yayness" - @krt.hostclass("boo").parent.should == "bar" + 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 }") - @krt.hostclass("foobar").code.should_not be_nil + @parser.parse("class foobar { $var = val }").code[0].code.should_not be_nil end it "should define parameters when provided" do - @parser.parse("class foobar($biz,$baz) {}") - @krt.hostclass("foobar").arguments.should == {"biz" => nil, "baz" => nil} + foobar = @parser.parse("class foobar($biz,$baz) {}").code[0].instantiate('')[0] + foobar.arguments.should == {"biz" => nil, "baz" => nil} end end @@ -400,13 +386,37 @@ describe Puppet::Parser do end it "should correctly mark exported resources as exported" do - @parser.parse("@@file { '/file': }") - @krt.hostclass("").code[0].exported.should be_true + @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': }") - @krt.hostclass("").code[0].virtual.should be_true + @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 diff --git a/spec/unit/parser/scope_spec.rb b/spec/unit/parser/scope_spec.rb index 9895f446b..2e390a53b 100755 --- a/spec/unit/parser/scope_spec.rb +++ b/spec/unit/parser/scope_spec.rb @@ -29,8 +29,7 @@ describe Puppet::Parser::Scope do end it "should be able to retrieve its parent module name from the source of its parent type" do - @topscope.source = Puppet::Resource::Type.new(:hostclass, :foo) - @topscope.source.module_name = "foo" + @topscope.source = Puppet::Resource::Type.new(:hostclass, :foo, :module_name => "foo") @scope.parent_module_name.should == "foo" end diff --git a/spec/unit/parser/type_loader_spec.rb b/spec/unit/parser/type_loader_spec.rb index b7e174753..b06251681 100644 --- a/spec/unit/parser/type_loader_spec.rb +++ b/spec/unit/parser/type_loader_spec.rb @@ -28,26 +28,18 @@ describe Puppet::Parser::TypeLoader do describe "when loading names from namespaces" do it "should do nothing if the name to import is an empty string" do @loader.expects(:name2files).never - @loader.try_load_fqname("") { |filename, modname| raise :should_not_occur }.should be_nil + @loader.try_load_fqname(:hostclass, "") { |filename, modname| raise :should_not_occur }.should be_nil end it "should attempt to import each generated name" do - @loader.expects(:import).with("foo/bar",nil) - @loader.expects(:import).with("foo",nil) - @loader.try_load_fqname("foo::bar") { |f| false } - end - - it "should yield after each import" do - yielded = [] - @loader.expects(:import).with("foo/bar",nil) - @loader.expects(:import).with("foo",nil) - @loader.try_load_fqname("foo::bar") { |filename, modname| yielded << [filename, modname]; false } - yielded.should == [["foo/bar", nil], ["foo", nil]] + @loader.expects(:import).with("foo/bar",nil).returns([]) + @loader.expects(:import).with("foo",nil).returns([]) + @loader.try_load_fqname(:hostclass, "foo::bar") { |f| false } end it "should know when a given name has been loaded" do - @loader.expects(:import).with("file",nil) - @loader.try_load_fqname("file") { |f| true } + @loader.expects(:import).with("file",nil).returns([]) + @loader.try_load_fqname(:hostclass, "file") { |f| true } @loader.should be_loaded("file") end end @@ -55,7 +47,7 @@ describe Puppet::Parser::TypeLoader do describe "when importing" do before do Puppet::Parser::Files.stubs(:find_manifests).returns ["modname", %w{file}] - @loader.stubs(:parse_file) + @loader.stubs(:parse_file).returns(Puppet::Parser::AST::Hostclass.new('')) end it "should return immediately when imports are being ignored" do @@ -86,13 +78,13 @@ describe Puppet::Parser::TypeLoader do it "should parse each found file" do Puppet::Parser::Files.expects(:find_manifests).returns ["modname", %w{/one}] - @loader.expects(:parse_file).with("/one") + @loader.expects(:parse_file).with("/one").returns(Puppet::Parser::AST::Hostclass.new('')) @loader.import("myfile") end it "should make each file qualified before attempting to parse it" do Puppet::Parser::Files.expects(:find_manifests).returns ["modname", %w{one}] - @loader.expects(:parse_file).with("/current/one") + @loader.expects(:parse_file).with("/current/one").returns(Puppet::Parser::AST::Hostclass.new('')) @loader.import("myfile", "/current/file") end @@ -105,7 +97,7 @@ describe Puppet::Parser::TypeLoader do it "should not attempt to import files that have already been imported" do Puppet::Parser::Files.expects(:find_manifests).returns ["modname", %w{/one}] - @loader.expects(:parse_file).once + @loader.expects(:parse_file).once.returns(Puppet::Parser::AST::Hostclass.new('')) @loader.import("myfile") # This will fail if it tries to reimport the file. @@ -116,7 +108,7 @@ describe Puppet::Parser::TypeLoader do describe "when parsing a file" do before do @parser = Puppet::Parser::Parser.new(@loader.environment) - @parser.stubs(:parse) + @parser.stubs(:parse).returns(Puppet::Parser::AST::Hostclass.new('')) @parser.stubs(:file=) Puppet::Parser::Parser.stubs(:new).with(@loader.environment).returns @parser end @@ -128,7 +120,7 @@ describe Puppet::Parser::TypeLoader do it "should assign the parser its file and parse" do @parser.expects(:file=).with("/my/file") - @parser.expects(:parse) + @parser.expects(:parse).returns(Puppet::Parser::AST::Hostclass.new('')) @loader.parse_file("/my/file") end end -- cgit