diff --git a/lib/puppet/node/environment.rb b/lib/puppet/node/environment.rb index b64fb8a1f..807479bc8 100644 --- a/lib/puppet/node/environment.rb +++ b/lib/puppet/node/environment.rb @@ -1,151 +1,151 @@ require 'puppet/util/cacher' require 'monitor' # Just define it, so this class has fewer load dependencies. class Puppet::Node end # Model the environment that a node can operate in. This class just # provides a simple wrapper for the functionality around environments. class Puppet::Node::Environment module Helper def environment Puppet::Node::Environment.new(@environment) end def environment=(env) if env.is_a?(String) or env.is_a?(Symbol) @environment = env else @environment = env.name end end end include Puppet::Util::Cacher @seen = {} # Return an existing environment instance, or create a new one. def self.new(name = nil) return name if name.is_a?(self) name ||= Puppet.settings.value(:environment) raise ArgumentError, "Environment name must be specified" unless name symbol = name.to_sym return @seen[symbol] if @seen[symbol] obj = self.allocate obj.send :initialize, symbol @seen[symbol] = obj end def self.current Thread.current[:environment] || root end def self.current=(env) Thread.current[:environment] = new(env) end def self.root @root end # This is only used for testing. def self.clear @seen.clear end attr_reader :name # Return an environment-specific setting. def [](param) Puppet.settings.value(param, self.name) end def initialize(name) @name = name extend MonitorMixin end def known_resource_types # This makes use of short circuit evaluation to get the right thread-safe # per environment semantics with an efficient most common cases; we almost # always just return our thread's known-resource types. Only at the start # of a compilation (after our thread var has been set to nil) or when the # environment has changed do we delve deeper. Thread.current[:known_resource_types] = nil if (krt = Thread.current[:known_resource_types]) && krt.environment != self Thread.current[:known_resource_types] ||= synchronize { - if @known_resource_types.nil? or @known_resource_types.stale? + if @known_resource_types.nil? or @known_resource_types.require_reparse? @known_resource_types = Puppet::Resource::TypeCollection.new(self) @known_resource_types.perform_initial_import end @known_resource_types } end def module(name) mod = Puppet::Module.new(name, self) return nil unless mod.exist? mod end # Cache the modulepath, so that we aren't searching through # all known directories all the time. cached_attr(:modulepath, :ttl => Puppet[:filetimeout]) do dirs = self[:modulepath].split(File::PATH_SEPARATOR) dirs = ENV["PUPPETLIB"].split(File::PATH_SEPARATOR) + dirs if ENV["PUPPETLIB"] validate_dirs(dirs) end # Return all modules from this environment. # Cache the list, because it can be expensive to create. cached_attr(:modules, :ttl => Puppet[:filetimeout]) do module_names = modulepath.collect { |path| Dir.entries(path) }.flatten.uniq module_names.collect do |path| begin Puppet::Module.new(path, self) rescue Puppet::Module::Error => e nil end end.compact end # Cache the manifestdir, so that we aren't searching through # all known directories all the time. cached_attr(:manifestdir, :ttl => Puppet[:filetimeout]) do validate_dirs(self[:manifestdir].split(File::PATH_SEPARATOR)) end def to_s name.to_s end def to_sym to_s.to_sym end # The only thing we care about when serializing an environment is its # identity; everything else is ephemeral and should not be stored or # transmitted. def to_zaml(z) self.to_s.to_zaml(z) end def validate_dirs(dirs) dirs.collect do |dir| if dir !~ /^#{File::SEPARATOR}/ File.join(Dir.getwd, dir) else dir end end.find_all do |p| p =~ /^#{File::SEPARATOR}/ && FileTest.directory?(p) end end @root = new(:'*root*') end diff --git a/lib/puppet/resource/type_collection.rb b/lib/puppet/resource/type_collection.rb index 347e1c0e0..4210475ad 100644 --- a/lib/puppet/resource/type_collection.rb +++ b/lib/puppet/resource/type_collection.rb @@ -1,216 +1,222 @@ 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 inspect "TypeCollection" + { :hostclasses => @hostclasses.keys, :definitions => @definitions.keys, :nodes => @nodes.keys }.inspect 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) 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 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) parser.file = file end parser.parse rescue => detail + @parse_failed = true + msg = "Could not parse for environment #{environment}: #{detail}" error = Puppet::Error.new(msg) error.set_backtrace(detail.backtrace) raise error end + def require_reparse? + @parse_failed || stale? + 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("::")) 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/node/environment_spec.rb b/spec/unit/node/environment_spec.rb index 6edcce56c..6109007b9 100755 --- a/spec/unit/node/environment_spec.rb +++ b/spec/unit/node/environment_spec.rb @@ -1,277 +1,278 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/node/environment' require 'puppet/util/execution' describe Puppet::Node::Environment do after do Puppet::Node::Environment.clear end it "should include the Cacher module" do Puppet::Node::Environment.ancestors.should be_include(Puppet::Util::Cacher) end it "should use the filetimeout for the ttl for the modulepath" do Puppet::Node::Environment.attr_ttl(:modulepath).should == Integer(Puppet[:filetimeout]) end it "should use the filetimeout for the ttl for the module list" do Puppet::Node::Environment.attr_ttl(:modules).should == Integer(Puppet[:filetimeout]) end it "should use the filetimeout for the ttl for the manifestdir" do Puppet::Node::Environment.attr_ttl(:manifestdir).should == Integer(Puppet[:filetimeout]) end it "should use the default environment if no name is provided while initializing an environment" do Puppet.settings.expects(:value).with(:environment).returns("one") Puppet::Node::Environment.new.name.should == :one end it "should treat environment instances as singletons" do Puppet::Node::Environment.new("one").should equal(Puppet::Node::Environment.new("one")) end it "should treat an environment specified as names or strings as equivalent" do Puppet::Node::Environment.new(:one).should equal(Puppet::Node::Environment.new("one")) end it "should return its name when converted to a string" do Puppet::Node::Environment.new(:one).to_s.should == "one" end it "should just return any provided environment if an environment is provided as the name" do one = Puppet::Node::Environment.new(:one) Puppet::Node::Environment.new(one).should equal(one) end describe "when managing known resource types" do before do @env = Puppet::Node::Environment.new("dev") @collection = Puppet::Resource::TypeCollection.new(@env) @collection.stubs(:perform_initial_import) Thread.current[:known_resource_types] = nil end it "should create a resource type collection if none exists" do Puppet::Resource::TypeCollection.expects(:new).with(@env).returns @collection @env.known_resource_types.should equal(@collection) end it "should reuse any existing resource type collection" do @env.known_resource_types.should equal(@env.known_resource_types) end it "should perform the initial import when creating a new collection" do @collection.expects(:perform_initial_import) Puppet::Resource::TypeCollection.expects(:new).returns @collection @env.known_resource_types end it "should return the same collection even if stale if it's the same thread" do Puppet::Resource::TypeCollection.stubs(:new).returns @collection @env.known_resource_types.stubs(:stale?).returns true @env.known_resource_types.should equal(@collection) end it "should return the current thread associated collection if there is one" do Thread.current[:known_resource_types] = @collection @env.known_resource_types.should equal(@collection) end - it "should give to all threads the same collection if it didn't change" do - Puppet::Resource::TypeCollection.expects(:new).with(@env).returns @collection - @env.known_resource_types + it "should give to all threads using the same environment the same collection if the collection isn't stale" do + original_thread_type_collection = Puppet::Resource::TypeCollection.new(@env) + Puppet::Resource::TypeCollection.expects(:new).with(@env).returns original_thread_type_collection + @env.known_resource_types.should equal(original_thread_type_collection) + + original_thread_type_collection.expects(:require_reparse?).returns(false) + Puppet::Resource::TypeCollection.stubs(:new).with(@env).returns @collection t = Thread.new { - @env.known_resource_types.should equal(@collection) + @env.known_resource_types.should equal(original_thread_type_collection) } t.join end - it "should give to new threads a new collection if it isn't stale" do - Puppet::Resource::TypeCollection.expects(:new).with(@env).returns @collection - @env.known_resource_types.expects(:stale?).returns(true) - - Puppet::Resource::TypeCollection.expects(:new).returns @collection + it "should generate a new TypeCollection if the current one requires reparsing" do + old_type_collection = @env.known_resource_types + old_type_collection.stubs(:require_reparse?).returns true + Thread.current[:known_resource_types] = nil + new_type_collection = @env.known_resource_types - t = Thread.new { - @env.known_resource_types.should equal(@collection) - } - t.join + new_type_collection.should be_a Puppet::Resource::TypeCollection + new_type_collection.should_not equal(old_type_collection) end - end [:modulepath, :manifestdir].each do |setting| it "should validate the #{setting} directories" do path = %w{/one /two}.join(File::PATH_SEPARATOR) env = Puppet::Node::Environment.new("testing") env.stubs(:[]).with(setting).returns path env.expects(:validate_dirs).with(%w{/one /two}) env.send(setting) end it "should return the validated dirs for #{setting}" do path = %w{/one /two}.join(File::PATH_SEPARATOR) env = Puppet::Node::Environment.new("testing") env.stubs(:[]).with(setting).returns path env.stubs(:validate_dirs).returns %w{/one /two} env.send(setting).should == %w{/one /two} end end it "should prefix the value of the 'PUPPETLIB' environment variable to the module path if present" do Puppet::Util::Execution.withenv("PUPPETLIB" => %w{/l1 /l2}.join(File::PATH_SEPARATOR)) do env = Puppet::Node::Environment.new("testing") module_path = %w{/one /two}.join(File::PATH_SEPARATOR) env.expects(:validate_dirs).with(%w{/l1 /l2 /one /two}).returns %w{/l1 /l2 /one /two} env.expects(:[]).with(:modulepath).returns module_path env.modulepath.should == %w{/l1 /l2 /one /two} end end describe "when validating modulepath or manifestdir directories" do it "should not return non-directories" do env = Puppet::Node::Environment.new("testing") FileTest.expects(:directory?).with("/one").returns true FileTest.expects(:directory?).with("/two").returns false env.validate_dirs(%w{/one /two}).should == %w{/one} end it "should use the current working directory to fully-qualify unqualified paths" do FileTest.stubs(:directory?).returns true env = Puppet::Node::Environment.new("testing") two = File.join(Dir.getwd, "two") env.validate_dirs(%w{/one two}).should == ["/one", two] end end describe "when modeling a specific environment" do it "should have a method for returning the environment name" do Puppet::Node::Environment.new("testing").name.should == :testing end it "should provide an array-like accessor method for returning any environment-specific setting" do env = Puppet::Node::Environment.new("testing") env.should respond_to(:[]) end it "should ask the Puppet settings instance for the setting qualified with the environment name" do Puppet.settings.expects(:value).with("myvar", :testing).returns("myval") env = Puppet::Node::Environment.new("testing") env["myvar"].should == "myval" end it "should be able to return an individual module that exists in its module path" do env = Puppet::Node::Environment.new("testing") mod = mock 'module' Puppet::Module.expects(:new).with("one", env).returns mod mod.expects(:exist?).returns true env.module("one").should equal(mod) end it "should return nil if asked for a module that does not exist in its path" do env = Puppet::Node::Environment.new("testing") mod = mock 'module' Puppet::Module.expects(:new).with("one", env).returns mod mod.expects(:exist?).returns false env.module("one").should be_nil end it "should be able to return its modules" do Puppet::Node::Environment.new("testing").should respond_to(:modules) end describe ".modules" do it "should return a module named for every directory in each module path" do env = Puppet::Node::Environment.new("testing") env.expects(:modulepath).at_least_once.returns %w{/a /b} Dir.expects(:entries).with("/a").returns %w{foo bar} Dir.expects(:entries).with("/b").returns %w{bee baz} env.modules.collect{|mod| mod.name}.sort.should == %w{foo bar bee baz}.sort end it "should remove duplicates" do env = Puppet::Node::Environment.new("testing") env.expects(:modulepath).returns( %w{/a /b} ).at_least_once Dir.expects(:entries).with("/a").returns %w{foo} Dir.expects(:entries).with("/b").returns %w{foo} env.modules.collect{|mod| mod.name}.sort.should == %w{foo} end it "should ignore invalid modules" do env = Puppet::Node::Environment.new("testing") env.stubs(:modulepath).returns %w{/a} Dir.expects(:entries).with("/a").returns %w{foo bar} Puppet::Module.expects(:new).with { |name, env| name == "foo" }.returns mock("foomod", :name => "foo") Puppet::Module.expects(:new).with { |name, env| name == "bar" }.raises( Puppet::Module::InvalidName, "name is invalid" ) env.modules.collect{|mod| mod.name}.sort.should == %w{foo} end it "should create modules with the correct environment" do env = Puppet::Node::Environment.new("testing") env.expects(:modulepath).at_least_once.returns %w{/a} Dir.expects(:entries).with("/a").returns %w{foo} env.modules.each {|mod| mod.environment.should == env } end it "should cache the module list" do env = Puppet::Node::Environment.new("testing") env.expects(:modulepath).at_least_once.returns %w{/a} Dir.expects(:entries).once.with("/a").returns %w{foo} env.modules env.modules end end end describe Puppet::Node::Environment::Helper do before do @helper = Object.new @helper.extend(Puppet::Node::Environment::Helper) end it "should be able to set and retrieve the environment" do @helper.environment = :foo @helper.environment.name.should == :foo end it "should accept an environment directly" do env = Puppet::Node::Environment.new :foo @helper.environment = env @helper.environment.name.should == :foo end it "should accept an environment as a string" do env = Puppet::Node::Environment.new "foo" @helper.environment = env @helper.environment.name.should == :foo end end end diff --git a/spec/unit/resource/type_collection_spec.rb b/spec/unit/resource/type_collection_spec.rb index cf7039a51..1576c0615 100644 --- a/spec/unit/resource/type_collection_spec.rb +++ b/spec/unit/resource/type_collection_spec.rb @@ -1,456 +1,463 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/resource/type_collection' require 'puppet/resource/type' describe Puppet::Resource::TypeCollection do include PuppetSpec::Files 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 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) 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) 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) 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" 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 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) 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 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) 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) 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) 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) 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) 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) 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) 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 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' end it "should only look in the topclass, if the name is qualified" do @loader.find("foo", "::bar", :hostclass).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 end end it "should use the generic 'find' method with an empty namespace to find nodes" do loader = Puppet::Resource::TypeCollection.new("env") loader.expects(:find).with("", "bar", :node) loader.find_node(stub("ignored"), "bar") 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' + @parser = Puppet::Parser::Parser.new("test") Puppet::Parser::Parser.stubs(:new).returns @parser @code = Puppet::Resource::TypeCollection.new("env") 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 filename = tmpfile('myfile') File.open(filename, 'w'){|f| } Puppet.settings[:manifest] = filename @parser.expects(:file=).with filename @parser.expects(:parse) @code.perform_initial_import end it "should pass the manifest file to the parser even if it does not exist on disk" do filename = tmpfile('myfile') Puppet.settings[:code] = "" Puppet.settings[:manifest] = filename @parser.expects(:file=).with(filename).once @parser.expects(:parse).once @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 @parser.stubs(:file=) lambda { @code.perform_initial_import }.should raise_error(Puppet::Error) end + + it "should mark the type collection as needing a reparse when there is an error parsing" do + @parser.expects(:parse).raises Puppet::ParseError.new("Syntax error at ...") + + lambda { @code.perform_initial_import }.should raise_error(Puppet::Error, /Syntax error at .../) + @code.require_reparse?.should be_true + 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