diff --git a/lib/puppet/parser/parser_support.rb b/lib/puppet/parser/parser_support.rb index c0fd37178..97d985cfb 100644 --- a/lib/puppet/parser/parser_support.rb +++ b/lib/puppet/parser/parser_support.rb @@ -1,235 +1,235 @@ # 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_or_load(namespace, name, :hostclass) + known_resource_types.find_hostclass(namespace, name) end def find_definition(namespace, name) - known_resource_types.find_or_load(namespace, name, :definition) + 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 # Create a new class, or merge with an existing class. def newclass(name, options = {}) known_resource_types.add Puppet::Resource::Type.new(:hostclass, name, ast_context(true).merge(options)) end # Create a new definition. def newdefine(name, options = {}) known_resource_types.add Puppet::Resource::Type.new(:definition, name, ast_context(true).merge(options)) end # Create a new node. Nodes are special, because they're stored in a global # table, not according to namespaces. def newnode(names, options = {}) names = [names] unless names.instance_of?(Array) context = ast_context(true) names.collect do |name| known_resource_types.add(Puppet::Resource::Type.new(:node, name, context.merge(options))) end 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) return parse_ruby_file if self.file =~ /\.rb$/ 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 if main # Store the results as the top-level class. newclass("", :code => main) end return known_resource_types ensure @lexer.clear end def parse_ruby_file require self.file 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/lib/puppet/parser/type_loader.rb b/lib/puppet/parser/type_loader.rb index 09aa636e1..516c1e32b 100644 --- a/lib/puppet/parser/type_loader.rb +++ b/lib/puppet/parser/type_loader.rb @@ -1,146 +1,140 @@ require 'puppet/node/environment' class Puppet::Parser::TypeLoader include Puppet::Node::Environment::Helper class Helper < Hash include MonitorMixin def done_with(item) synchronize do delete(item)[:busy].signal if self.has_key?(item) and self[item][:loader] == Thread.current end end def owner_of(item) synchronize do if !self.has_key? item self[item] = { :loader => Thread.current, :busy => self.new_cond} :nobody elsif self[item][:loader] == Thread.current :this_thread else flag = self[item][:busy] flag.wait flag.signal :another_thread end end end end # Import our files. def import(file, current_file = nil) return if Puppet[:ignoreimport] # use a path relative to the file doing the importing if current_file dir = current_file.sub(%r{[^/]+$},'').sub(/\/$/, '') else dir = "." end if dir == "" dir = "." end pat = file modname, files = Puppet::Parser::Files.find_manifests(pat, :cwd => dir, :environment => environment) if files.size == 0 raise Puppet::ImportError.new("No file(s) found for import of '#{pat}'") end files.each do |file| unless file =~ /^#{File::SEPARATOR}/ file = File.join(dir, file) end unless imported? file @imported[file] = true parse_file(file) end end modname end def imported?(file) @imported.has_key?(file) end def known_resource_types environment.known_resource_types end def initialize(env) self.environment = env @loaded = {} @loading = Helper.new @imported = {} end - def load_until(namespaces, name) - return nil if name == "" # special-case main. - name2files(namespaces, name).each do |filename| - modname = begin - import_if_possible(filename) - rescue Puppet::ImportError => detail - # We couldn't load the item - # I'm not convienced we should just drop these errors, but this - # preserves existing behaviours. - nil - end - if result = yield(filename) - Puppet.debug "Automatically imported #{name} from #{filename} into #{environment}" - result.module_name = modname if modname and result.respond_to?(:module_name=) - return result + # Try to load the object with the given fully qualified name. For + # each file that was actually loaded, yield(filename, modname). + def try_load_fqname(fqname) + return nil if fqname == "" # special-case main. + name2files(fqname).each do |filename| + if not loaded?(filename) + modname = begin + import_if_possible(filename) + rescue Puppet::ImportError => detail + # We couldn't load the item + # I'm not convienced we should just drop these errors, but this + # preserves existing behaviours. + nil + end + yield(filename, modname) end end - nil end def loaded?(name) @loaded.include?(name) end - def name2files(namespaces, name) - return [name.sub(/^::/, '').gsub("::", File::SEPARATOR)] if name =~ /^::/ - - result = namespaces.inject([]) do |names_to_try, namespace| - fullname = (namespace + "::#{name}").sub(/^::/, '') - - # Try to load the module init file if we're a qualified name - names_to_try << fullname.split("::")[0] if fullname.include?("::") - - # Then the fully qualified name - names_to_try << fullname - end - - # Otherwise try to load the bare name on its own. This - # is appropriate if the class we're looking for is in a - # module that's different from our namespace. - result << name - result.uniq.collect { |f| f.gsub("::", File::SEPARATOR) } - end - def parse_file(file) Puppet.debug("importing '#{file}' in environment #{environment}") parser = Puppet::Parser::Parser.new(environment) parser.file = file parser.parse end # Utility method factored out of load for handling thread-safety. # This isn't tested in the specs, because that's basically impossible. def import_if_possible(file, current_file = nil) @loaded[file] || begin case @loading.owner_of(file) when :this_thread nil when :another_thread import_if_possible(file,current_file) when :nobody @loaded[file] = import(file,current_file) end ensure @loading.done_with(file) end end + + private + + # Return a list of all file basenames that should be tried in order + # to load the object with the given fully qualified name. + def name2files(fqname) + result = [] + ary = fqname.split("::") + while ary.length > 0 + result << ary.join(File::SEPARATOR) + ary.pop + end + return result + end + end diff --git a/lib/puppet/resource/type_collection.rb b/lib/puppet/resource/type_collection.rb index 63d110395..3a327e264 100644 --- a/lib/puppet/resource/type_collection.rb +++ b/lib/puppet/resource/type_collection.rb @@ -1,214 +1,223 @@ class Puppet::Resource::TypeCollection attr_reader :environment def clear @hostclasses.clear @definitions.clear @nodes.clear end def initialize(env) @environment = env.is_a?(String) ? Puppet::Node::Environment.new(env) : env @hostclasses = {} @definitions = {} @nodes = {} # So we can keep a list and match the first-defined regex @node_list = [] @watched_files = {} end def <<(thing) add(thing) self end def add(instance) if instance.type == :hostclass and other = @hostclasses[instance.name] and other.type == :hostclass other.merge(instance) return other end method = "add_#{instance.type}" send(method, instance) instance.resource_type_collection = self instance end def add_hostclass(instance) dupe_check(instance, @hostclasses) { |dupe| "Class '#{instance.name}' is already defined#{dupe.error_context}; cannot redefine" } dupe_check(instance, @definitions) { |dupe| "Definition '#{instance.name}' is already defined#{dupe.error_context}; cannot be redefined as a class" } @hostclasses[instance.name] = instance instance end def hostclass(name) @hostclasses[munge_name(name)] end def add_node(instance) dupe_check(instance, @nodes) { |dupe| "Node '#{instance.name}' is already defined#{dupe.error_context}; cannot redefine" } @node_list << instance @nodes[instance.name] = instance instance end def loader require 'puppet/parser/type_loader' @loader ||= Puppet::Parser::TypeLoader.new(environment) end def node(name) name = munge_name(name) if node = @nodes[name] return node end @node_list.each do |node| next unless node.name_is_regex? return node if node.match(name) end nil end def node_exists?(name) @nodes[munge_name(name)] end def nodes? @nodes.length > 0 end def add_definition(instance) dupe_check(instance, @hostclasses) { |dupe| "'#{instance.name}' is already defined#{dupe.error_context} as a class; cannot redefine as a definition" } dupe_check(instance, @definitions) { |dupe| "Definition '#{instance.name}' is already defined#{dupe.error_context}; cannot be redefined" } @definitions[instance.name] = instance end def definition(name) @definitions[munge_name(name)] end - def find(namespaces, name, type) - #Array("") == [] for some reason - namespaces = [namespaces] unless namespaces.is_a?(Array) - - if name =~ /^::/ - return send(type, name.sub(/^::/, '')) - end - - namespaces.each do |namespace| - ary = namespace.split("::") - - while ary.length > 0 - tmp_namespace = ary.join("::") - if r = find_partially_qualified(tmp_namespace, name, type) - return r - end - - # Delete the second to last object, which reduces our namespace by one. - ary.pop - end - - if result = send(type, name) - return result - end - end - nil - end - - def find_or_load(namespaces, name, type) - name = name.downcase - namespaces = [namespaces] unless namespaces.is_a?(Array) - namespaces = namespaces.collect { |ns| ns.downcase } - - # This could be done in the load_until, but the knowledge seems to - # belong here. - if r = find(namespaces, name, type) - return r - end - - loader.load_until(namespaces, name) { find(namespaces, name, type) } - end - def find_node(namespaces, name) - find("", name, :node) + @nodes[munge_name(name)] end def find_hostclass(namespaces, name) find_or_load(namespaces, name, :hostclass) end def find_definition(namespaces, name) find_or_load(namespaces, name, :definition) end [:hostclasses, :nodes, :definitions].each do |m| define_method(m) do instance_variable_get("@#{m}").dup end end def perform_initial_import return if Puppet.settings[:ignoreimport] parser = Puppet::Parser::Parser.new(environment) if code = Puppet.settings.uninterpolated_value(:code, environment.to_s) and code != "" parser.string = code else file = Puppet.settings.value(:manifest, environment.to_s) return unless File.exist?(file) parser.file = file end parser.parse rescue => detail msg = "Could not parse for environment #{environment}: #{detail}" error = Puppet::Error.new(msg) error.set_backtrace(detail.backtrace) raise error end def stale? @watched_files.values.detect { |file| file.changed? } end def version return @version if defined?(@version) if environment[:config_version] == "" @version = Time.now.to_i return @version end @version = Puppet::Util.execute([environment[:config_version]]).strip rescue Puppet::ExecutionFailure => e raise Puppet::ParseError, "Unable to set config_version: #{e.message}" end def watch_file(file) @watched_files[file] = Puppet::Util::LoadedFile.new(file) end def watching_file?(file) @watched_files.include?(file) end private - def find_partially_qualified(namespace, name, type) - send(type, [namespace, name].join("::")) + # Return a list of all possible fully-qualified names that might be + # meant by the given name, in the context of namespaces. + def resolve_namespaces(namespaces, name) + name = name.downcase + if name =~ /^::/ + # name is explicitly fully qualified, so just return it, sans + # initial "::". + return [name.sub(/^::/, '')] + end + if name == "" + # The name "" has special meaning--it always refers to a "main" + # hostclass which contains all toplevel resources. + return [""] + end + + namespaces = [namespaces] unless namespaces.is_a?(Array) + namespaces = namespaces.collect { |ns| ns.downcase } + + result = [] + namespaces.each do |namespace| + ary = namespace.split("::") + + # Search each namespace nesting in innermost-to-outermost order. + while ary.length > 0 + result << "#{ary.join("::")}::#{name}" + ary.pop + end + + # Finally, search the toplevel namespace. + result << name + end + + return result.uniq + end + + # Resolve namespaces and find the given object. Autoload it if + # necessary. + def find_or_load(namespaces, name, type) + resolve_namespaces(namespaces, name).each do |fqname| + if result = send(type, fqname) + return result + end + loader.try_load_fqname(fqname) do |filename, modname| + if result = send(type, fqname) + Puppet.debug "Automatically imported #{name} from #{filename} into #{environment}" + result.module_name = modname if modname and result.respond_to?(:module_name=) + return result + end + end + end + + # Nothing found. + return nil end def munge_name(name) name.to_s.downcase end def dupe_check(instance, hash) return unless dupe = hash[instance.name] message = yield dupe instance.fail Puppet::ParseError, message end end 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 @@ -1,201 +1,154 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/parser/type_loader' require 'puppet_spec/files' describe Puppet::Parser::TypeLoader do include PuppetSpec::Files before do @loader = Puppet::Parser::TypeLoader.new(:myenv) end it "should support an environment" do loader = Puppet::Parser::TypeLoader.new(:myenv) loader.environment.name.should == :myenv end it "should include the Environment Helper" do @loader.class.ancestors.should be_include(Puppet::Node::Environment::Helper) end it "should delegate its known resource types to its environment" do @loader.known_resource_types.should be_instance_of(Puppet::Resource::TypeCollection) end 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 before do Puppet::Parser::Files.stubs(:find_manifests).returns ["modname", %w{file}] @loader.stubs(:parse_file) end it "should return immediately when imports are being ignored" do Puppet::Parser::Files.expects(:find_manifests).never Puppet[:ignoreimport] = true @loader.import("foo").should be_nil end it "should find all manifests matching the file or pattern" do Puppet::Parser::Files.expects(:find_manifests).with { |pat, opts| pat == "myfile" }.returns ["modname", %w{one}] @loader.import("myfile") end it "should use the directory of the current file if one is set" do Puppet::Parser::Files.expects(:find_manifests).with { |pat, opts| opts[:cwd] == "/current" }.returns ["modname", %w{one}] @loader.import("myfile", "/current/file") end it "should pass the environment when looking for files" do Puppet::Parser::Files.expects(:find_manifests).with { |pat, opts| opts[:environment] == @loader.environment }.returns ["modname", %w{one}] @loader.import("myfile") end it "should fail if no files are found" do Puppet::Parser::Files.expects(:find_manifests).returns [nil, []] lambda { @loader.import("myfile") }.should raise_error(Puppet::ImportError) end it "should parse each found file" do Puppet::Parser::Files.expects(:find_manifests).returns ["modname", %w{/one}] @loader.expects(:parse_file).with("/one") @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.import("myfile", "/current/file") end it "should know when a given file has been imported" do Puppet::Parser::Files.expects(:find_manifests).returns ["modname", %w{/one}] @loader.import("myfile") @loader.should be_imported("/one") end 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.import("myfile") # This will fail if it tries to reimport the file. @loader.import("myfile") end end describe "when parsing a file" do before do @parser = Puppet::Parser::Parser.new(@loader.environment) @parser.stubs(:parse) @parser.stubs(:file=) Puppet::Parser::Parser.stubs(:new).with(@loader.environment).returns @parser end it "should create a new parser instance for each file using the current environment" do Puppet::Parser::Parser.expects(:new).with(@loader.environment).returns @parser @loader.parse_file("/my/file") end it "should assign the parser its file and parse" do @parser.expects(:file=).with("/my/file") @parser.expects(:parse) @loader.parse_file("/my/file") end end it "should be able to add classes to the current resource type collection" do file = tmpfile("simple_file") File.open(file, "w") { |f| f.puts "class foo {}" } @loader.import(file) @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 diff --git a/spec/unit/resource/type_collection_spec.rb b/spec/unit/resource/type_collection_spec.rb index 577aea42b..4bad29116 100644 --- a/spec/unit/resource/type_collection_spec.rb +++ b/spec/unit/resource/type_collection_spec.rb @@ -1,467 +1,499 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/resource/type_collection' require 'puppet/resource/type' describe Puppet::Resource::TypeCollection do before do @instance = Puppet::Resource::Type.new(:hostclass, "foo") @code = Puppet::Resource::TypeCollection.new("env") end it "should require an environment at initialization" do env = Puppet::Node::Environment.new("testing") Puppet::Resource::TypeCollection.new(env).environment.should equal(env) end it "should convert the environment into an environment instance if a string is provided" do env = Puppet::Node::Environment.new("testing") Puppet::Resource::TypeCollection.new("testing").environment.should equal(env) end it "should create a 'loader' at initialization" do Puppet::Resource::TypeCollection.new("testing").loader.should be_instance_of(Puppet::Parser::TypeLoader) end it "should be able to add a resource type" do Puppet::Resource::TypeCollection.new("env").should respond_to(:add) end it "should consider '<<' to be an alias to 'add' but should return self" do loader = Puppet::Resource::TypeCollection.new("env") loader.expects(:add).with "foo" loader.expects(:add).with "bar" loader << "foo" << "bar" end it "should set itself as the code collection for added resource types" do loader = Puppet::Resource::TypeCollection.new("env") node = Puppet::Resource::Type.new(:node, "foo") @code.add(node) @code.node("foo").should equal(node) node.resource_type_collection.should equal(@code) end it "should store node resource types as nodes" do node = Puppet::Resource::Type.new(:node, "foo") @code.add(node) @code.node("foo").should equal(node) end it "should store hostclasses as hostclasses" do klass = Puppet::Resource::Type.new(:hostclass, "foo") @code.add(klass) @code.hostclass("foo").should equal(klass) end it "should store definitions as definitions" do define = Puppet::Resource::Type.new(:definition, "foo") @code.add(define) @code.definition("foo").should equal(define) end it "should merge new classes with existing classes of the same name" do loader = Puppet::Resource::TypeCollection.new("env") first = Puppet::Resource::Type.new(:hostclass, "foo") second = Puppet::Resource::Type.new(:hostclass, "foo") loader.add first first.expects(:merge).with(second) loader.add(second) end it "should remove all nodes, classes, and definitions when cleared" do loader = Puppet::Resource::TypeCollection.new("env") loader.add Puppet::Resource::Type.new(:hostclass, "class") loader.add Puppet::Resource::Type.new(:definition, "define") loader.add Puppet::Resource::Type.new(:node, "node") loader.clear loader.hostclass("class").should be_nil loader.definition("define").should be_nil loader.node("node").should be_nil end + describe "when resolving namespaces" do + [ ['', '::foo', ['foo']], + ['a', '::foo', ['foo']], + ['a::b', '::foo', ['foo']], + [['a::b'], '::foo', ['foo']], + [['a::b', 'c'], '::foo', ['foo']], + [['A::B', 'C'], '::Foo', ['foo']], + ['', '', ['']], + ['a', '', ['']], + ['a::b', '', ['']], + [['a::b'], '', ['']], + [['a::b', 'c'], '', ['']], + [['A::B', 'C'], '', ['']], + ['', 'foo', ['foo']], + ['a', 'foo', ['a::foo', 'foo']], + ['a::b', 'foo', ['a::b::foo', 'a::foo', 'foo']], + ['A::B', 'Foo', ['a::b::foo', 'a::foo', 'foo']], + [['a::b'], 'foo', ['a::b::foo', 'a::foo', 'foo']], + [['a', 'b'], 'foo', ['a::foo', 'foo', 'b::foo']], + [['a::b', 'c::d'], 'foo', ['a::b::foo', 'a::foo', 'foo', 'c::d::foo', 'c::foo']], + [['a::b', 'a::c'], 'foo', ['a::b::foo', 'a::foo', 'foo', 'a::c::foo']], + ].each do |namespaces, name, expected_result| + it "should resolve #{name.inspect} in namespaces #{namespaces.inspect} correctly" do + @code.instance_eval { resolve_namespaces(namespaces, name) }.should == expected_result + end + end + end + describe "when looking up names" do before do @type = Puppet::Resource::Type.new(:hostclass, "ns::klass") end it "should support looking up with multiple namespaces" do @code.add @type @code.find_hostclass(%w{boo baz ns}, "klass").should equal(@type) end it "should not attempt to import anything when the type is already defined" do @code.add @type @code.loader.expects(:import).never @code.find_hostclass(%w{ns}, "klass").should equal(@type) end describe "that need to be loaded" do it "should use the loader to load the files" do - @code.loader.expects(:load_until).with(["ns"], "klass") - @code.find_or_load(["ns"], "klass", :hostclass) + @code.loader.expects(:try_load_fqname).with("ns::klass") + @code.loader.expects(:try_load_fqname).with("klass") + @code.find_hostclass(["ns"], "klass") end it "should downcase the name and downcase and array-fy the namespaces before passing to the loader" do - @code.loader.expects(:load_until).with(["ns"], "klass") - @code.find_or_load("Ns", "Klass", :hostclass) + @code.loader.expects(:try_load_fqname).with("ns::klass") + @code.loader.expects(:try_load_fqname).with("klass") + @code.find_hostclass("Ns", "Klass") end it "should attempt to find the type when the loader yields" do - @code.loader.expects(:load_until).yields - @code.expects(:find).with(["ns"], "klass", :hostclass).times(2).returns(false).then.returns(true) - @code.find_or_load("ns", "klass", :hostclass) + @code.loader.expects(:try_load_fqname).yields + @code.expects(:hostclass).with("ns::klass").times(2).returns(false).then.returns(true) + @code.find_hostclass("ns", "klass") end - it "should return the result of 'load_until'" do - @code.loader.expects(:load_until).returns "foo" - @code.find_or_load("Ns", "Klass", :hostclass).should == "foo" + it "should return nil if the name isn't found" do + @code.stubs(:try_load_fqname).returns(nil) + @code.find_hostclass("Ns", "Klass").should be_nil end - it "should return nil if the name isn't found" do - @code.stubs(:load_until).returns(nil) - @code.find_or_load("Ns", "Klass", :hostclass).should be_nil + it "already-loaded names at broader scopes should not shadow autoloaded names" do + @code.add Puppet::Resource::Type.new(:hostclass, "bar") + @code.loader.expects(:try_load_fqname).with("foo::bar") + @code.find_hostclass("foo", "bar") end end end %w{hostclass node definition}.each do |data| before do @instance = Puppet::Resource::Type.new(data, "foo") end it "should have a method for adding a #{data}" do Puppet::Resource::TypeCollection.new("env").should respond_to("add_#{data}") end it "should use the name of the instance to add it" do loader = Puppet::Resource::TypeCollection.new("env") loader.send("add_#{data}", @instance) loader.send(data, @instance.name).should equal(@instance) end unless data == "hostclass" it "should fail to add a #{data} when one already exists" do loader = Puppet::Resource::TypeCollection.new("env") loader.add @instance lambda { loader.add(@instance) }.should raise_error(Puppet::ParseError) end end it "should return the added #{data}" do loader = Puppet::Resource::TypeCollection.new("env") loader.add(@instance).should equal(@instance) end it "should be able to retrieve #{data} by name" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(data, "bar") loader.add instance loader.send(data, "bar").should equal(instance) end it "should retrieve #{data} insensitive to case" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(data, "Bar") loader.add instance loader.send(data, "bAr").should equal(instance) end it "should return nil when asked for a #{data} that has not been added" do Puppet::Resource::TypeCollection.new("env").send(data, "foo").should be_nil end it "should be able to retrieve all #{data}s" do plurals = { "hostclass" => "hostclasses", "node" => "nodes", "definition" => "definitions" } loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(data, "foo") loader.add instance loader.send(plurals[data]).should == { "foo" => instance } end end describe "when finding a qualified instance" do it "should return any found instance if the instance name is fully qualified" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar") loader.add instance - loader.find("namespace", "::foo::bar", :hostclass).should equal(instance) + loader.find_hostclass("namespace", "::foo::bar").should equal(instance) end it "should return nil if the instance name is fully qualified and no such instance exists" do loader = Puppet::Resource::TypeCollection.new("env") - loader.find("namespace", "::foo::bar", :hostclass).should be_nil + loader.find_hostclass("namespace", "::foo::bar").should be_nil end it "should be able to find classes in the base namespace" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo") loader.add instance - loader.find("", "foo", :hostclass).should equal(instance) + loader.find_hostclass("", "foo").should equal(instance) end it "should return the partially qualified object if it exists in a provided namespace" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance - loader.find("foo", "bar::baz", :hostclass).should equal(instance) + loader.find_hostclass("foo", "bar::baz").should equal(instance) end it "should be able to find partially qualified objects in any of the provided namespaces" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance - loader.find(["nons", "foo", "otherns"], "bar::baz", :hostclass).should equal(instance) + loader.find_hostclass(["nons", "foo", "otherns"], "bar::baz").should equal(instance) end it "should return the unqualified object if it exists in a provided namespace" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar") loader.add instance - loader.find("foo", "bar", :hostclass).should equal(instance) + loader.find_hostclass("foo", "bar").should equal(instance) end it "should return the unqualified object if it exists in the parent namespace" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar") loader.add instance - loader.find("foo::bar::baz", "bar", :hostclass).should equal(instance) + loader.find_hostclass("foo::bar::baz", "bar").should equal(instance) end it "should should return the partially qualified object if it exists in the parent namespace" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance - loader.find("foo::bar", "bar::baz", :hostclass).should equal(instance) + loader.find_hostclass("foo::bar", "bar::baz").should equal(instance) end it "should return the qualified object if it exists in the root namespace" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance - loader.find("foo::bar", "foo::bar::baz", :hostclass).should equal(instance) + loader.find_hostclass("foo::bar", "foo::bar::baz").should equal(instance) end it "should return nil if the object cannot be found" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance - loader.find("foo::bar", "eh", :hostclass).should be_nil + loader.find_hostclass("foo::bar", "eh").should be_nil end describe "when topscope has a class that has the same name as a local class" do before do @loader = Puppet::Resource::TypeCollection.new("env") [ "foo::bar", "bar" ].each do |name| @loader.add Puppet::Resource::Type.new(:hostclass, name) end end it "should favor the local class, if the name is unqualified" do - @loader.find("foo", "bar", :hostclass).name.should == 'foo::bar' + @loader.find_hostclass("foo", "bar").name.should == 'foo::bar' end it "should only look in the topclass, if the name is qualified" do - @loader.find("foo", "::bar", :hostclass).name.should == 'bar' + @loader.find_hostclass("foo", "::bar").name.should == 'bar' end end it "should not look in the local scope for classes when the name is qualified" do @loader = Puppet::Resource::TypeCollection.new("env") @loader.add Puppet::Resource::Type.new(:hostclass, "foo::bar") - @loader.find("foo", "::bar", :hostclass).should == nil + @loader.find_hostclass("foo", "::bar").should == nil end end - it "should use the generic 'find' method with an empty namespace to find nodes" do + it "should be able to find nodes" do + node = Puppet::Resource::Type.new(:node, "bar") loader = Puppet::Resource::TypeCollection.new("env") - loader.expects(:find).with("", "bar", :node) - loader.find_node(stub("ignored"), "bar") + loader.add(node) + loader.find_node(stub("ignored"), "bar").should == node end it "should use the 'find_or_load' method to find hostclasses" do loader = Puppet::Resource::TypeCollection.new("env") loader.expects(:find_or_load).with("foo", "bar", :hostclass) loader.find_hostclass("foo", "bar") end it "should use the 'find_or_load' method to find definitions" do loader = Puppet::Resource::TypeCollection.new("env") loader.expects(:find_or_load).with("foo", "bar", :definition) loader.find_definition("foo", "bar") end it "should indicate whether any nodes are defined" do loader = Puppet::Resource::TypeCollection.new("env") loader.add_node(Puppet::Resource::Type.new(:node, "foo")) loader.should be_nodes end it "should indicate whether no nodes are defined" do Puppet::Resource::TypeCollection.new("env").should_not be_nodes end describe "when finding nodes" do before :each do @loader = Puppet::Resource::TypeCollection.new("env") end it "should return any node whose name exactly matches the provided node name" do node = Puppet::Resource::Type.new(:node, "foo") @loader << node @loader.node("foo").should equal(node) end it "should return the first regex node whose regex matches the provided node name" do node1 = Puppet::Resource::Type.new(:node, /\w/) node2 = Puppet::Resource::Type.new(:node, /\d/) @loader << node1 << node2 @loader.node("foo10").should equal(node1) end it "should preferentially return a node whose name is string-equal over returning a node whose regex matches a provided name" do node1 = Puppet::Resource::Type.new(:node, /\w/) node2 = Puppet::Resource::Type.new(:node, "foo") @loader << node1 << node2 @loader.node("foo").should equal(node2) end end describe "when managing files" do before do @loader = Puppet::Resource::TypeCollection.new("env") Puppet::Util::LoadedFile.stubs(:new).returns stub("watched_file") end it "should have a method for specifying a file should be watched" do @loader.should respond_to(:watch_file) end it "should have a method for determining if a file is being watched" do @loader.watch_file("/foo/bar") @loader.should be_watching_file("/foo/bar") end it "should use LoadedFile to watch files" do Puppet::Util::LoadedFile.expects(:new).with("/foo/bar").returns stub("watched_file") @loader.watch_file("/foo/bar") end it "should be considered stale if any files have changed" do file1 = stub 'file1', :changed? => false file2 = stub 'file2', :changed? => true Puppet::Util::LoadedFile.expects(:new).times(2).returns(file1).then.returns(file2) @loader.watch_file("/foo/bar") @loader.watch_file("/other/bar") @loader.should be_stale end it "should not be considered stable if no files have changed" do file1 = stub 'file1', :changed? => false file2 = stub 'file2', :changed? => false Puppet::Util::LoadedFile.expects(:new).times(2).returns(file1).then.returns(file2) @loader.watch_file("/foo/bar") @loader.watch_file("/other/bar") @loader.should_not be_stale end end describe "when performing initial import" do before do @parser = stub 'parser', :file= => nil, :string => nil, :parse => nil Puppet::Parser::Parser.stubs(:new).returns @parser @code = Puppet::Resource::TypeCollection.new("env") end it "should create a new parser instance" do Puppet::Parser::Parser.expects(:new).returns @parser @code.perform_initial_import end it "should set the parser's string to the 'code' setting and parse if code is available" do Puppet.settings[:code] = "my code" @parser.expects(:string=).with "my code" @parser.expects(:parse) @code.perform_initial_import end it "should set the parser's file to the 'manifest' setting and parse if no code is available and the manifest is available" do File.stubs(:expand_path).with("/my/file").returns "/my/file" File.expects(:exist?).with("/my/file").returns true Puppet.settings[:manifest] = "/my/file" @parser.expects(:file=).with "/my/file" @parser.expects(:parse) @code.perform_initial_import end it "should not attempt to load a manifest if none is present" do File.stubs(:expand_path).with("/my/file").returns "/my/file" File.expects(:exist?).with("/my/file").returns false Puppet.settings[:manifest] = "/my/file" @parser.expects(:file=).never @parser.expects(:parse).never @code.perform_initial_import end it "should fail helpfully if there is an error importing" do File.stubs(:exist?).returns true @parser.expects(:parse).raises ArgumentError lambda { @code.perform_initial_import }.should raise_error(Puppet::Error) end it "should not do anything if the ignore_import settings is set" do Puppet.settings[:ignoreimport] = true @parser.expects(:string=).never @parser.expects(:file=).never @parser.expects(:parse).never @code.perform_initial_import end end describe "when determining the configuration version" do before do @code = Puppet::Resource::TypeCollection.new("env") end it "should default to the current time" do time = Time.now Time.stubs(:now).returns time @code.version.should == time.to_i end it "should use the output of the environment's config_version setting if one is provided" do @code.environment.stubs(:[]).with(:config_version).returns("/my/foo") Puppet::Util.expects(:execute).with(["/my/foo"]).returns "output\n" @code.version.should == "output" end it "should raise a puppet parser error if executing config_version fails" do @code.environment.stubs(:[]).with(:config_version).returns("test") Puppet::Util.expects(:execute).raises(Puppet::ExecutionFailure.new("msg")) lambda { @code.version }.should raise_error(Puppet::ParseError) end end end diff --git a/spec/unit/resource/type_spec.rb b/spec/unit/resource/type_spec.rb index 4d3942c5c..ac9d3811b 100755 --- a/spec/unit/resource/type_spec.rb +++ b/spec/unit/resource/type_spec.rb @@ -1,723 +1,722 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/resource/type' describe Puppet::Resource::Type do it "should have a 'name' attribute" do Puppet::Resource::Type.new(:hostclass, "foo").name.should == "foo" end [:code, :doc, :line, :file, :resource_type_collection, :ruby_code].each do |attr| it "should have a '#{attr}' attribute" do type = Puppet::Resource::Type.new(:hostclass, "foo") type.send(attr.to_s + "=", "yay") type.send(attr).should == "yay" end end [:hostclass, :node, :definition].each do |type| it "should know when it is a #{type}" do Puppet::Resource::Type.new(type, "foo").send("#{type}?").should be_true end end it "should indirect 'resource_type'" do Puppet::Resource::Type.indirection.name.should == :resource_type end it "should default to 'parser' for its terminus class" do Puppet::Resource::Type.indirection.terminus_class.should == :parser end describe "when converting to json" do before do @type = Puppet::Resource::Type.new(:hostclass, "foo") end def from_json(json) Puppet::Resource::Type.from_pson(json) end def double_convert Puppet::Resource::Type.from_pson(PSON.parse(@type.to_pson)) end it "should include the name and type" do double_convert.name.should == @type.name double_convert.type.should == @type.type end it "should include any arguments" do @type.set_arguments("one" => nil, "two" => "foo") double_convert.arguments.should == {"one" => nil, "two" => "foo"} end it "should include any extra attributes" do @type.file = "/my/file" @type.line = 50 double_convert.file.should == "/my/file" double_convert.line.should == 50 end end describe "when a node" do it "should allow a regex as its name" do lambda { Puppet::Resource::Type.new(:node, /foo/) }.should_not raise_error end it "should allow a AST::HostName instance as its name" do regex = Puppet::Parser::AST::Regex.new(:value => /foo/) name = Puppet::Parser::AST::HostName.new(:value => regex) lambda { Puppet::Resource::Type.new(:node, name) }.should_not raise_error end it "should match against the regexp in the AST::HostName when a HostName instance is provided" do regex = Puppet::Parser::AST::Regex.new(:value => /\w/) name = Puppet::Parser::AST::HostName.new(:value => regex) node = Puppet::Resource::Type.new(:node, name) node.match("foo").should be_true end it "should return the value of the hostname if provided a string-form AST::HostName instance as the name" do name = Puppet::Parser::AST::HostName.new(:value => "foo") node = Puppet::Resource::Type.new(:node, name) node.name.should == "foo" end describe "and the name is a regex" do it "should have a method that indicates that this is the case" do Puppet::Resource::Type.new(:node, /w/).should be_name_is_regex end it "should set its namespace to ''" do Puppet::Resource::Type.new(:node, /w/).namespace.should == "" end it "should return the regex converted to a string when asked for its name" do Puppet::Resource::Type.new(:node, /ww/).name.should == "ww" end it "should downcase the regex when returning the name as a string" do Puppet::Resource::Type.new(:node, /W/).name.should == "w" end it "should remove non-alpha characters when returning the name as a string" do Puppet::Resource::Type.new(:node, /w*w/).name.should_not include("*") end it "should remove leading dots when returning the name as a string" do Puppet::Resource::Type.new(:node, /.ww/).name.should_not =~ /^\./ end it "should have a method for matching its regex name against a provided name" do Puppet::Resource::Type.new(:node, /.ww/).should respond_to(:match) end it "should return true when its regex matches the provided name" do Puppet::Resource::Type.new(:node, /\w/).match("foo").should be_true end it "should return false when its regex does not match the provided name" do (!!Puppet::Resource::Type.new(:node, /\d/).match("foo")).should be_false end it "should return true when its name, as a string, is matched against an equal string" do Puppet::Resource::Type.new(:node, "foo").match("foo").should be_true end it "should return false when its name is matched against an unequal string" do Puppet::Resource::Type.new(:node, "foo").match("bar").should be_false end it "should match names insensitive to case" do Puppet::Resource::Type.new(:node, "fOo").match("foO").should be_true end end it "should return the name converted to a string when the name is not a regex" do pending "Need to define LoadedCode behaviour first" name = Puppet::Parser::AST::HostName.new(:value => "foo") Puppet::Resource::Type.new(:node, name).name.should == "foo" end it "should return the name converted to a string when the name is a regex" do pending "Need to define LoadedCode behaviour first" name = Puppet::Parser::AST::HostName.new(:value => /regex/) Puppet::Resource::Type.new(:node, name).name.should == /regex/.to_s end it "should mark any created scopes as a node scope" do pending "Need to define LoadedCode behaviour first" name = Puppet::Parser::AST::HostName.new(:value => /regex/) Puppet::Resource::Type.new(:node, name).name.should == /regex/.to_s end end describe "when initializing" do it "should require a resource super type" do Puppet::Resource::Type.new(:hostclass, "foo").type.should == :hostclass end it "should fail if provided an invalid resource super type" do lambda { Puppet::Resource::Type.new(:nope, "foo") }.should raise_error(ArgumentError) end it "should set its name to the downcased, stringified provided name" do Puppet::Resource::Type.new(:hostclass, "Foo::Bar".intern).name.should == "foo::bar" end it "should set its namespace to the downcased, stringified qualified name for classes" do Puppet::Resource::Type.new(:hostclass, "Foo::Bar::Baz".intern).namespace.should == "foo::bar::baz" end [:definition, :node].each do |type| it "should set its namespace to the downcased, stringified qualified portion of the name for #{type}s" do Puppet::Resource::Type.new(type, "Foo::Bar::Baz".intern).namespace.should == "foo::bar" end end %w{code line file doc}.each do |arg| it "should set #{arg} if provided" do type = Puppet::Resource::Type.new(:hostclass, "foo", arg.to_sym => "something") type.send(arg).should == "something" end end it "should set any provided arguments with the keys as symbols" do type = Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {:foo => "bar", :baz => "biz"}) type.should be_valid_parameter("foo") type.should be_valid_parameter("baz") end it "should set any provided arguments with they keys as strings" do type = Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {"foo" => "bar", "baz" => "biz"}) type.should be_valid_parameter(:foo) type.should be_valid_parameter(:baz) end it "should function if provided no arguments" do type = Puppet::Resource::Type.new(:hostclass, "foo") type.should_not be_valid_parameter(:foo) end end describe "when testing the validity of an attribute" do it "should return true if the parameter was typed at initialization" do Puppet::Resource::Type.new(:hostclass, "foo", :arguments => {"foo" => "bar"}).should be_valid_parameter("foo") end it "should return true if it is a metaparam" do Puppet::Resource::Type.new(:hostclass, "foo").should be_valid_parameter("require") end it "should return true if the parameter is named 'name'" do Puppet::Resource::Type.new(:hostclass, "foo").should be_valid_parameter("name") end it "should return false if it is not a metaparam and was not provided at initialization" do Puppet::Resource::Type.new(:hostclass, "foo").should_not be_valid_parameter("yayness") end end describe "when creating a subscope" do before do @scope = stub 'scope', :newscope => nil @resource = stub 'resource' @type = Puppet::Resource::Type.new(:hostclass, "foo") end it "should return a new scope created with the provided scope as the parent" do @scope.expects(:newscope).returns "foo" @type.subscope(@scope, @resource).should == "foo" end it "should set the source as itself" do @scope.expects(:newscope).with { |args| args[:source] == @type } @type.subscope(@scope, @resource) end it "should set the scope's namespace to its namespace" do @type.expects(:namespace).returns "yayness" @scope.expects(:newscope).with { |args| args[:namespace] == "yayness" } @type.subscope(@scope, @resource) end it "should set the scope's resource to the provided resource" do @scope.expects(:newscope).with { |args| args[:resource] == @resource } @type.subscope(@scope, @resource) end end describe "when setting its parameters in the scope" do before do @scope = Puppet::Parser::Scope.new(:compiler => stub("compiler", :environment => Puppet::Node::Environment.new), :source => stub("source")) @resource = Puppet::Parser::Resource.new(:foo, "bar", :scope => @scope) @type = Puppet::Resource::Type.new(:hostclass, "foo") end it "should set each of the resource's parameters as variables in the scope" do @type.set_arguments :foo => nil, :boo => nil @resource[:foo] = "bar" @resource[:boo] = "baz" @type.set_resource_parameters(@resource, @scope) @scope.lookupvar("foo").should == "bar" @scope.lookupvar("boo").should == "baz" end it "should set the variables as strings" do @type.set_arguments :foo => nil @resource[:foo] = "bar" @type.set_resource_parameters(@resource, @scope) @scope.lookupvar("foo").should == "bar" end it "should fail if any of the resource's parameters are not valid attributes" do @type.set_arguments :foo => nil @resource[:boo] = "baz" lambda { @type.set_resource_parameters(@resource, @scope) }.should raise_error(Puppet::ParseError) end it "should evaluate and set its default values as variables for parameters not provided by the resource" do @type.set_arguments :foo => stub("value", :safeevaluate => "something") @type.set_resource_parameters(@resource, @scope) @scope.lookupvar("foo").should == "something" end it "should set all default values as parameters in the resource" do @type.set_arguments :foo => stub("value", :safeevaluate => "something") @type.set_resource_parameters(@resource, @scope) @resource[:foo].should == "something" end it "should fail if the resource does not provide a value for a required argument" do @type.set_arguments :foo => nil @resource.expects(:to_hash).returns({}) lambda { @type.set_resource_parameters(@resource, @scope) }.should raise_error(Puppet::ParseError) end it "should set the resource's title as a variable if not otherwise provided" do @type.set_resource_parameters(@resource, @scope) @scope.lookupvar("title").should == "bar" end it "should set the resource's name as a variable if not otherwise provided" do @type.set_resource_parameters(@resource, @scope) @scope.lookupvar("name").should == "bar" end it "should set its module name in the scope if available" do @type.module_name = "mymod" @type.set_resource_parameters(@resource, @scope) @scope.lookupvar("module_name").should == "mymod" end it "should set its caller module name in the scope if available" do @scope.expects(:parent_module_name).returns "mycaller" @type.set_resource_parameters(@resource, @scope) @scope.lookupvar("caller_module_name").should == "mycaller" end end describe "when describing and managing parent classes" do before do @code = Puppet::Resource::TypeCollection.new("env") @parent = Puppet::Resource::Type.new(:hostclass, "bar") @code.add @parent @child = Puppet::Resource::Type.new(:hostclass, "foo", :parent => "bar") @code.add @child @env = stub "environment", :known_resource_types => @code @scope = stub "scope", :environment => @env, :namespaces => [""] end it "should be able to define a parent" do Puppet::Resource::Type.new(:hostclass, "foo", :parent => "bar") end it "should use the code collection to find the parent resource type" do @child.parent_type(@scope).should equal(@parent) end it "should be able to find parent nodes" do parent = Puppet::Resource::Type.new(:node, "bar") @code.add parent child = Puppet::Resource::Type.new(:node, "foo", :parent => "bar") @code.add child child.parent_type(@scope).should equal(parent) end it "should cache a reference to the parent type" do @code.stubs(:hostclass).with("foo::bar").returns nil @code.expects(:hostclass).with("bar").once.returns @parent @child.parent_type(@scope) @child.parent_type end it "should correctly state when it is another type's child" do @child.parent_type(@scope) @child.should be_child_of(@parent) end it "should be considered the child of a parent's parent" do @grandchild = Puppet::Resource::Type.new(:hostclass, "baz", :parent => "foo") @code.add @grandchild @child.parent_type(@scope) @grandchild.parent_type(@scope) @grandchild.should be_child_of(@parent) end it "should correctly state when it is not another type's child" do @notchild = Puppet::Resource::Type.new(:hostclass, "baz") @code.add @notchild @notchild.should_not be_child_of(@parent) end end describe "when evaluating its code" do before do @compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) @scope = Puppet::Parser::Scope.new :compiler => @compiler @resource = Puppet::Parser::Resource.new(:foo, "yay", :scope => @scope) # This is so the internal resource lookup works, yo. @compiler.catalog.add_resource @resource @known_resource_types = stub 'known_resource_types' @resource.stubs(:known_resource_types).returns @known_resource_types @type = Puppet::Resource::Type.new(:hostclass, "foo") end it "should set all of its parameters in a subscope" do subscope = stub 'subscope', :compiler => @compiler @type.expects(:subscope).with(@scope, @resource).returns subscope @type.expects(:set_resource_parameters).with(@resource, subscope) @type.evaluate_code(@resource) end it "should not create a subscope for the :main class" do @resource.stubs(:title).returns(:main) @type.expects(:subscope).never @type.expects(:set_resource_parameters).with(@resource, @scope) @type.evaluate_code(@resource) end it "should store the class scope" do @type.evaluate_code(@resource) @scope.class_scope(@type).should be_instance_of(@scope.class) end it "should still create a scope but not store it if the type is a definition" do @type = Puppet::Resource::Type.new(:definition, "foo") @type.evaluate_code(@resource) @scope.class_scope(@type).should be_nil end it "should evaluate the AST code if any is provided" do code = stub 'code' @type.stubs(:code).returns code @type.stubs(:subscope).returns stub_everything("subscope", :compiler => @compiler) code.expects(:safeevaluate).with @type.subscope @type.evaluate_code(@resource) end describe "and ruby code is provided" do it "should create a DSL Resource API and evaluate it" do @type.stubs(:ruby_code).returns(proc { "foo" }) @api = stub 'api' Puppet::DSL::ResourceAPI.expects(:new).with { |res, scope, code| code == @type.ruby_code }.returns @api @api.expects(:evaluate) @type.evaluate_code(@resource) end end it "should noop if there is no code" do @type.expects(:code).returns nil @type.evaluate_code(@resource) end describe "and it has a parent class" do before do @parent_type = Puppet::Resource::Type.new(:hostclass, "parent") @type.parent = "parent" @parent_resource = Puppet::Parser::Resource.new(:class, "parent", :scope => @scope) @compiler.add_resource @scope, @parent_resource @type.resource_type_collection = @scope.known_resource_types @type.resource_type_collection.add @parent_type end it "should evaluate the parent's resource" do @type.parent_type(@scope) @type.evaluate_code(@resource) @scope.class_scope(@parent_type).should_not be_nil end it "should not evaluate the parent's resource if it has already been evaluated" do @parent_resource.evaluate @type.parent_type(@scope) @parent_resource.expects(:evaluate).never @type.evaluate_code(@resource) end it "should use the parent's scope as its base scope" do @type.parent_type(@scope) @type.evaluate_code(@resource) @scope.class_scope(@type).parent.object_id.should == @scope.class_scope(@parent_type).object_id end end describe "and it has a parent node" do before do @type = Puppet::Resource::Type.new(:node, "foo") @parent_type = Puppet::Resource::Type.new(:node, "parent") @type.parent = "parent" @parent_resource = Puppet::Parser::Resource.new(:node, "parent", :scope => @scope) @compiler.add_resource @scope, @parent_resource @type.resource_type_collection = @scope.known_resource_types - @type.resource_type_collection.stubs(:node).with("parent").returns(@parent_type) - @type.resource_type_collection.stubs(:node).with("Parent").returns(@parent_type) + @type.resource_type_collection.add(@parent_type) end it "should evaluate the parent's resource" do @type.parent_type(@scope) @type.evaluate_code(@resource) @scope.class_scope(@parent_type).should_not be_nil end it "should not evaluate the parent's resource if it has already been evaluated" do @parent_resource.evaluate @type.parent_type(@scope) @parent_resource.expects(:evaluate).never @type.evaluate_code(@resource) end it "should use the parent's scope as its base scope" do @type.parent_type(@scope) @type.evaluate_code(@resource) @scope.class_scope(@type).parent.object_id.should == @scope.class_scope(@parent_type).object_id end end end describe "when creating a resource" do before do @node = Puppet::Node.new("foo", :environment => 'env') @compiler = Puppet::Parser::Compiler.new(@node) @scope = Puppet::Parser::Scope.new(:compiler => @compiler) @top = Puppet::Resource::Type.new :hostclass, "top" @middle = Puppet::Resource::Type.new :hostclass, "middle", :parent => "top" @code = Puppet::Resource::TypeCollection.new("env") @code.add @top @code.add @middle @node.environment.stubs(:known_resource_types).returns(@code) end it "should create a resource instance" do @top.mk_plain_resource(@scope).should be_instance_of(Puppet::Parser::Resource) end it "should set its resource type to 'class' when it is a hostclass" do Puppet::Resource::Type.new(:hostclass, "top").mk_plain_resource(@scope).type.should == "Class" end it "should set its resource type to 'node' when it is a node" do Puppet::Resource::Type.new(:node, "top").mk_plain_resource(@scope).type.should == "Node" end it "should fail when it is a definition" do lambda { Puppet::Resource::Type.new(:definition, "top").mk_plain_resource(@scope) }.should raise_error(ArgumentError) end it "should add the created resource to the scope's catalog" do @top.mk_plain_resource(@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.mk_plain_resource(@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 = Puppet::Resource::Type.new :hostclass, "something", :parent => "yay" @code.add othertop lambda { othertop.mk_plain_resource(@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.mk_plain_resource(@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.mk_plain_resource(@scope).should == "something" end it "should not create a new parent resource if one already exists and it has a parent class" do @top.mk_plain_resource(@scope) top_resource = @compiler.catalog.resource(:class, "top") @middle.mk_plain_resource(@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.mk_plain_resource(@scope) @compiler.catalog.should be_tagged("middle") end it "should tag the catalog with the parent class tags when it is evaluated" do @middle.mk_plain_resource(@scope) @compiler.catalog.should be_tagged("top") end end describe "when merging code from another instance" do def code(str) Puppet::Parser::AST::Leaf.new :value => str end it "should fail unless it is a class" do lambda { Puppet::Resource::Type.new(:node, "bar").merge("foo") }.should raise_error(Puppet::Error) end it "should fail unless the source instance is a class" do dest = Puppet::Resource::Type.new(:hostclass, "bar") source = Puppet::Resource::Type.new(:node, "foo") lambda { dest.merge(source) }.should raise_error(Puppet::Error) end it "should fail if both classes have different parent classes" do code = Puppet::Resource::TypeCollection.new("env") {"a" => "b", "c" => "d"}.each do |parent, child| code.add Puppet::Resource::Type.new(:hostclass, parent) code.add Puppet::Resource::Type.new(:hostclass, child, :parent => parent) end lambda { code.hostclass("b").merge(code.hostclass("d")) }.should raise_error(Puppet::Error) end it "should fail if it's named 'main' and 'freeze_main' is enabled" do Puppet.settings[:freeze_main] = true code = Puppet::Resource::TypeCollection.new("env") code.add Puppet::Resource::Type.new(:hostclass, "") other = Puppet::Resource::Type.new(:hostclass, "") lambda { code.hostclass("").merge(other) }.should raise_error(Puppet::Error) end it "should copy the other class's parent if it has not parent" do dest = Puppet::Resource::Type.new(:hostclass, "bar") parent = Puppet::Resource::Type.new(:hostclass, "parent") source = Puppet::Resource::Type.new(:hostclass, "foo", :parent => "parent") dest.merge(source) dest.parent.should == "parent" end it "should copy the other class's documentation as its docs if it has no docs" do dest = Puppet::Resource::Type.new(:hostclass, "bar") source = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "yayness") dest.merge(source) dest.doc.should == "yayness" end it "should append the other class's docs to its docs if it has any" do dest = Puppet::Resource::Type.new(:hostclass, "bar", :doc => "fooness") source = Puppet::Resource::Type.new(:hostclass, "foo", :doc => "yayness") dest.merge(source) dest.doc.should == "foonessyayness" end it "should turn its code into an ASTArray if necessary" do dest = Puppet::Resource::Type.new(:hostclass, "bar", :code => code("foo")) source = Puppet::Resource::Type.new(:hostclass, "foo", :code => code("bar")) dest.merge(source) dest.code.should be_instance_of(Puppet::Parser::AST::ASTArray) end it "should set the other class's code as its code if it has none" do dest = Puppet::Resource::Type.new(:hostclass, "bar") source = Puppet::Resource::Type.new(:hostclass, "foo", :code => code("bar")) dest.merge(source) dest.code.value.should == "bar" end it "should append the other class's code to its code if it has any" do dcode = Puppet::Parser::AST::ASTArray.new :children => [code("dest")] dest = Puppet::Resource::Type.new(:hostclass, "bar", :code => dcode) scode = Puppet::Parser::AST::ASTArray.new :children => [code("source")] source = Puppet::Resource::Type.new(:hostclass, "foo", :code => scode) dest.merge(source) dest.code.children.collect { |l| l.value }.should == %w{dest source} end end end