diff --git a/lib/puppet/application/face_base.rb b/lib/puppet/application/face_base.rb index d28a8af3b..d02769412 100644 --- a/lib/puppet/application/face_base.rb +++ b/lib/puppet/application/face_base.rb @@ -1,221 +1,231 @@ require 'puppet/application' require 'puppet/face' require 'optparse' require 'pp' class Puppet::Application::FaceBase < 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 - option("--render-as FORMAT") do |_arg| - format = _arg.to_sym - unless @render_method = Puppet::Network::FormatHandler.format(format) - unless format == :for_humans or format == :json - raise ArgumentError, "I don't know how to render '#{format}'" - end - end - @render_as = format + option("--render-as FORMAT") do |format| + self.render_as = format.to_sym end option("--mode RUNMODE", "-r") do |arg| raise "Invalid run mode #{arg}; supported modes are user, agent, master" unless %w{user agent master}.include?(arg) self.class.run_mode(arg.to_sym) set_run_mode self.class.run_mode end attr_accessor :face, :action, :type, :arguments, :render_as - def render(result) - format = render_as || action.render_as || :for_humans + def render_as=(format) + if format == :for_humans or format == :json + @render_as = format + elsif network_format = Puppet::Network::FormatHandler.format(format) + method = network_format.render_method + if method == "to_pson" then + @render_as = :json + else + @render_as = method.to_sym + end + else + raise ArgumentError, "I don't know how to render '#{format}'" + end + end + def render(result) # Invoke the rendering hook supplied by the user, if appropriate. - if hook = action.when_rendering(format) then + if hook = action.when_rendering(render_as) then result = hook.call(result) end - if format == :for_humans then + if render_as == :for_humans then render_for_humans(result) - elsif format == :json or @render_method == "to_pson" + elsif render_as == :json PSON::pretty_generate(result, :allow_nan => true, :max_nesting => false) else - result.send(@render_method) + result.send(render_as) end end def render_for_humans(result) # String to String return result if result.is_a? String return result if result.is_a? Numeric # Simple hash to table if result.is_a? Hash and result.keys.all? { |x| x.is_a? String or x.is_a? Numeric } output = '' column_a = result.map do |k,v| k.to_s.length end.max + 2 column_b = 79 - column_a result.sort_by { |k,v| k.to_s } .each do |key, value| output << key.to_s.ljust(column_a) output << PP.pp(value, '', column_b). chomp.gsub(/\n */) { |x| x + (' ' * column_a) } output << "\n" end return output end # ...or pretty-print the inspect outcome. return result.pretty_inspect end def preinit super Signal.trap(:INT) do $stderr.puts "Cancelling Face" exit(0) end end def parse_options # We need to parse enough of the command line out early, to identify what # the action is, so that we can obtain the full set of options to parse. # REVISIT: These should be configurable versions, through a global # '--version' option, but we don't implement that yet... --daniel 2011-03-29 @type = self.class.name.to_s.sub(/.+:/, '').downcase.to_sym @face = Puppet::Face[@type, :current] # Now, walk the command line and identify the action. We skip over # arguments based on introspecting the action and all, and find the first # non-option word to use as the action. action = nil index = -1 until @action or (index += 1) >= command_line.args.length do item = command_line.args[index] if item =~ /^-/ then option = @face.options.find do |name| item =~ /^-+#{name.to_s.gsub(/[-_]/, '[-_]')}(?:[ =].*)?$/ end if option then option = @face.get_option(option) # If we have an inline argument, just carry on. We don't need to # care about optional vs mandatory in that case because we do a real # parse later, and that will totally take care of raising the error # when we get there. --daniel 2011-04-04 if option.takes_argument? and !item.index('=') then index += 1 unless (option.optional_argument? and command_line.args[index + 1] =~ /^-/) end elsif option = find_global_settings_argument(item) then unless Puppet.settings.boolean? option.name then # As far as I can tell, we treat non-bool options as always having # a mandatory argument. --daniel 2011-04-05 index += 1 # ...so skip the argument. end elsif option = find_application_argument(item) then index += 1 if (option[:argument] and option[:optional]) else raise OptionParser::InvalidOption.new(item.sub(/=.*$/, '')) end else @action = @face.get_action(item.to_sym) end end if @action.nil? - @action = @face.get_default_action() - @is_default_action = true + if @action = @face.get_default_action() then + @is_default_action = true + else + Puppet.err "#{face.name} does not have a default action, and no action was given" + Puppet.err Puppet::Face[:help, :current].help(@face.name) + exit false + end end # Now we can interact with the default option code to build behaviour # around the full set of options we now know we support. @action.options.each do |option| option = @action.get_option(option) # make it the object. self.class.option(*option.optparse) # ...and make the CLI parse it. - end if @action + end # ...and invoke our parent to parse all the command line options. super end def find_global_settings_argument(item) Puppet.settings.each do |name, object| object.optparse_args.each do |arg| next unless arg =~ /^-/ # sadly, we have to emulate some of optparse here... pattern = /^#{arg.sub('[no-]', '').sub(/[ =].*$/, '')}(?:[ =].*)?$/ pattern.match item and return object end end return nil # nothing found. end def find_application_argument(item) self.class.option_parser_commands.each do |options, function| options.each do |option| next unless option =~ /^-/ pattern = /^#{option.sub('[no-]', '').sub(/[ =].*$/, '')}(?:[ =].*)?$/ next unless pattern.match(item) return { :argument => option =~ /[ =]/, :optional => option =~ /[ =]\[/ } end end return nil # not found end def setup Puppet::Util::Log.newdestination :console @arguments = command_line.args # Note: because of our definition of where the action is set, we end up # with it *always* being the first word of the remaining set of command # line arguments. So, strip that off when we construct the arguments to # pass down to the face action. --daniel 2011-04-04 # Of course, now that we have default actions, we should leave the # "action" name on if we didn't actually consume it when we found our # action. @arguments.delete_at(0) unless @is_default_action # We copy all of the app options to the end of the call; This allows each # action to read in the options. This replaces the older model where we # would invoke the action with options set as global state in the # interface object. --daniel 2011-03-28 @arguments << options + + # If we don't have a rendering format, set one early. + self.render_as ||= (@action.render_as || :for_humans) end def main status = false # Call the method associated with the provided action (e.g., 'find'). if @action begin result = @face.send(@action.name, *arguments) puts render(result) unless result.nil? status = true rescue Exception => detail puts detail.backtrace if Puppet[:trace] Puppet.err detail.to_s end else - if arguments.first.is_a? Hash - puts "#{face} does not have a default action" - else - puts "#{face} does not respond to action #{arguments.first}" - end - - puts Puppet::Face[:help, :current].help(@face.name, *arguments) + puts "#{face} does not respond to action #{arguments.first}" + puts Puppet::Face[:help, :current].help(@face.name) end exit status end end diff --git a/spec/unit/application/face_base_spec.rb b/spec/unit/application/face_base_spec.rb index 0c75236f8..275702085 100755 --- a/spec/unit/application/face_base_spec.rb +++ b/spec/unit/application/face_base_spec.rb @@ -1,321 +1,321 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/application/face_base' require 'tmpdir' class Puppet::Application::FaceBase::Basetest < Puppet::Application::FaceBase end describe Puppet::Application::FaceBase do let :app do app = Puppet::Application::FaceBase::Basetest.new app.command_line.stubs(:subcommand_name).returns('subcommand') Puppet::Util::Log.stubs(:newdestination) app end describe "#find_global_settings_argument" do it "should not match --ca to --ca-location" do option = mock('ca option', :optparse_args => ["--ca"]) Puppet.settings.expects(:each).yields(:ca, option) app.find_global_settings_argument("--ca-location").should be_nil end end describe "#parse_options" do before :each do app.command_line.stubs(:args).returns %w{} end describe "with just an action" do before :all do # We have to stub Signal.trap to avoid a crazy mess where we take # over signal handling and make it impossible to cancel the test # suite run. # # It would be nice to fix this elsewhere, but it is actually hard to # capture this in rspec 2.5 and all. :( --daniel 2011-04-08 Signal.stubs(:trap) app.command_line.stubs(:args).returns %w{foo} app.preinit app.parse_options end it "should set the face based on the type" do app.face.name.should == :basetest end it "should find the action" do app.action.should be app.action.name.should == :foo end end it "should use the default action if not given any arguments" do app.command_line.stubs(:args).returns [] - action = stub(:options => []) + action = stub(:options => [], :render_as => nil) Puppet::Face[:basetest, '0.0.1'].expects(:get_default_action).returns(action) app.stubs(:main) app.run app.action.should == action app.arguments.should == [ { } ] end it "should use the default action if not given a valid one" do app.command_line.stubs(:args).returns %w{bar} - action = stub(:options => []) + action = stub(:options => [], :render_as => nil) Puppet::Face[:basetest, '0.0.1'].expects(:get_default_action).returns(action) app.stubs(:main) app.run app.action.should == action app.arguments.should == [ 'bar', { } ] end it "should have no action if not given a valid one and there is no default action" do app.command_line.stubs(:args).returns %w{bar} Puppet::Face[:basetest, '0.0.1'].expects(:get_default_action).returns(nil) app.stubs(:main) - app.run - app.action.should be_nil - app.arguments.should == [ 'bar', { } ] + expect { app.run }.to exit_with 1 + @logs.first.message.should =~ /does not have a default action/ end it "should report a sensible error when options with = fail" do app.command_line.stubs(:args).returns %w{--action=bar foo} expect { app.preinit; app.parse_options }. to raise_error OptionParser::InvalidOption, /invalid option: --action/ end it "should fail if an action option is before the action" do app.command_line.stubs(:args).returns %w{--action foo} expect { app.preinit; app.parse_options }. to raise_error OptionParser::InvalidOption, /invalid option: --action/ end it "should fail if an unknown option is before the action" do app.command_line.stubs(:args).returns %w{--bar foo} expect { app.preinit; app.parse_options }. to raise_error OptionParser::InvalidOption, /invalid option: --bar/ end it "should fail if an unknown option is after the action" do app.command_line.stubs(:args).returns %w{foo --bar} expect { app.preinit; app.parse_options }. to raise_error OptionParser::InvalidOption, /invalid option: --bar/ end it "should accept --bar as an argument to a mandatory option after action" do app.command_line.stubs(:args).returns %w{foo --mandatory --bar} app.preinit app.parse_options app.action.name.should == :foo app.options.should == { :mandatory => "--bar" } end it "should accept --bar as an argument to a mandatory option before action" do app.command_line.stubs(:args).returns %w{--mandatory --bar foo} app.preinit app.parse_options app.action.name.should == :foo app.options.should == { :mandatory => "--bar" } end it "should not skip when --foo=bar is given" do app.command_line.stubs(:args).returns %w{--mandatory=bar --bar foo} expect { app.preinit; app.parse_options }. to raise_error OptionParser::InvalidOption, /invalid option: --bar/ end { "boolean options before" => %w{--trace foo}, "boolean options after" => %w{foo --trace} }.each do |name, args| it "should accept global boolean settings #{name} the action" do app.command_line.stubs(:args).returns args app.preinit app.parse_options Puppet[:trace].should be_true end end { "before" => %w{--syslogfacility user1 foo}, " after" => %w{foo --syslogfacility user1} }.each do |name, args| it "should accept global settings with arguments #{name} the action" do app.command_line.stubs(:args).returns args app.preinit app.parse_options Puppet[:syslogfacility].should == "user1" end end it "should handle application-level options" do - app.command_line.stubs(:args).returns %w{help --verbose help} + app.command_line.stubs(:args).returns %w{basetest --verbose return_true} app.preinit app.parse_options app.face.name.should == :basetest end end describe "#setup" do it "should remove the action name from the arguments" do app.command_line.stubs(:args).returns %w{--mandatory --bar foo} app.preinit app.parse_options app.setup app.arguments.should == [{ :mandatory => "--bar" }] end it "should pass positional arguments" do app.command_line.stubs(:args).returns %w{--mandatory --bar foo bar baz quux} app.preinit app.parse_options app.setup app.arguments.should == ['bar', 'baz', 'quux', { :mandatory => "--bar" }] end end describe "#main" do before :each do app.stubs(:puts) # don't dump text to screen. app.face = Puppet::Face[:basetest, '0.0.1'] app.action = app.face.get_action(:foo) app.arguments = ["myname", "myarg"] end it "should send the specified verb and name to the face" do app.face.expects(:foo).with(*app.arguments) expect { app.main }.to exit_with 0 end it "should lookup help when it cannot do anything else" do app.action = nil - Puppet::Face[:help, :current].expects(:help).with(:basetest, *app.arguments) + Puppet::Face[:help, :current].expects(:help).with(:basetest) expect { app.main }.to exit_with 1 end it "should use its render method to render any result" do app.expects(:render).with(app.arguments.length + 1) expect { app.main }.to exit_with 0 end end describe "error reporting" do before :each do app.stubs(:puts) # don't dump text to screen. + app.render_as = :json app.face = Puppet::Face[:basetest, '0.0.1'] app.arguments = [] end it "should exit 0 when the action returns true" do app.action = app.face.get_action :return_true expect { app.main }.to exit_with 0 end it "should exit 0 when the action returns false" do app.action = app.face.get_action :return_false expect { app.main }.to exit_with 0 end it "should exit 0 when the action returns nil" do app.action = app.face.get_action :return_nil expect { app.main }.to exit_with 0 end it "should exit non-0 when the action raises" do app.action = app.face.get_action :return_raise expect { app.main }.not_to exit_with 0 end - - it "should exit non-0 when the action does not exist" do - app.action = nil - app.arguments = ["foo"] - expect { app.main }.to exit_with 1 - end end describe "#render" do before :each do - app.face = Puppet::Face[:basetest, '0.0.1'] - app.action = app.face.get_action(:foo) + app.face = Puppet::Face[:basetest, '0.0.1'] + app.action = app.face.get_action(:foo) end - ["hello", 1, 1.0].each do |input| - it "should just return a #{input.class.name}" do - app.render(input).should == input + context "default rendering" do + before :each do app.setup end + + ["hello", 1, 1.0].each do |input| + it "should just return a #{input.class.name}" do + app.render(input).should == input + end end - end - [[1, 2], ["one"], [{ 1 => 1 }]].each do |input| - it "should render #{input.class} using the 'pp' library" do - app.render(input).should == input.pretty_inspect + [[1, 2], ["one"], [{ 1 => 1 }]].each do |input| + it "should render #{input.class} using the 'pp' library" do + app.render(input).should == input.pretty_inspect + end end - end - it "should render a non-trivially-keyed Hash with the 'pp' library" do - hash = { [1,2] => 3, [2,3] => 5, [3,4] => 7 } - app.render(hash).should == hash.pretty_inspect - end + it "should render a non-trivially-keyed Hash with the 'pp' library" do + hash = { [1,2] => 3, [2,3] => 5, [3,4] => 7 } + app.render(hash).should == hash.pretty_inspect + end - it "should render a {String,Numeric}-keyed Hash into a table" do - object = Object.new - hash = { "one" => 1, "two" => [], "three" => {}, "four" => object, - 5 => 5, 6.0 => 6 } + it "should render a {String,Numeric}-keyed Hash into a table" do + object = Object.new + hash = { "one" => 1, "two" => [], "three" => {}, "four" => object, + 5 => 5, 6.0 => 6 } - # Gotta love ASCII-betical sort order. Hope your objects are better - # structured for display than my test one is. --daniel 2011-04-18 - app.render(hash).should == < { "1" => '1' * 40, "2" => '2' * 40, '3' => '3' * 40 }, - "text" => { "a" => 'a' * 40, 'b' => 'b' * 40, 'c' => 'c' * 40 } - } - app.render(hash).should == < { "1" => '1' * 40, "2" => '2' * 40, '3' => '3' * 40 }, + "text" => { "a" => 'a' * 40, 'b' => 'b' * 40, 'c' => 'c' * 40 } + } + app.render(hash).should == <"1111111111111111111111111111111111111111", "2"=>"2222222222222222222222222222222222222222", "3"=>"3333333333333333333333333333333333333333"} text {"a"=>"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "b"=>"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "c"=>"cccccccccccccccccccccccccccccccccccccccc"} EOT - end + end - it "should invoke the action rendering hook while rendering" do - app.action.set_rendering_method_for(:for_humans, proc { |value| "bi-winning!" }) - app.action.render_as = :for_humans - app.render("bi-polar?").should == "bi-winning!" - end + it "should invoke the action rendering hook while rendering" do + app.action.set_rendering_method_for(:for_humans, proc { |value| "bi-winning!" }) + app.render("bi-polar?").should == "bi-winning!" + end - it "should render JSON when asked for json" do - app.action.render_as = :json - json = app.render({ :one => 1, :two => 2 }) - json.should =~ /"one":\s*1\b/ - json.should =~ /"two":\s*2\b/ - PSON.parse(json).should == { "one" => 1, "two" => 2 } + it "should render JSON when asked for json" do + app.render_as = :json + json = app.render({ :one => 1, :two => 2 }) + json.should =~ /"one":\s*1\b/ + json.should =~ /"two":\s*2\b/ + PSON.parse(json).should == { "one" => 1, "two" => 2 } + end end it "should fail early if asked to render an invalid format" do app.command_line.stubs(:args).returns %w{--render-as interpretive-dance help help} # We shouldn't get here, thanks to the exception, and our expectation on # it, but this helps us fail if that slips up and all. --daniel 2011-04-27 Puppet::Face[:help, :current].expects(:help).never - # ...and this is just annoying. Thanks, puppet/application.rb. - $stderr.expects(:puts). - with "Could not parse options: I don't know how to render 'interpretive-dance'" + expect { + expect { app.setup; app.run }.to exit_with 1 + }.to print(/I don't know how to render 'interpretive-dance'/) + end - expect { app.run }.to exit_with 1 + it "should work if asked to render a NetworkHandler format" do + app.command_line.stubs(:args).returns %w{facts find dummy --render-as yaml} + expect { app.parse_options; app.setup; app.run }.to exit_with 0 end end end