diff --git a/lib/puppet/application/face_base.rb b/lib/puppet/application/face_base.rb index 2a048a532..df828b5a2 100644 --- a/lib/puppet/application/face_base.rb +++ b/lib/puppet/application/face_base.rb @@ -1,158 +1,166 @@ require 'puppet/application' require 'puppet/face' require 'optparse' 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("--format FORMAT") do |arg| @format = arg.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, :format attr_writer :exit_code # This allows you to set the exit code if you don't want to just exit # immediately but you need to indicate a failure. def exit_code @exit_code || 0 end # Override this if you need custom rendering. def render(result) render_method = Puppet::Network::FormatHandler.format(format).render_method if render_method == "to_pson" jj result - exit(0) else result.send(render_method) end 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] @format = @face.default_format # 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 else raise OptionParser::InvalidOption.new(item.sub(/=.*$/, '')) end else - action = @face.get_action(item.to_sym) - if action.nil? then - raise OptionParser::InvalidArgument.new("#{@face} does not have an #{item} action") - end - @action = action + @action = @face.get_action(item.to_sym) end end - unless @action - raise OptionParser::MissingArgument.new("No action given on the command line") + if @action.nil? + @action = @face.get_default_action() + @is_default_action = true 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 + end if @action # ...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 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 - @arguments.delete_at(0) + # 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 end def main # Call the method associated with the provided action (e.g., 'find'). - if result = @face.send(@action.name, *arguments) - puts render(result) + if @action + result = @face.send(@action.name, *arguments) + puts render(result) if result + 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) end exit(exit_code) end end diff --git a/lib/puppet/face/help.rb b/lib/puppet/face/help.rb index 1c2da9e83..8bd495f8a 100644 --- a/lib/puppet/face/help.rb +++ b/lib/puppet/face/help.rb @@ -1,104 +1,105 @@ require 'puppet/face' require 'puppet/util/command_line' require 'pathname' require 'erb' Puppet::Face.define(:help, '0.0.1') do summary "Displays help about puppet subcommands" action(:help) do summary "Display help about faces and their actions." option "--version VERSION" do desc "Which version of the interface to show help for" end + default when_invoked do |*args| # Check our invocation, because we want varargs and can't do defaults # yet. REVISIT: when we do option defaults, and positional options, we # should rewrite this to use those. --daniel 2011-04-04 options = args.pop if options.nil? or args.length > 2 then raise ArgumentError, "help only takes two (optional) arguments, a face name, and an action" end version = :current if options.has_key? :version then if options[:version].to_s !~ /^current$/i then version = options[:version] else if args.length == 0 then raise ArgumentError, "version only makes sense when a face is given" end end end # Name those parameters... facename, actionname = args if facename then if legacy_applications.include? facename then actionname and raise ArgumentError, "Legacy subcommands don't take actions" return Puppet::Application[facename].help else face = Puppet::Face[facename.to_sym, version] actionname and action = face.get_action(actionname.to_sym) end end case args.length when 0 then template = erb 'global.erb' when 1 then face or fail ArgumentError, "Unable to load face #{facename}" template = erb 'face.erb' when 2 then face or fail ArgumentError, "Unable to load face #{facename}" action or fail ArgumentError, "Unable to load action #{actionname} from #{face}" template = erb 'action.erb' else fail ArgumentError, "Too many arguments to help action" end # Run the ERB template in our current binding, including all the local # variables we established just above. --daniel 2011-04-11 return template.result(binding) end end def erb(name) template = (Pathname(__FILE__).dirname + "help" + name) erb = ERB.new(template.read, nil, '%') erb.filename = template.to_s return erb end def legacy_applications # The list of applications, less those that are duplicated as a face. Puppet::Util::CommandLine.available_subcommands.reject do |appname| Puppet::Face.face? appname.to_sym, :current or # ...this is a nasty way to exclude non-applications. :( %w{face_base indirection_base}.include? appname end.sort end def horribly_extract_summary_from(appname) begin require "puppet/application/#{appname}" help = Puppet::Application[appname].help.split("\n") # Now we find the line with our summary, extract it, and return it. This # depends on the implementation coincidence of how our pages are # formatted. If we can't match the pattern we expect we return the empty # string to ensure we don't blow up in the summary. --daniel 2011-04-11 while line = help.shift do if md = /^puppet-#{appname}\([^\)]+\) -- (.*)$/.match(line) then return md[1] end end rescue Exception # Damn, but I hate this: we just ignore errors here, no matter what # class they are. Meh. end return '' end end diff --git a/lib/puppet/interface/action.rb b/lib/puppet/interface/action.rb index db338e39e..3c18c2aaf 100644 --- a/lib/puppet/interface/action.rb +++ b/lib/puppet/interface/action.rb @@ -1,129 +1,129 @@ # -*- coding: utf-8 -*- require 'puppet/interface' require 'puppet/interface/option' class Puppet::Interface::Action def initialize(face, name, attrs = {}) raise "#{name.inspect} is an invalid action name" unless name.to_s =~ /^[a-z]\w*$/ @face = face @name = name.to_sym @options = {} attrs.each do |k, v| send("#{k}=", v) end end # This is not nice, but it is the easiest way to make us behave like the # Ruby Method object rather than UnboundMethod. Duplication is vaguely # annoying, but at least we are a shallow clone. --daniel 2011-04-12 def __dup_and_rebind_to(to) bound_version = self.dup bound_version.instance_variable_set(:@face, to) return bound_version end attr_reader :name def to_s() "#{@face}##{@name}" end - attr_accessor :summary + attr_accessor :default, :summary # Initially, this was defined to allow the @action.invoke pattern, which is # a very natural way to invoke behaviour given our introspection # capabilities. Heck, our initial plan was to have the faces delegate to # the action object for invocation and all. # # It turns out that we have a binding problem to solve: @face was bound to # the parent class, not the subclass instance, and we don't pass the # appropriate context or change the binding enough to make this work. # # We could hack around it, by either mandating that you pass the context in # to invoke, or try to get the binding right, but that has probably got # subtleties that we don't instantly think of – especially around threads. # # So, we are pulling this method for now, and will return it to life when we # have the time to resolve the problem. For now, you should replace... # # @action = @face.get_action(name) # @action.invoke(arg1, arg2, arg3) # # ...with... # # @action = @face.get_action(name) # @face.send(@action.name, arg1, arg2, arg3) # # I understand that is somewhat cumbersome, but it functions as desired. # --daniel 2011-03-31 # # PS: This code is left present, but commented, to support this chunk of # documentation, for the benefit of the reader. # # def invoke(*args, &block) # @face.send(name, *args, &block) # end def when_invoked=(block) # We need to build an instance method as a wrapper, using normal code, to # be able to expose argument defaulting between the caller and definer in # the Ruby API. An extra method is, sadly, required for Ruby 1.8 to work. # # In future this also gives us a place to hook in additional behaviour # such as calling out to the action instance to validate and coerce # parameters, which avoids any exciting context switching and all. # # Hopefully we can improve this when we finally shuffle off the last of # Ruby 1.8 support, but that looks to be a few "enterprise" release eras # away, so we are pretty stuck with this for now. # # Patches to make this work more nicely with Ruby 1.9 using runtime # version checking and all are welcome, but they can't actually help if # the results are not totally hidden away in here. # # Incidentally, we though about vendoring evil-ruby and actually adjusting # the internal C structure implementation details under the hood to make # this stuff work, because it would have been cleaner. Which gives you an # idea how motivated we were to make this cleaner. Sorry. --daniel 2011-03-31 internal_name = "#{@name} implementation, required on Ruby 1.8".to_sym file = __FILE__ + "+eval" line = __LINE__ + 1 wrapper = "def #{@name}(*args, &block) args << {} unless args.last.is_a? Hash args << block if block_given? self.__send__(#{internal_name.inspect}, *args) end" if @face.is_a?(Class) @face.class_eval do eval wrapper, nil, file, line end @face.define_method(internal_name, &block) else @face.instance_eval do eval wrapper, nil, file, line end @face.meta_def(internal_name, &block) end end def add_option(option) option.aliases.each do |name| if conflict = get_option(name) then raise ArgumentError, "Option #{option} conflicts with existing option #{conflict}" elsif conflict = @face.get_option(name) then raise ArgumentError, "Option #{option} conflicts with existing option #{conflict} on #{@face}" end end option.aliases.each do |name| @options[name] = option end option end def option?(name) @options.include? name.to_sym end def options (@options.keys + @face.options).sort end def get_option(name) @options[name.to_sym] || @face.get_option(name) end end diff --git a/lib/puppet/interface/action_builder.rb b/lib/puppet/interface/action_builder.rb index 34bb3fa44..639d8fc7f 100644 --- a/lib/puppet/interface/action_builder.rb +++ b/lib/puppet/interface/action_builder.rb @@ -1,35 +1,39 @@ require 'puppet/interface' require 'puppet/interface/action' class Puppet::Interface::ActionBuilder attr_reader :action def self.build(face, name, &block) raise "Action #{name.inspect} must specify a block" unless block new(face, name, &block).action end private def initialize(face, name, &block) @face = face @action = Puppet::Interface::Action.new(face, name) instance_eval(&block) end # Ideally the method we're defining here would be added to the action, and a # method on the face would defer to it, but we can't get scope correct, so # we stick with this. --daniel 2011-03-24 def when_invoked(&block) raise "when_invoked on an ActionBuilder with no corresponding Action" unless @action @action.when_invoked = block end def option(*declaration, &block) option = Puppet::Interface::OptionBuilder.build(@action, *declaration, &block) @action.add_option(option) end + def default + @action.default = true + end + def summary(text) @action.summary = text end end diff --git a/lib/puppet/interface/action_manager.rb b/lib/puppet/interface/action_manager.rb index d75697afa..440be2d1b 100644 --- a/lib/puppet/interface/action_manager.rb +++ b/lib/puppet/interface/action_manager.rb @@ -1,56 +1,65 @@ require 'puppet/interface/action_builder' module Puppet::Interface::ActionManager # Declare that this app can take a specific action, and provide # the code to do so. def action(name, &block) @actions ||= {} + @default_action ||= nil raise "Action #{name} already defined for #{self}" if action?(name) action = Puppet::Interface::ActionBuilder.build(self, name, &block) + if action.default + raise "Actions #{@default_action.name} and #{name} cannot both be default" if @default_action + @default_action = action + end @actions[action.name] = action end # This is the short-form of an action definition; it doesn't use the # builder, just creates the action directly from the block. def script(name, &block) @actions ||= {} raise "Action #{name} already defined for #{self}" if action?(name) @actions[name] = Puppet::Interface::Action.new(self, name, :when_invoked => block) end def actions @actions ||= {} result = @actions.keys if self.is_a?(Class) and superclass.respond_to?(:actions) result += superclass.actions elsif self.class.respond_to?(:actions) result += self.class.actions end result.sort end def get_action(name) @actions ||= {} result = @actions[name.to_sym] if result.nil? if self.is_a?(Class) and superclass.respond_to?(:get_action) found = superclass.get_action(name) elsif self.class.respond_to?(:get_action) found = self.class.get_action(name) end if found then # This is not the nicest way to make action equivalent to the Ruby # Method object, rather than UnboundMethod, but it will do for now, # and we only have to make this change in *one* place. --daniel 2011-04-12 result = @actions[name.to_sym] = found.__dup_and_rebind_to(self) end end return result end + def get_default_action + @default_action + end + def action?(name) actions.include?(name.to_sym) end end diff --git a/spec/unit/application/face_base_spec.rb b/spec/unit/application/face_base_spec.rb index 939712ef8..eaf60b434 100755 --- a/spec/unit/application/face_base_spec.rb +++ b/spec/unit/application/face_base_spec.rb @@ -1,187 +1,218 @@ #!/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 before :all do Puppet::Face.define(:basetest, '0.0.1') do option("--[no-]boolean") option("--mandatory MANDATORY") option("--optional [OPTIONAL]") action :foo do option("--action") when_invoked { |*args| args.length } end end end 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 "parsing the command line" do context "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 set the format based on the face default" do app.format.should == :pson end it "should find the action" do app.action.should be app.action.name.should == :foo end end - it "should fail if no action is given" do - expect { app.preinit; app.parse_options }. - to raise_error OptionParser::MissingArgument, /No action given/ + it "should use the default action if not given any arguments" do + app.command_line.stubs(:args).returns [] + action = stub(:options => []) + 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 => []) + 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', { } ] 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 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.expects(:exit).with(0) app.face = Puppet::Face[:basetest, '0.0.1'] app.action = app.face.get_action(:foo) app.format = :pson app.arguments = ["myname", "myarg"] end it "should send the specified verb and name to the face" do app.face.expects(:foo).with(*app.arguments) app.main 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) + app.stubs(:puts) # meh. Don't print nil, thanks. --daniel 2011-04-12 + app.main + end + it "should use its render method to render any result" do app.expects(:render).with(app.arguments.length + 1) app.stubs(:puts) # meh. Don't print nil, thanks. --daniel 2011-04-12 app.main end end end diff --git a/spec/unit/interface/action_builder_spec.rb b/spec/unit/interface/action_builder_spec.rb index 5b04df900..8f29c8a7b 100755 --- a/spec/unit/interface/action_builder_spec.rb +++ b/spec/unit/interface/action_builder_spec.rb @@ -1,69 +1,80 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/interface/action_builder' describe Puppet::Interface::ActionBuilder do describe "::build" do it "should build an action" do action = Puppet::Interface::ActionBuilder.build(nil, :foo) do end action.should be_a(Puppet::Interface::Action) action.name.should == :foo end it "should define a method on the face which invokes the action" do face = Puppet::Interface.new(:action_builder_test_interface, '0.0.1') action = Puppet::Interface::ActionBuilder.build(face, :foo) do when_invoked do "invoked the method" end end face.foo.should == "invoked the method" end it "should require a block" do expect { Puppet::Interface::ActionBuilder.build(nil, :foo) }. should raise_error("Action :foo must specify a block") end describe "when handling options" do let :face do Puppet::Interface.new(:option_handling, '0.0.1') end it "should have a #option DSL function" do method = nil Puppet::Interface::ActionBuilder.build(face, :foo) do method = self.method(:option) end method.should be end it "should define an option without a block" do action = Puppet::Interface::ActionBuilder.build(face, :foo) do option "--bar" end action.should be_option :bar end it "should accept an empty block" do action = Puppet::Interface::ActionBuilder.build(face, :foo) do option "--bar" do # This space left deliberately blank. end end action.should be_option :bar end end context "inline documentation" do let :face do Puppet::Interface.new(:inline_action_docs, '0.0.1') end it "should set the summary" do action = Puppet::Interface::ActionBuilder.build(face, :foo) do summary "this is some text" end action.summary.should == "this is some text" end end + + context "action defaulting" do + let :face do Puppet::Interface.new(:default_action, '0.0.1') end + + it "should set the default to true" do + action = Puppet::Interface::ActionBuilder.build(face, :foo) do + default + end + action.default.should == true + end + end end end diff --git a/spec/unit/interface/action_manager_spec.rb b/spec/unit/interface/action_manager_spec.rb index c4b21eaac..387faa043 100755 --- a/spec/unit/interface/action_manager_spec.rb +++ b/spec/unit/interface/action_manager_spec.rb @@ -1,232 +1,250 @@ #!/usr/bin/env rspec require 'spec_helper' # This is entirely an internal class for Interface, so we have to load it instead of our class. require 'puppet/interface' +require 'puppet/face' class ActionManagerTester include Puppet::Interface::ActionManager end describe Puppet::Interface::ActionManager do subject { ActionManagerTester.new } describe "when included in a class" do it "should be able to define an action" do subject.action(:foo) do when_invoked { "something "} end end it "should be able to define a 'script' style action" do subject.script :bar do "a bar is where beer is found" end end it "should be able to list defined actions" do subject.action(:foo) do when_invoked { "something" } end subject.action(:bar) do when_invoked { "something" } end subject.actions.should =~ [:foo, :bar] end it "should list 'script' actions" do subject.script :foo do "foo" end subject.actions.should =~ [:foo] end it "should list both script and normal actions" do subject.action :foo do when_invoked do "foo" end end subject.script :bar do "a bar is where beer is found" end subject.actions.should =~ [:foo, :bar] end it "should be able to indicate when an action is defined" do subject.action(:foo) do when_invoked { "something" } end subject.should be_action(:foo) end it "should indicate an action is defined for script actions" do subject.script :foo do "foo" end subject.should be_action :foo end it "should correctly treat action names specified as strings" do subject.action(:foo) do when_invoked { "something" } end subject.should be_action("foo") end end describe "when used to extend a class" do subject { Class.new.extend(Puppet::Interface::ActionManager) } it "should be able to define an action" do subject.action(:foo) do when_invoked { "something "} end end it "should be able to list defined actions" do subject.action(:foo) do when_invoked { "something" } end subject.action(:bar) do when_invoked { "something" } end subject.actions.should include(:bar) subject.actions.should include(:foo) end it "should be able to indicate when an action is defined" do subject.action(:foo) { "something" } subject.should be_action(:foo) end end describe "when used both at the class and instance level" do before do @klass = Class.new do include Puppet::Interface::ActionManager extend Puppet::Interface::ActionManager end @instance = @klass.new end it "should be able to define an action at the class level" do @klass.action(:foo) do when_invoked { "something "} end end it "should create an instance method when an action is defined at the class level" do @klass.action(:foo) do when_invoked { "something" } end @instance.foo.should == "something" end it "should be able to define an action at the instance level" do @instance.action(:foo) do when_invoked { "something "} end end it "should create an instance method when an action is defined at the instance level" do @instance.action(:foo) do when_invoked { "something" } end @instance.foo.should == "something" end it "should be able to list actions defined at the class level" do @klass.action(:foo) do when_invoked { "something" } end @klass.action(:bar) do when_invoked { "something" } end @klass.actions.should include(:bar) @klass.actions.should include(:foo) end it "should be able to list actions defined at the instance level" do @instance.action(:foo) do when_invoked { "something" } end @instance.action(:bar) do when_invoked { "something" } end @instance.actions.should include(:bar) @instance.actions.should include(:foo) end it "should be able to list actions defined at both instance and class level" do @klass.action(:foo) do when_invoked { "something" } end @instance.action(:bar) do when_invoked { "something" } end @instance.actions.should include(:bar) @instance.actions.should include(:foo) end it "should be able to indicate when an action is defined at the class level" do @klass.action(:foo) do when_invoked { "something" } end @instance.should be_action(:foo) end it "should be able to indicate when an action is defined at the instance level" do @klass.action(:foo) do when_invoked { "something" } end @instance.should be_action(:foo) end it "should list actions defined in superclasses" do @subclass = Class.new(@klass) @instance = @subclass.new @klass.action(:parent) do when_invoked { "a" } end @subclass.action(:sub) do when_invoked { "a" } end @instance.action(:instance) do when_invoked { "a" } end @instance.should be_action(:parent) @instance.should be_action(:sub) @instance.should be_action(:instance) end it "should create an instance method when an action is defined in a superclass" do @subclass = Class.new(@klass) @instance = @subclass.new @klass.action(:foo) do when_invoked { "something" } end @instance.foo.should == "something" end end + describe "#action" do + it 'should add an action' do + subject.action(:foo) { } + subject.get_action(:foo).should be_a Puppet::Interface::Action + end + + it 'should support default actions' do + subject.action(:foo) { default } + subject.get_default_action.should == subject.get_action(:foo) + end + + it 'should not support more than one default action' do + subject.action(:foo) { default } + expect { subject.action(:bar) { default } }.should raise_error + end + end + describe "#get_action" do let :parent_class do parent_class = Class.new(Puppet::Interface) parent_class.action(:foo) {} parent_class end it "should check that we can find inherited actions when we are a class" do Class.new(parent_class).get_action(:foo).name.should == :foo end it "should check that we can find inherited actions when we are an instance" do instance = parent_class.new(:foo, '0.0.0') instance.get_action(:foo).name.should == :foo end end end