diff --git a/lib/puppet/application/faces.rb b/lib/puppet/application/faces.rb index e7fce66b1..3145da821 100644 --- a/lib/puppet/application/faces.rb +++ b/lib/puppet/application/faces.rb @@ -1,126 +1,122 @@ require 'puppet/application' require 'puppet/face' class Puppet::Application::Faces < Puppet::Application should_parse_config run_mode :agent option("--debug", "-d") do |arg| Puppet::Util::Log.level = :debug end option("--verbose", "-v") do Puppet::Util::Log.level = :info end def help <<-HELP puppet-faces(8) -- List available Faces and actions ======== SYNOPSIS -------- Lists the available subcommands (with applicable terminuses and/or actions) provided by the Puppet Faces API. This information is automatically read from the Puppet code present on the system. By default, the output includes all terminuses and actions. USAGE ----- puppet faces [-d|--debug] [-v|--verbose] [actions|terminuses] OPTIONS ------- Note that any configuration option valid in the configuration file is also a valid long argument. See the configuration file documentation at http://docs.puppetlabs.com/references/stable/configuration.html for the full list of acceptable parameters. A commented list of all configuration options can also be generated by running puppet agent with '--genconfig'. * --verbose: Sets the log level to "info." This option has no tangible effect at the time of this writing. * --debug: Sets the log level to "debug." This option has no tangible effect at the time of this writing. AUTHOR ------ Puppet Labs COPYRIGHT --------- Copyright (c) 2011 Puppet Labs, LLC Licensed under the Apache 2.0 License HELP end def list(*arguments) if arguments.empty? arguments = %w{terminuses actions} end faces.each do |name| str = "#{name}:\n" if arguments.include?("terminuses") begin - terms = terminus_classes(name.to_sym) + terms = Puppet::Indirector::Face.terminus_classes(name.to_sym) str << "\tTerminuses: #{terms.join(", ")}\n" rescue => detail puts detail.backtrace if Puppet[:trace] $stderr.puts "Could not load terminuses for #{name}: #{detail}" end end if arguments.include?("actions") begin actions = actions(name.to_sym) str << "\tActions: #{actions.join(", ")}\n" rescue => detail puts detail.backtrace if Puppet[:trace] $stderr.puts "Could not load actions for #{name}: #{detail}" end end print str end end attr_accessor :name, :arguments def main list(*arguments) end def setup Puppet::Util::Log.newdestination :console load_applications # Call this to load all of the apps @arguments = command_line.args @arguments ||= [] end def faces Puppet::Face.faces end - def terminus_classes(indirection) - Puppet::Indirector::Terminus.terminus_classes(indirection).collect { |t| t.to_s }.sort - end - def actions(indirection) return [] unless face = Puppet::Face[indirection, '0.0.1'] face.load_actions return face.actions.sort { |a, b| a.to_s <=> b.to_s } end def load_applications command_line.available_subcommands.each do |app| command_line.require_application app end end end diff --git a/lib/puppet/indirector/terminus.rb b/lib/puppet/indirector/terminus.rb index 4ebd0d004..d488869d1 100644 --- a/lib/puppet/indirector/terminus.rb +++ b/lib/puppet/indirector/terminus.rb @@ -1,148 +1,145 @@ require 'puppet/indirector' require 'puppet/indirector/indirection' require 'puppet/util/instance_loader' # A simple class that can function as the base class for indirected types. class Puppet::Indirector::Terminus require 'puppet/util/docs' extend Puppet::Util::Docs class << self include Puppet::Util::InstanceLoader attr_accessor :name, :terminus_type attr_reader :abstract_terminus, :indirection # Are we an abstract terminus type, rather than an instance with an # associated indirection? def abstract_terminus? abstract_terminus end # Convert a constant to a short name. def const2name(const) const.sub(/^[A-Z]/) { |i| i.downcase }.gsub(/[A-Z]/) { |i| "_#{i.downcase}" }.intern end # Look up the indirection if we were only provided a name. def indirection=(name) if name.is_a?(Puppet::Indirector::Indirection) @indirection = name elsif ind = Puppet::Indirector::Indirection.instance(name) @indirection = ind else raise ArgumentError, "Could not find indirection instance #{name} for #{self.name}" end end def indirection_name @indirection.name end # Register our subclass with the appropriate indirection. # This follows the convention that our terminus is named after the # indirection. def inherited(subclass) longname = subclass.to_s if longname =~ /# 0 indirection_name = names.pop.sub(/^[A-Z]/) { |i| i.downcase }.gsub(/[A-Z]/) { |i| "_#{i.downcase}" }.intern if indirection_name == "" or indirection_name.nil? raise Puppet::DevError, "Could not discern indirection model from class constant" end # This will throw an exception if the indirection instance cannot be found. # Do this last, because it also registers the terminus type with the indirection, # which needs the above information. subclass.indirection = indirection_name # And add this instance to the instance hash. Puppet::Indirector::Terminus.register_terminus_class(subclass) end # Mark that this instance is abstract. def mark_as_abstract_terminus @abstract_terminus = true end def model indirection.model end # Convert a short name to a constant. def name2const(name) name.to_s.capitalize.sub(/_(.)/) { |i| $1.upcase } end # Register a class, probably autoloaded. def register_terminus_class(klass) setup_instance_loading klass.indirection_name instance_hash(klass.indirection_name)[klass.name] = klass end # Return a terminus by name, using the autoloader. def terminus_class(indirection_name, terminus_type) setup_instance_loading indirection_name loaded_instance(indirection_name, terminus_type) end # Return all terminus classes for a given indirection. def terminus_classes(indirection_name) setup_instance_loading indirection_name - - # Load them all. - instance_loader(indirection_name).loadall - - # And return the list of names. - loaded_instances(indirection_name) + instance_loader(indirection_name).files_to_load.map do |file| + File.basename(file).chomp(".rb").intern + end end private def setup_instance_loading(type) instance_load type, "puppet/indirector/#{type}" unless instance_loading?(type) end end def indirection self.class.indirection end def initialize raise Puppet::DevError, "Cannot create instances of abstract terminus types" if self.class.abstract_terminus? end def model self.class.model end def name self.class.name end def terminus_type self.class.terminus_type end end diff --git a/lib/puppet/interface/documentation.rb b/lib/puppet/interface/documentation.rb index aedcc1c24..fcaec2568 100644 --- a/lib/puppet/interface/documentation.rb +++ b/lib/puppet/interface/documentation.rb @@ -1,197 +1,199 @@ # This isn't usable outside Puppet::Interface; don't load it alone. class Puppet::Interface module DocGen def self.strip_whitespace(text) text.gsub!(/[ \t\f]+$/, '') # We need to identify an indent: the minimum number of whitespace # characters at the start of any line in the text. # # Using split rather than each_line is because the later only takes a # block on Ruby 1.8.5 / Centos, and we support that. --daniel 2011-05-03 indent = text.split(/\n/).map {|x| x.index(/[^\s]/) }.compact.min if indent > 0 then text.gsub!(/^[ \t\f]{0,#{indent}}/, '') end return text end # The documentation attributes all have some common behaviours; previously # we open-coded them across the set of six things, but that seemed # wasteful - especially given that they were literally the same, and had # the same bug hidden in them. # # This feels a bit like overkill, but at least the common code is common # now. --daniel 2011-04-29 def attr_doc(name, &validate) # Now, which form of the setter do we want, validated or not? get_arg = "value.to_s" if validate define_method(:"_validate_#{name}", validate) get_arg = "_validate_#{name}(#{get_arg})" end # We use module_eval, which I don't like much, because we can't have an # argument to a block with a default value in Ruby 1.8, and I don't like # the side-effects (eg: no argument count validation) of using blocks # without as metheds. When we are 1.9 only (hah!) you can totally # replace this with some up-and-up define_method. --daniel 2011-04-29 module_eval(<<-EOT, __FILE__, __LINE__ + 1) def #{name}(value = nil) self.#{name} = value unless value.nil? @#{name} end def #{name}=(value) @#{name} = Puppet::Interface::DocGen.strip_whitespace(#{get_arg}) end EOT end end module TinyDocs extend Puppet::Interface::DocGen attr_doc :summary do |value| value =~ /\n/ and raise ArgumentError, "Face summary should be a single line; put the long text in 'description' instead." value end attr_doc :description end module FullDocs extend Puppet::Interface::DocGen include TinyDocs attr_doc :examples attr_doc :notes attr_doc :license attr_doc :short_description def short_description(value = nil) self.short_description = value unless value.nil? if @short_description.nil? then return nil if @description.nil? lines = @description.split("\n") - grab = [5, lines.index('') || 5].min - @short_description = lines[0, grab].join("\n") + ' [...]' + first_paragraph_break = lines.index('') || 5 + grab = [5, first_paragraph_break].min + @short_description = lines[0, grab].join("\n") + @short_description += ' [...]' if (grab < lines.length and first_paragraph_break >= 5) end @short_description end def author(value = nil) unless value.nil? then unless value.is_a? String raise ArgumentError, 'author must be a string; use multiple statements for multiple authors' end if value =~ /\n/ then raise ArgumentError, 'author should be a single line; use multiple statements for multiple authors' end @authors.push(Puppet::Interface::DocGen.strip_whitespace(value)) end @authors.empty? ? nil : @authors.join("\n") end def authors @authors end def author=(value) if Array(value).any? {|x| x =~ /\n/ } then raise ArgumentError, 'author should be a single line; use multiple statements' end @authors = Array(value).map{|x| Puppet::Interface::DocGen.strip_whitespace(x) } end alias :authors= :author= def copyright(owner = nil, years = nil) if years.nil? and not owner.nil? then raise ArgumentError, 'copyright takes the owners names, then the years covered' end self.copyright_owner = owner unless owner.nil? self.copyright_years = years unless years.nil? if self.copyright_years or self.copyright_owner then "Copyright #{self.copyright_years} by #{self.copyright_owner}" else "Unknown copyright owner and years." end end attr_accessor :copyright_owner def copyright_owner=(value) case value when String then @copyright_owner = value when Array then @copyright_owner = value.join(", ") else raise ArgumentError, "copyright owner must be a string or an array of strings" end @copyright_owner end attr_accessor :copyright_years def copyright_years=(value) years = munge_copyright_year value years = (years.is_a?(Array) ? years : [years]). sort_by do |x| x.is_a?(Range) ? x.first : x end @copyright_years = years.map do |year| if year.is_a? Range then "#{year.first}-#{year.last}" else year end end.join(", ") end def munge_copyright_year(input) case input when Range then input when Integer then if input < 1970 then fault = "before 1970" elsif input > (future = Time.now.year + 2) then fault = "after #{future}" end if fault then raise ArgumentError, "copyright with a year #{fault} is very strange; did you accidentally add or subtract two years?" end input when String then input.strip.split(/,/).map do |part| part = part.strip if part =~ /^\d+$/ then part.to_i elsif found = part.split(/-/) then unless found.length == 2 and found.all? {|x| x.strip =~ /^\d+$/ } raise ArgumentError, "#{part.inspect} is not a good copyright year or range" end Range.new(found[0].to_i, found[1].to_i) else raise ArgumentError, "#{part.inspect} is not a good copyright year or range" end end when Array then result = [] input.each do |item| item = munge_copyright_year item if item.is_a? Array result.concat item else result << item end end result else raise ArgumentError, "#{input.inspect} is not a good copyright year, set, or range" end end end end diff --git a/lib/puppet/util/autoload.rb b/lib/puppet/util/autoload.rb index f0dd0a5c5..6537a4a4e 100644 --- a/lib/puppet/util/autoload.rb +++ b/lib/puppet/util/autoload.rb @@ -1,156 +1,158 @@ require 'puppet/util/warnings' require 'puppet/util/cacher' # Autoload paths, either based on names or all at once. class Puppet::Util::Autoload require 'puppet/util/autoload/file_cache' include Puppet::Util include Puppet::Util::Warnings include Puppet::Util::Cacher include Puppet::Util::Autoload::FileCache @autoloaders = {} @loaded = [] class << self attr_reader :autoloaders private :autoloaders end # Send [], []=, and :clear to the @autloaders hash Puppet::Util.classproxy self, :autoloaders, "[]", "[]=" # List all loaded files. def self.list_loaded @loaded.sort { |a,b| a[0] <=> b[0] }.collect do |path, hash| "#{path}: #{hash[:file]}" end end # Has a given path been loaded? This is used for testing whether a # changed file should be loaded or just ignored. This is only # used in network/client/master, when downloading plugins, to # see if a given plugin is currently loaded and thus should be # reloaded. def self.loaded?(path) path = path.to_s.sub(/\.rb$/, '') @loaded.include?(path) end # Save the fact that a given path has been loaded. This is so # we can load downloaded plugins if they've already been loaded # into memory. def self.loaded(file) $" << file + ".rb" unless $".include?(file) @loaded << file unless @loaded.include?(file) end attr_accessor :object, :path, :objwarn, :wrap def initialize(obj, path, options = {}) @path = path.to_s raise ArgumentError, "Autoload paths cannot be fully qualified" if @path !~ /^\w/ @object = obj self.class[obj] = self options.each do |opt, value| opt = opt.intern if opt.is_a? String begin self.send(opt.to_s + "=", value) rescue NoMethodError raise ArgumentError, "#{opt} is not a valid option" end end @wrap = true unless defined?(@wrap) end # Load a single plugin by name. We use 'load' here so we can reload a # given plugin. def load(name,env=nil) path = name.to_s + ".rb" searchpath(env).each do |dir| file = File.join(dir, path) next unless file_exist?(file) begin Kernel.load file, @wrap name = symbolize(name) loaded name, file return true rescue SystemExit,NoMemoryError raise rescue Exception => detail puts detail.backtrace if Puppet[:trace] raise Puppet::Error, "Could not autoload #{name}: #{detail}" end end false end # Mark the named object as loaded. Note that this supports unqualified # queries, while we store the result as a qualified query in the class. def loaded(name, file) self.class.loaded(File.join(@path, name.to_s)) end # Indicate whether the specfied plugin has been loaded. def loaded?(name) self.class.loaded?(File.join(@path, name.to_s)) end # Load all instances that we can. This uses require, rather than load, # so that already-loaded files don't get reloaded unnecessarily. def loadall # Load every instance of everything we can find. - searchpath.each do |dir| - Dir.glob("#{dir}/*.rb").each do |file| - name = File.basename(file).sub(".rb", '').intern - next if loaded?(name) - begin - Kernel.require file - loaded(name, file) - rescue SystemExit,NoMemoryError - raise - rescue Exception => detail - puts detail.backtrace if Puppet[:trace] - raise Puppet::Error, "Could not autoload #{file}: #{detail}" - end + files_to_load.each do |file| + name = File.basename(file).chomp(".rb").intern + next if loaded?(name) + begin + Kernel.require file + loaded(name, file) + rescue SystemExit,NoMemoryError + raise + rescue Exception => detail + puts detail.backtrace if Puppet[:trace] + raise Puppet::Error, "Could not autoload #{file}: #{detail}" end end end + def files_to_load + searchpath.map { |dir| Dir.glob("#{dir}/*.rb") }.flatten + end + # The list of directories to search through for loadable plugins. def searchpath(env=nil) - search_directories(env).collect { |d| File.join(d, @path) }.find_all { |d| FileTest.directory?(d) } + search_directories(env).uniq.collect { |d| File.join(d, @path) }.find_all { |d| FileTest.directory?(d) } end def module_directories(env=nil) # We have to require this late in the process because otherwise we might have # load order issues. require 'puppet/node/environment' real_env = Puppet::Node::Environment.new(env) # We're using a per-thread cache of said module directories, so that # we don't scan the filesystem each time we try to load something with # this autoload instance. But since we don't want to cache for the eternity # this env_module_directories gets reset after the compilation on the master. # This is also reset after an agent ran. # One of the side effect of this change is that this module directories list will be # shared among all autoload that we have running at a time. But that won't be an issue # as by definition those directories are shared by all autoload. Thread.current[:env_module_directories] ||= {} Thread.current[:env_module_directories][real_env] ||= real_env.modulepath.collect do |dir| Dir.entries(dir).reject { |f| f =~ /^\./ }.collect { |f| File.join(dir, f) } end.flatten.collect { |d| [File.join(d, "plugins"), File.join(d, "lib")] }.flatten.find_all do |d| FileTest.directory?(d) end end def search_directories(env=nil) [module_directories(env), Puppet[:libdir].split(File::PATH_SEPARATOR), $LOAD_PATH].flatten end end diff --git a/spec/shared_behaviours/documentation_on_faces.rb b/spec/shared_behaviours/documentation_on_faces.rb index 3cfb178f7..204b173a5 100644 --- a/spec/shared_behaviours/documentation_on_faces.rb +++ b/spec/shared_behaviours/documentation_on_faces.rb @@ -1,256 +1,263 @@ # encoding: UTF-8 shared_examples_for "documentation on faces" do defined?(Attrs) or Attrs = [:summary, :description, :examples, :short_description, :notes, :author] defined?(SingleLineAttrs) or SingleLineAttrs = [:summary, :author] # Simple, procedural tests that apply to a bunch of methods. Attrs.each do |attr| it "should accept a #{attr}" do expect { subject.send("#{attr}=", "hello") }.not_to raise_error subject.send(attr).should == "hello" end it "should accept a long (single line) value for #{attr}" do text = "I never know when to stop with the word banana" + ("na" * 1000) expect { subject.send("#{attr}=", text) }.to_not raise_error subject.send(attr).should == text end end Attrs.each do |getter| setter = "#{getter}=".to_sym context "#{getter}" do it "should strip leading whitespace on a single line" do subject.send(setter, " death to whitespace") subject.send(getter).should == "death to whitespace" end it "should strip trailing whitespace on a single line" do subject.send(setter, "death to whitespace ") subject.send(getter).should == "death to whitespace" end it "should strip whitespace at both ends at once" do subject.send(setter, " death to whitespace ") subject.send(getter).should == "death to whitespace" end multiline_text = "with\nnewlines" if SingleLineAttrs.include? getter then it "should not accept multiline values" do expect { subject.send(setter, multiline_text) }. to raise_error ArgumentError, /#{getter} should be a single line/ subject.send(getter).should be_nil end else it "should accept multiline values" do expect { subject.send(setter, multiline_text) }.not_to raise_error subject.send(getter).should == multiline_text end [1, 2, 4, 7, 25].each do |length| context "#{length} chars indent" do indent = ' ' * length it "should strip leading whitespace on multiple lines" do text = "this\nis\the\final\outcome" subject.send(setter, text.gsub(/^/, indent)) subject.send(getter).should == text end it "should not remove formatting whitespace, only global indent" do text = "this\n is\n the\n ultimate\ntest\n" subject.send(setter, text.gsub(/^/, indent)) subject.send(getter).should == text end end end it "should strip whitespace with a blank line" do subject.send(setter, " this\n\n should outdent\n") subject.send(getter).should == "this\n\nshould outdent\n" end end end end describe "#short_description" do it "should return the set value if set after description" do subject.description = "hello\ngoodbye" subject.short_description = "whatever" subject.short_description.should == "whatever" end it "should return the set value if set before description" do subject.short_description = "whatever" subject.description = "hello\ngoodbye" subject.short_description.should == "whatever" end it "should return nothing if not set and no description" do subject.short_description.should be_nil end it "should return the first paragraph of description if not set (where it is one line long)" do subject.description = "hello" subject.short_description.should == subject.description end it "should return the first paragraph of description if not set (where there is no paragraph break)" do subject.description = "hello\ngoodbye" subject.short_description.should == subject.description end it "should return the first paragraph of description if not set (where there is a paragraph break)" do subject.description = "hello\ngoodbye\n\nmore\ntext\nhere\n\nfinal\nparagraph" subject.short_description.should == "hello\ngoodbye" end - it "should trim a very, very long first paragraph" do + it "should trim a very, very long first paragraph and add ellipsis" do line = "this is a very, very, very long long line full of text\n" subject.description = line * 20 + "\n\nwhatever, dude." - subject.short_description.should == (line * 5).chomp + subject.short_description.should == (line * 5).chomp + ' [...]' + end + + it "should trim a very very long only paragraph even if it is followed by a new paragraph" do + line = "this is a very, very, very long long line full of text\n" + subject.description = line * 20 + + subject.short_description.should == (line * 5).chomp + ' [...]' end end describe "multiple authors" do authors = %w{John Paul George Ringo} context "in the DSL" do it "should support multiple authors" do authors.each {|name| subject.author name } subject.authors.should =~ authors subject.author.should == authors.join("\n") end it "should reject author as an array" do expect { subject.author ["Foo", "Bar"] }. to raise_error ArgumentError, /author must be a string/ end end context "#author=" do it "should accept a single name" do subject.author = "Fred" subject.author.should == "Fred" end it "should accept an array of names" do subject.author = authors subject.authors.should =~ authors subject.author.should == authors.join("\n") end it "should not append when set multiple times" do subject.author = "Fred" subject.author = "John" subject.author.should == "John" end it "should reject arrays with embedded newlines" do expect { subject.author = ["Fred\nJohn"] }. to raise_error ArgumentError, /author should be a single line/ end end end describe "#license" do it "should default to reserving rights" do subject.license.should =~ /All Rights Reserved/ end it "should accept an arbitrary license string on the object" do subject.license = "foo" subject.license.should == "foo" end it "should accept symbols to specify existing licenses..." end describe "#copyright" do it "should fail with just a name" do expect { subject.copyright("invalid") }. to raise_error ArgumentError, /copyright takes the owners names, then the years covered/ end [1997, "1997"].each do |year| it "should accept an entity name and a #{year.class.name} year" do subject.copyright("me", year) subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /#{year}/ end it "should accept multiple entity names and a #{year.class.name} year" do subject.copyright ["me", "you"], year subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /\byou\b/ subject.copyright.should =~ /#{year}/ end end ["1997-2003", "1997 - 2003", 1997..2003].each do |range| it "should accept a #{range.class.name} range of years" do subject.copyright("me", range) subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /1997-2003/ end it "should accept a #{range.class.name} range of years" do subject.copyright ["me", "you"], range subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /\byou\b/ subject.copyright.should =~ /1997-2003/ end end [[1997, 2003], ["1997", 2003], ["1997", "2003"]].each do |input| it "should accept the set of years #{input.inspect} in an array" do subject.copyright "me", input subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /1997, 2003/ end it "should accept the set of years #{input.inspect} in an array" do subject.copyright ["me", "you"], input subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /\byou\b/ subject.copyright.should =~ /1997, 2003/ end end it "should warn if someone does math accidentally on the range of years" do expect { subject.copyright "me", 1997-2003 }. to raise_error ArgumentError, /copyright with a year before 1970 is very strange; did you accidentally add or subtract two years\?/ end it "should accept complex copyright years" do years = [1997, 1999, 2000..2002, 2005].reverse subject.copyright "me", years subject.copyright.should =~ /\bme\b/ subject.copyright.should =~ /1997, 1999, 2000-2002, 2005/ end end # Things that are automatically generated. [:name, :options, :synopsis].each do |attr| describe "##{attr}" do it "should not allow you to set #{attr}" do subject.should_not respond_to :"#{attr}=" end it "should have a #{attr}" do subject.send(attr).should_not be_nil end it "'s #{attr} should not be empty..." do subject.send(attr).should_not == '' end end end end diff --git a/spec/unit/application/indirection_base_spec.rb b/spec/unit/application/indirection_base_spec.rb index d72def6cf..8a5eee2c6 100755 --- a/spec/unit/application/indirection_base_spec.rb +++ b/spec/unit/application/indirection_base_spec.rb @@ -1,41 +1,42 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/application/indirection_base' require 'puppet/indirector/face' ######################################################################## # Stub for testing; the names are critical, sadly. --daniel 2011-03-30 class Puppet::Application::TestIndirection < Puppet::Application::IndirectionBase end face = Puppet::Indirector::Face.define(:testindirection, '0.0.1') do summary "fake summary" copyright "Puppet Labs", 2011 license "Apache 2 license; see COPYING" end # REVISIT: This horror is required because we don't allow anything to be # :current except for if it lives on, and is loaded from, disk. --daniel 2011-03-29 face.instance_variable_set('@version', :current) Puppet::Face.register(face) ######################################################################## - describe Puppet::Application::IndirectionBase do subject { Puppet::Application::TestIndirection.new } it "should accept a terminus command line option" do # It would be nice not to have to stub this, but whatever... writing an # entire indirection stack would cause us more grief. --daniel 2011-03-31 terminus = stub_everything("test indirection terminus") + terminus.stubs(:name).returns(:testindirection) + Puppet::Indirector::Indirection.expects(:instance). with(:testindirection).returns(terminus) subject.command_line.instance_variable_set('@args', %w{--terminus foo save bar}) # Not a very nice thing. :( $stderr.stubs(:puts) Puppet.stubs(:err) expect { subject.run }.to exit_with 0 end end diff --git a/spec/unit/indirector/terminus_spec.rb b/spec/unit/indirector/terminus_spec.rb index 33932cfca..2f37c1ff5 100755 --- a/spec/unit/indirector/terminus_spec.rb +++ b/spec/unit/indirector/terminus_spec.rb @@ -1,244 +1,250 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/defaults' require 'puppet/indirector' require 'puppet/indirector/file' describe Puppet::Indirector::Terminus, :'fails_on_ruby_1.9.2' => true do before :each do Puppet::Indirector::Terminus.stubs(:register_terminus_class) @indirection = stub 'indirection', :name => :my_stuff, :register_terminus_type => nil Puppet::Indirector::Indirection.stubs(:instance).with(:my_stuff).returns(@indirection) @abstract_terminus = Class.new(Puppet::Indirector::Terminus) do def self.to_s "Testing::Abstract" end end @terminus_class = Class.new(@abstract_terminus) do def self.to_s "MyStuff::TermType" end end @terminus = @terminus_class.new end describe Puppet::Indirector::Terminus do it "should provide a method for setting terminus class documentation" do @terminus_class.should respond_to(:desc) end it "should support a class-level name attribute" do @terminus_class.should respond_to(:name) end it "should support a class-level indirection attribute" do @terminus_class.should respond_to(:indirection) end it "should support a class-level terminus-type attribute" do @terminus_class.should respond_to(:terminus_type) end it "should support a class-level model attribute" do @terminus_class.should respond_to(:model) end it "should accept indirection instances as its indirection" do indirection = stub 'indirection', :is_a? => true, :register_terminus_type => nil proc { @terminus_class.indirection = indirection }.should_not raise_error @terminus_class.indirection.should equal(indirection) end it "should look up indirection instances when only a name has been provided" do indirection = mock 'indirection' Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns(indirection) @terminus_class.indirection = :myind @terminus_class.indirection.should equal(indirection) end it "should fail when provided a name that does not resolve to an indirection" do Puppet::Indirector::Indirection.expects(:instance).with(:myind).returns(nil) proc { @terminus_class.indirection = :myind }.should raise_error(ArgumentError) # It shouldn't overwrite our existing one (or, more normally, it shouldn't set # anything). @terminus_class.indirection.should equal(@indirection) end end describe Puppet::Indirector::Terminus, " when creating terminus classes" do it "should associate the subclass with an indirection based on the subclass constant" do @terminus.indirection.should equal(@indirection) end it "should set the subclass's type to the abstract terminus name" do @terminus.terminus_type.should == :abstract end it "should set the subclass's name to the indirection name" do @terminus.name.should == :term_type end it "should set the subclass's model to the indirection model" do @indirection.expects(:model).returns :yay @terminus.model.should == :yay end end describe Puppet::Indirector::Terminus, " when a terminus instance" do it "should return the class's name as its name" do @terminus.name.should == :term_type end it "should return the class's indirection as its indirection" do @terminus.indirection.should equal(@indirection) end it "should set the instances's type to the abstract terminus type's name" do @terminus.terminus_type.should == :abstract end it "should set the instances's model to the indirection's model" do @indirection.expects(:model).returns :yay @terminus.model.should == :yay end end end # LAK: This could reasonably be in the Indirection instances, too. It doesn't make # a whole heckuva lot of difference, except that with the instance loading in # the Terminus base class, we have to have a check to see if we're already # instance-loading a given terminus class type. describe Puppet::Indirector::Terminus, " when managing terminus classes" do it "should provide a method for registering terminus classes" do Puppet::Indirector::Terminus.should respond_to(:register_terminus_class) end it "should provide a method for returning terminus classes by name and type" do terminus = stub 'terminus_type', :name => :abstract, :indirection_name => :whatever Puppet::Indirector::Terminus.register_terminus_class(terminus) Puppet::Indirector::Terminus.terminus_class(:whatever, :abstract).should equal(terminus) end it "should set up autoloading for any terminus class types requested" do Puppet::Indirector::Terminus.expects(:instance_load).with(:test2, "puppet/indirector/test2") Puppet::Indirector::Terminus.terminus_class(:test2, :whatever) end it "should load terminus classes that are not found" do # Set up instance loading; it would normally happen automatically Puppet::Indirector::Terminus.instance_load :test1, "puppet/indirector/test1" Puppet::Indirector::Terminus.instance_loader(:test1).expects(:load).with(:yay) Puppet::Indirector::Terminus.terminus_class(:test1, :yay) end it "should fail when no indirection can be found", :'fails_on_ruby_1.9.2' => true do Puppet::Indirector::Indirection.expects(:instance).with(:my_indirection).returns(nil) @abstract_terminus = Class.new(Puppet::Indirector::Terminus) do def self.to_s "Abstract" end end proc { @terminus = Class.new(@abstract_terminus) do def self.to_s "MyIndirection::TestType" end end }.should raise_error(ArgumentError) end it "should register the terminus class with the terminus base class", :'fails_on_ruby_1.9.2' => true do Puppet::Indirector::Terminus.expects(:register_terminus_class).with do |type| type.indirection_name == :my_indirection and type.name == :test_terminus end @indirection = stub 'indirection', :name => :my_indirection, :register_terminus_type => nil Puppet::Indirector::Indirection.expects(:instance).with(:my_indirection).returns(@indirection) @abstract_terminus = Class.new(Puppet::Indirector::Terminus) do def self.to_s "Abstract" end end @terminus = Class.new(@abstract_terminus) do def self.to_s "MyIndirection::TestTerminus" end end end end describe Puppet::Indirector::Terminus, " when parsing class constants for indirection and terminus names" do before do @subclass = mock 'subclass' @subclass.stubs(:to_s).returns("TestInd::OneTwo") @subclass.stubs(:mark_as_abstract_terminus) Puppet::Indirector::Terminus.stubs(:register_terminus_class) end it "should fail when anonymous classes are used" do proc { Puppet::Indirector::Terminus.inherited(Class.new) }.should raise_error(Puppet::DevError) end it "should use the last term in the constant for the terminus class name" do @subclass.expects(:name=).with(:one_two) @subclass.stubs(:indirection=) Puppet::Indirector::Terminus.inherited(@subclass) end it "should convert the terminus name to a downcased symbol" do @subclass.expects(:name=).with(:one_two) @subclass.stubs(:indirection=) Puppet::Indirector::Terminus.inherited(@subclass) end it "should use the second to last term in the constant for the indirection name" do @subclass.expects(:indirection=).with(:test_ind) @subclass.stubs(:name=) @subclass.stubs(:terminus_type=) Puppet::Indirector::File.inherited(@subclass) end it "should convert the indirection name to a downcased symbol" do @subclass.expects(:indirection=).with(:test_ind) @subclass.stubs(:name=) @subclass.stubs(:terminus_type=) Puppet::Indirector::File.inherited(@subclass) end it "should convert camel case to lower case with underscores as word separators" do @subclass.expects(:name=).with(:one_two) @subclass.stubs(:indirection=) Puppet::Indirector::Terminus.inherited(@subclass) end end describe Puppet::Indirector::Terminus, " when creating terminus class types", :'fails_on_ruby_1.9.2' => true do before do Puppet::Indirector::Terminus.stubs(:register_terminus_class) @subclass = Class.new(Puppet::Indirector::Terminus) do def self.to_s "Puppet::Indirector::Terminus::MyTermType" end end end it "should set the name of the abstract subclass to be its class constant" do @subclass.name.should equal(:my_term_type) end it "should mark abstract terminus types as such" do @subclass.should be_abstract_terminus end it "should not allow instances of abstract subclasses to be created" do proc { @subclass.new }.should raise_error(Puppet::DevError) end end +describe Puppet::Indirector::Terminus, " when listing terminus classes" do + it "should list the terminus files available to load" do + Puppet::Util::Autoload.any_instance.stubs(:files_to_load).returns ["/foo/bar/baz", "/max/runs/marathon"] + Puppet::Indirector::Terminus.terminus_classes('my_stuff').should == [:baz, :marathon] + end +end diff --git a/spec/unit/util/autoload_spec.rb b/spec/unit/util/autoload_spec.rb index 512f06c75..d61b7689e 100755 --- a/spec/unit/util/autoload_spec.rb +++ b/spec/unit/util/autoload_spec.rb @@ -1,119 +1,119 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/util/autoload' describe Puppet::Util::Autoload do before do @autoload = Puppet::Util::Autoload.new("foo", "tmp") @autoload.stubs(:eachdir).yields "/my/dir" end it "should use the Cacher module" do Puppet::Util::Autoload.ancestors.should be_include(Puppet::Util::Cacher) end describe "when building the search path" do it "should collect all of the plugins and lib directories that exist in the current environment's module path" do Puppet.settings.expects(:value).with(:environment).returns "foo" Puppet.settings.expects(:value).with(:modulepath, :foo).returns "/a:/b:/c" Dir.expects(:entries).with("/a").returns %w{one two} Dir.expects(:entries).with("/b").returns %w{one two} FileTest.stubs(:directory?).returns false FileTest.expects(:directory?).with("/a").returns true FileTest.expects(:directory?).with("/b").returns true %w{/a/one/plugins /a/two/lib /b/one/plugins /b/two/lib}.each do |d| FileTest.expects(:directory?).with(d).returns true end @autoload.module_directories.should == %w{/a/one/plugins /a/two/lib /b/one/plugins /b/two/lib} end it "should not look for lib directories in directories starting with '.'" do Puppet.settings.expects(:value).with(:environment).returns "foo" Puppet.settings.expects(:value).with(:modulepath, :foo).returns "/a" Dir.expects(:entries).with("/a").returns %w{. ..} FileTest.expects(:directory?).with("/a").returns true FileTest.expects(:directory?).with("/a/./lib").never FileTest.expects(:directory?).with("/a/./plugins").never FileTest.expects(:directory?).with("/a/../lib").never FileTest.expects(:directory?).with("/a/../plugins").never @autoload.module_directories end it "should include the module directories, the Puppet libdir, and all of the Ruby load directories" do Puppet.stubs(:[]).with(:libdir).returns(%w{/libdir1 /lib/dir/two /third/lib/dir}.join(File::PATH_SEPARATOR)) @autoload.expects(:module_directories).returns %w{/one /two} @autoload.search_directories.should == %w{/one /two /libdir1 /lib/dir/two /third/lib/dir} + $LOAD_PATH end - it "should include in its search path all of the search directories that have a subdirectory matching the autoload path" do + it "should include in its search path all of the unique search directories that have a subdirectory matching the autoload path" do @autoload = Puppet::Util::Autoload.new("foo", "loaddir") - @autoload.expects(:search_directories).returns %w{/one /two /three} + @autoload.expects(:search_directories).returns %w{/one /two /three /three} FileTest.expects(:directory?).with("/one/loaddir").returns true FileTest.expects(:directory?).with("/two/loaddir").returns false FileTest.expects(:directory?).with("/three/loaddir").returns true @autoload.searchpath.should == ["/one/loaddir", "/three/loaddir"] end end it "should include its FileCache module" do Puppet::Util::Autoload.ancestors.should be_include(Puppet::Util::Autoload::FileCache) end describe "when loading a file" do before do @autoload.stubs(:searchpath).returns %w{/a} end [RuntimeError, LoadError, SyntaxError].each do |error| it "should die with Puppet::Error if a #{error.to_s} exception is thrown" do @autoload.stubs(:file_exist?).returns true Kernel.expects(:load).raises error lambda { @autoload.load("foo") }.should raise_error(Puppet::Error) end end it "should not raise an error if the file is missing" do @autoload.load("foo").should == false end it "should register loaded files with the main loaded file list so they are not reloaded by ruby" do @autoload.stubs(:file_exist?).returns true Kernel.stubs(:load) @autoload.load("myfile") $LOADED_FEATURES.should be_include("tmp/myfile.rb") end end describe "when loading all files" do before do @autoload.stubs(:searchpath).returns %w{/a} Dir.stubs(:glob).returns "/path/to/file.rb" @autoload.class.stubs(:loaded?).returns(false) end [RuntimeError, LoadError, SyntaxError].each do |error| it "should die an if a #{error.to_s} exception is thrown", :'fails_on_ruby_1.9.2' => true do Kernel.expects(:require).raises error lambda { @autoload.loadall }.should raise_error(Puppet::Error) end end it "should require the full path to the file", :'fails_on_ruby_1.9.2' => true do Kernel.expects(:require).with("/path/to/file.rb") @autoload.loadall end end end