diff --git a/lib/puppet/face/help/action.erb b/lib/puppet/face/help/action.erb index eaf131464..7a9b87117 100644 --- a/lib/puppet/face/help/action.erb +++ b/lib/puppet/face/help/action.erb @@ -1,3 +1,47 @@ -Use: puppet <%= face.name %> [options] <%= action.name %> [options] +puppet <%= face.name %><%= action.default? ? '' : " #{action.name}" %>(1) -- <%= action.summary || face.summary %> +<%= '=' * (_erbout.length - 1) %> -Summary: <%= action.summary %> +% if action.synopsis +SYNOPSIS +-------- + +<%= action.synopsis %> + +% end +% if action.description +DESCRIPTION +----------- +<%= action.description %> + +%end +% unless action.options.empty? +OPTIONS +------- +% action.options.sort.each do |name| +% option = action.get_option name +<%= " " + option.optparse.join(" |" ) %> +<%= option.desc and option.desc.gsub(/^/, ' ') %> + +% end +% end +% if action.examples +EXAMPLES +-------- +<%= action.examples %> +% end +% if action.notes +NOTES +----- +<%= action.notes %> + +% end +% unless action.authors.empty? +AUTHOR +------ +<%= action.authors.map {|x| " * " + x } .join("\n") %> + +%end +COPYRIGHT AND LICENSE +--------------------- +<%= action.copyright %> +<%= action.license %> diff --git a/lib/puppet/face/help/face.erb b/lib/puppet/face/help/face.erb index efe5fd809..944f7a96b 100644 --- a/lib/puppet/face/help/face.erb +++ b/lib/puppet/face/help/face.erb @@ -1,7 +1,47 @@ -Use: puppet <%= face.name %> [options] [options] +NAME + <%= face.name %> -- <%= face.summary || "unknown face..." %> -Available actions: +% if face.synopsis +SYNOPSIS +<%= face.synopsis.gsub(/^/, ' ') %> + +% end +% if face.description +DESCRIPTION +<%= face.description.chomp.gsub(/^/, ' ') %> + +%end +% unless face.options.empty? +OPTIONS +% face.options.sort.each do |name| +% option = face.get_option name +<%= " " + option.optparse.join(" |" ) %> +<%= option.desc and option.desc.gsub(/^/, ' ') %> + +% end +% end +ACTIONS +% padding = face.actions.map{|x| x.to_s.length}.max + 2 % face.actions.each do |actionname| % action = face.get_action(actionname) - <%= action.name.to_s.ljust(16) %> <%= action.summary %> + <%= action.name.to_s.ljust(padding) %> <%= action.summary %> % end + +% if face.examples +EXAMPLES +<%= face.examples %> +% end +% if face.notes +NOTES +<%= face.notes %> + +% end +% unless face.authors.empty? +AUTHOR +<%= face.authors.join("\n").gsub(/^/, ' * ') %> + +%end +COPYRIGHT AND LICENSE +<%= face.copyright.gsub(/^/, ' ') %> +<%= face.license.gsub(/^/, ' ') %> + diff --git a/lib/puppet/interface.rb b/lib/puppet/interface.rb index ced00863d..adf6c991c 100644 --- a/lib/puppet/interface.rb +++ b/lib/puppet/interface.rb @@ -1,159 +1,333 @@ require 'puppet' require 'puppet/util/autoload' +require 'prettyprint' class Puppet::Interface require 'puppet/interface/face_collection' require 'puppet/interface/action_manager' include Puppet::Interface::ActionManager extend Puppet::Interface::ActionManager require 'puppet/interface/option_manager' include Puppet::Interface::OptionManager extend Puppet::Interface::OptionManager include Puppet::Util class << self # This is just so we can search for actions. We only use its # list of directories to search. # Can't we utilize an external autoloader, or simply use the $LOAD_PATH? -pvb def autoloader @autoloader ||= Puppet::Util::Autoload.new(:application, "puppet/face") end def faces Puppet::Interface::FaceCollection.faces end def register(instance) Puppet::Interface::FaceCollection.register(instance) end def define(name, version, &block) face = Puppet::Interface::FaceCollection[name, version] if face.nil? then face = self.new(name, version) Puppet::Interface::FaceCollection.register(face) # REVISIT: Shouldn't this be delayed until *after* we evaluate the # current block, not done before? --daniel 2011-04-07 face.load_actions end face.instance_eval(&block) if block_given? return face end def face?(name, version) Puppet::Interface::FaceCollection[name, version] end def [](name, version) unless face = Puppet::Interface::FaceCollection[name, version] if current = Puppet::Interface::FaceCollection[name, :current] raise Puppet::Error, "Could not find version #{version} of #{name}" else raise Puppet::Error, "Could not find Puppet Face #{name.inspect}" end end face end end def set_default_format(format) Puppet.warning("set_default_format is deprecated (and ineffective); use render_as on your actions instead.") end ######################################################################## # Documentation. We currently have to rewrite both getters because we share # the same instance between build-time and the runtime instance. When that # splits out this should merge into a module that both the action and face # include. --daniel 2011-04-17 - attr_accessor :summary, :description + attr_accessor :summary def summary(value = nil) self.summary = value unless value.nil? @summary end def summary=(value) value = value.to_s value =~ /\n/ and raise ArgumentError, "Face summary should be a single line; put the long text in 'description' instead." @summary = value end + attr_accessor :description def description(value = nil) self.description = value unless value.nil? @description end + attr_accessor :examples + def examples(value = nil) + self.examples = value unless value.nil? + @examples + end + + attr_accessor :short_description + def short_description(value = nil) + self.short_description = value unless value.nil? + if @short_description.nil? then + fail "REVISIT: Extract this..." + 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(value) + end + @authors.empty? ? nil : @authors.join("\n") + 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) + end + def authors + @authors + end + def authors=(value) + if Array(value).any? {|x| x =~ /\n/ } then + raise ArgumentError, 'author should be a single line; use multiple statements' + end + @authors = Array(value) + end + + attr_accessor :notes + def notes(value = nil) + @notes = value unless value.nil? + @notes + end + + attr_accessor :license + def license(value = nil) + @license = value unless value.nil? + @license + end + + 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 + + def synopsis + output = PrettyPrint.format do |s| + s.text("puppet #{name} ") + s.breakable + + options.each do |option| + option = get_option(option) + wrap = option.required? ? %w{ < > } : %w{ [ ] } + + s.group(0, *wrap) do + option.optparse.each do |item| + unless s.current_group.first? + s.breakable + s.text '|' + s.breakable + end + s.text item + end + end + end + end + end + ######################################################################## attr_reader :name, :version def initialize(name, version, &block) unless Puppet::Interface::FaceCollection.validate_version(version) raise ArgumentError, "Cannot create face #{name.inspect} with invalid version number '#{version}'!" end - @name = Puppet::Interface::FaceCollection.underscorize(name) + @name = Puppet::Interface::FaceCollection.underscorize(name) @version = version + # The few bits of documentation we actually demand. The default license + # is a favour to our end users; if you happen to get that in a core face + # report it as a bug, please. --daniel 2011-04-26 + @authors = [] + @license = 'All Rights Reserved' + instance_eval(&block) if block_given? end # Try to find actions defined in other files. def load_actions Puppet::Interface.autoloader.search_directories.each do |dir| Dir.glob(File.join(dir, "puppet/face/#{name}", "*.rb")).each do |file| action = file.sub(dir, '').sub(/^[\\\/]/, '').sub(/\.rb/, '') Puppet.debug "Loading action '#{action}' for '#{name}' from '#{dir}/#{action}.rb'" require(action) end end end def to_s "Puppet::Face[#{name.inspect}, #{version.inspect}]" end ######################################################################## # Action decoration, whee! You are not expected to care about this code, # which exists to support face building and construction. I marked these # private because the implementation is crude and ugly, and I don't yet know # enough to work out how to make it clean. # # Once we have established that these methods will likely change radically, # to be unrecognizable in the final outcome. At which point we will throw # all this away, replace it with something nice, and work out if we should # be making this visible to the outside world... --daniel 2011-04-14 private def __invoke_decorations(type, action, passed_args = [], passed_options = {}) [:before, :after].member?(type) or fail "unknown decoration type #{type}" # Collect the decoration methods matching our pass. methods = action.options.select do |name| passed_options.has_key? name end.map do |name| action.get_option(name).__decoration_name(type) end methods.each do |hook| begin respond_to? hook and self.__send__(hook, action, passed_args, passed_options) rescue => e Puppet.warning("invoking #{action} #{type} hook: #{e}") end end end def __add_method(name, proc) meta_def(name, &proc) method(name).unbind end def self.__add_method(name, proc) define_method(name, proc) instance_method(name) end end diff --git a/spec/shared_behaviours/documentation_on_faces.rb b/spec/shared_behaviours/documentation_on_faces.rb index 41b4015c9..ef2645620 100644 --- a/spec/shared_behaviours/documentation_on_faces.rb +++ b/spec/shared_behaviours/documentation_on_faces.rb @@ -1,35 +1,178 @@ # encoding: UTF-8 shared_examples_for "documentation on faces" do - context "description" do - describe "#summary" do - it "should accept a summary" do - text = "this is my summary" - expect { subject.summary = text }.to_not raise_error - subject.summary.should == text + 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 + + # Should they accept multiple lines? + Attrs.each do |attr| + text = "with\nnewlines" + + if SingleLineAttrs.include? attr then + it "should not accept multiline values for #{attr}" do + expect { subject.send("#{attr}=", text) }. + to raise_error ArgumentError, /#{attr} should be a single line/ + subject.send(attr).should be_nil + end + else + it "should accept multiline values for #{attr}" do + expect { subject.send("#{attr}=", text) }.not_to raise_error + subject.send(attr).should == text + end + 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 in the DSL" do + subject.license("foo") + subject.license.should == "foo" + 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 - it "should accept a long, long, long summary" do - text = "I never know when to stop with the word banana" + ("na" * 1000) - expect { subject.summary = text }.to_not raise_error - subject.summary.should == text + 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 reject a summary with a newline" do - expect { subject.summary = "with\nnewlines" }. - to raise_error ArgumentError, /summary should be a single line/ + 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 - describe "#description" do - it "should accept a description" do - subject.description = "hello" - subject.description.should == "hello" + ["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 accept a description with a newline" do - subject.description = "hello \n my \n fine \n friend" - subject.description.should == "hello \n my \n fine \n friend" + it "should have a #{attr}" do + subject.send(attr).should_not be_nil end end end end diff --git a/spec/unit/interface_spec.rb b/spec/unit/interface_spec.rb index a1d70cf64..27da39766 100755 --- a/spec/unit/interface_spec.rb +++ b/spec/unit/interface_spec.rb @@ -1,220 +1,224 @@ require 'spec_helper' require 'puppet/face' require 'puppet/interface' describe Puppet::Interface do subject { Puppet::Interface } before :each do @faces = Puppet::Interface::FaceCollection. instance_variable_get("@faces").dup @dq = $".dup $".delete_if do |path| path =~ %r{/face/.*\.rb$} end Puppet::Interface::FaceCollection.instance_variable_get("@faces").clear end after :each do Puppet::Interface::FaceCollection.instance_variable_set("@faces", @faces) $".clear ; @dq.each do |item| $" << item end end describe "#[]" do it "should fail when no version is requested" do expect { subject[:huzzah] }.should raise_error ArgumentError end it "should raise an exception when the requested version is unavailable" do expect { subject[:huzzah, '17.0.0'] }.should raise_error, Puppet::Error end it "should raise an exception when the requested face doesn't exist" do expect { subject[:burrble_toot, :current] }.should raise_error, Puppet::Error end describe "version matching" do { '1' => '1.1.1', '1.0' => '1.0.1', '1.0.1' => '1.0.1', '1.1' => '1.1.1', '1.1.1' => '1.1.1' }.each do |input, expect| it "should match #{input.inspect} to #{expect.inspect}" do face = subject[:version_matching, input] face.should be face.version.should == expect end end %w{1.0.2 1.2}.each do |input| it "should not match #{input.inspect} to any version" do expect { subject[:version_matching, input] }. to raise_error Puppet::Error, /Could not find version/ end end end end describe "#define" do it "should register the face" do face = subject.define(:face_test_register, '0.0.1') face.should == subject[:face_test_register, '0.0.1'] end it "should load actions" do subject.any_instance.expects(:load_actions) subject.define(:face_test_load_actions, '0.0.1') end it "should require a version number" do expect { subject.define(:no_version) }.to raise_error ArgumentError end it "should support summary builder and accessor methods" do subject.new(:foo, '1.0.0').should respond_to(:summary).with(0).arguments subject.new(:foo, '1.0.0').should respond_to(:summary=).with(1).arguments end # Required documentation methods... { :summary => "summary", - :description => "This is the description of the stuff\n\nWhee" + :description => "This is the description of the stuff\n\nWhee", + :examples => "This is my example", + :short_description => "This is my custom short description", + :notes => "These are my notes...", + :author => "This is my authorship data", }.each do |attr, value| it "should support #{attr} in the builder" do face = subject.new(:builder, '1.0.0') do self.send(attr, value) end face.send(attr).should == value end end end describe "#initialize" do it "should require a version number" do expect { subject.new(:no_version) }.to raise_error ArgumentError end it "should require a valid version number" do expect { subject.new(:bad_version, 'Rasins') }. should raise_error ArgumentError end it "should instance-eval any provided block" do face = subject.new(:face_test_block, '0.0.1') do action(:something) do when_invoked { "foo" } end end face.something.should == "foo" end end it "should have a name" do subject.new(:me, '0.0.1').name.should == :me end it "should stringify with its own name" do subject.new(:me, '0.0.1').to_s.should =~ /\bme\b/ end # Why? it "should create a class-level autoloader" do subject.autoloader.should be_instance_of(Puppet::Util::Autoload) end it "should try to require faces that are not known" do pending "mocking require causes random stack overflow" subject::FaceCollection.expects(:require).with "puppet/face/foo" subject[:foo, '0.0.1'] end it "should be able to load all actions in all search paths" it_should_behave_like "things that declare options" do def add_options_to(&block) subject.new(:with_options, '0.0.1', &block) end end describe "with face-level options" do it "should not return any action-level options" do face = subject.new(:with_options, '0.0.1') do option "--foo" option "--bar" action :baz do option "--quux" end end face.options.should =~ [:foo, :bar] end it "should fail when a face option duplicates an action option" do expect { subject.new(:action_level_options, '0.0.1') do action :bar do option "--foo" end option "--foo" end }.should raise_error ArgumentError, /Option foo conflicts with existing option foo on/i end it "should work when two actions have the same option" do face = subject.new(:with_options, '0.0.1') do action :foo do option "--quux" end action :bar do option "--quux" end end face.get_action(:foo).options.should =~ [:quux] face.get_action(:bar).options.should =~ [:quux] end end describe "with inherited options" do let :parent do parent = Class.new(subject) parent.option("--inherited") parent.action(:parent_action) do end parent end let :face do face = parent.new(:example, '0.2.1') face.option("--local") face.action(:face_action) do end face end describe "#options" do it "should list inherited options" do face.options.should =~ [:inherited, :local] end it "should see all options on face actions" do face.get_action(:face_action).options.should =~ [:inherited, :local] end it "should see all options on inherited actions accessed on the subclass" do face.get_action(:parent_action).options.should =~ [:inherited, :local] end it "should not see subclass actions on the parent class" do parent.options.should =~ [:inherited] end it "should not see subclass actions on actions accessed on the parent class" do parent.get_action(:parent_action).options.should =~ [:inherited] end end describe "#get_option" do it "should return an inherited option object" do face.get_option(:inherited).should be_an_instance_of subject::Option end end end it_should_behave_like "documentation on faces" do subject do Puppet::Interface.new(:face_documentation, '0.0.1') end end end