Signed-off-by: Brice Figureau <brice-pup...@daysofwonder.com>
---
 lib/puppet/application.rb |  233 +++++++++++++++++++++++++++
 spec/unit/application.rb  |  380 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 613 insertions(+), 0 deletions(-)
 create mode 100644 lib/puppet/application.rb
 create mode 100755 spec/unit/application.rb

diff --git a/lib/puppet/application.rb b/lib/puppet/application.rb
new file mode 100644
index 0000000..c650ff7
--- /dev/null
+++ b/lib/puppet/application.rb
@@ -0,0 +1,233 @@
+require 'puppet'
+require 'getoptlong'
+
+# This class handles all the aspects of a Puppet application/executable
+# * setting up options
+# * setting up logs
+# * choosing what to run
+#
+# === Usage
+# The application is a Puppet::Application object that register itself in the 
list
+# of available application. Each application needs a +name+ and a getopt 
+options+
+# description array.
+#
+# The executable uses the application object like this:
+#      Puppet::Application[:example].run
+#
+# 
+# options = [
+#                   [ "--all",      "-a",  GetoptLong::NO_ARGUMENT ],
+#                   [ "--debug",    "-d",  GetoptLong::NO_ARGUMENT ] ]
+# Puppet::Application.new(:example, options) do
+# 
+#     command do
+#         ARGV.shift
+#     end
+# 
+#     option(:all) do
+#         @all = true
+#     end
+#     
+#     command(:read) do
+#         # read action
+#     end
+# 
+#     command(:write) do
+#         # writeaction
+#     end
+# 
+# end
+#
+# === Options
+# When creating a Puppet::Application, the caller should pass an array in the 
GetoptLong 
+# options format to the initializer.
+# This application then parses ARGV and on each found options:
+# * If the option has been defined in the options array:
+#  * If the application defined an option with option(<option-name>) it's 
block executed
+#  * Or, a global options is set either with the argument or true if the 
option doesn't require an argument 
+# * If the option is unknown, and an option(:unknown) was registered then the 
argument is managed by it.
+# * and finally, if none of the above has worked, the option is sent to 
Puppet.settings
+#
+# --help is managed directly by the Puppet::Application class
+#
+# === Setup
+# Applications can use the setup block to perform any initialization.
+# The defaul +setup+ behaviour is to: read Puppet configuration and manage log 
level and destination
+#
+# === What and how to run
+# If the +dispatch+ block is defined it is called. This block should return 
the name of the registered command
+# to be run.
+# If it doesn't exist, it defaults to execute the +main+ command if defined.
+#
+class Puppet::Application
+    include Puppet::Util
+
+    @@applications = {}
+    class << self
+        include Puppet::Util
+    end
+
+    attr_reader :options
+
+    def self.[](name)
+        name = symbolize(name)
+        @@applications[name]
+    end
+
+    def should_parse_config
+        @parse_config = true
+    end
+
+    def should_not_parse_config
+        @parse_config = false
+    end
+
+    def should_parse_config?
+        unless @parse_config.nil?
+            return @parse_config
+        end
+        @parse_config = true
+    end
+
+    # used to declare a new command
+    def command(name, &block)
+        meta_def(symbolize(name), &block)
+    end
+
+    # used to declare code that handle an option
+    def option(name, &block)
+        fname = "handle_#{name}"
+        meta_def(symbolize(fname), &block)
+    end
+
+    # used to declare accessor in a more natural way in the 
+    # various applications
+    def attr_accessor(*args)
+        args.each do |arg|
+            meta_def(arg) do
+                instance_variable_get("@#{arg}".to_sym)
+            end
+            meta_def("#{arg}=") do |value|
+                instance_variable_set("@#{arg}".to_sym, value)
+            end
+        end
+    end
+
+    # used to declare code run instead the default setup
+    def setup(&block)
+        meta_def(:run_setup, &block)
+    end
+
+    # used to declare code to choose which command to run
+    def dispatch(&block)
+        meta_def(:get_command, &block)
+    end
+
+    # used to execute code before running anything else
+    def preinit(&block)
+        meta_def(:run_preinit, &block)
+    end
+
+    def initialize(name, options = [], &block)
+        name = symbolize(name)
+
+        @getopt = options
+
+        setup do 
+            default_setup
+        end
+
+        dispatch do
+            :main
+        end
+
+        # empty by default
+        preinit do
+        end
+
+        @options = {}
+
+        instance_eval(&block) if block_given?
+
+        @@applications[name] = self
+    end
+
+    # This is the main application entry point
+    def run
+        run_preinit
+        parse_options
+        Puppet.parse_config if should_parse_config?
+        run_setup
+        run_command
+    end
+
+    def main
+        raise NotImplementedError, "No valid command or main"
+    end
+
+    def run_command
+        if command = get_command() and respond_to?(command)
+            send(command)
+        else
+            main
+        end
+    end
+
+    def default_setup
+        # Handle the logging settings
+        if options[:debug] or options[:verbose]
+            Puppet::Util::Log.newdestination(:console)
+            if options[:debug]
+                Puppet::Util::Log.level = :debug
+            else
+                Puppet::Util::Log.level = :info
+            end
+        end
+
+        unless options[:setdest]
+            Puppet::Util::Log.newdestination(:syslog)
+        end
+    end
+
+    def parse_options
+        option_names = @getopt.collect { |a| a[0] }
+
+        Puppet.settings.addargs(@getopt)
+        result = GetoptLong.new(*...@getopt)
+
+        begin
+            result.each do |opt, arg|
+                key = opt.gsub(/^--/, '').gsub(/-/,'_').to_sym
+
+                method = "handle_#{key}"
+                if respond_to?(method)
+                    send(method, arg)
+                elsif option_names.include?(opt)
+                    @options[key] = arg || true
+                else
+                    unless respond_to?(:handle_unknown) and 
send(:handle_unknown, opt, arg)
+                        Puppet.settings.handlearg(opt, arg)
+                    end
+                end
+            end
+        rescue GetoptLong::InvalidOption => detail
+            $stderr.puts "Try '#{$0} --help'"
+            exit(1)
+        end
+    end
+
+    # this is used for testing
+    def self.exit(code)
+        exit(code)
+    end
+
+    def handle_help(arg)
+        if Puppet.features.usage?
+            RDoc::usage && exit
+        else
+            puts "No help available unless you have RDoc::usage installed"
+            exit
+        end
+    end
+
+end
\ No newline at end of file
diff --git a/spec/unit/application.rb b/spec/unit/application.rb
new file mode 100755
index 0000000..9eabc53
--- /dev/null
+++ b/spec/unit/application.rb
@@ -0,0 +1,380 @@
+#!/usr/bin/env ruby
+
+require File.dirname(__FILE__) + '/../spec_helper'
+
+require 'puppet/application'
+require 'puppet'
+require 'getoptlong'
+
+describe Puppet::Application do
+
+    before :each do
+        @app = Puppet::Application.new(:test)
+    end
+
+    it "should have a run entry-point" do
+        @app.should respond_to(:run)
+    end
+
+    it "should have a read accessor to options" do
+        @app.should respond_to(:options)
+    end
+
+    it "should create a default run_setup method" do
+        @app.should respond_to(:run_setup)
+    end
+
+    it "should create a default run_preinit method" do
+        @app.should respond_to(:run_preinit)
+    end
+
+    it "should create a default get_command method" do
+        @app.should respond_to(:get_command)
+    end
+
+    it "should return :main as default get_command" do
+        @app.get_command.should == :main
+    end
+
+    describe "when parsing command-line options" do
+
+        before :each do
+            @argv_bak = ARGV.dup
+            ARGV.clear
+
+            Puppet.settings.stubs(:addargs)
+            @opt = stub 'opt', :each => nil
+            GetoptLong.stubs(:new).returns(@opt)
+        end
+
+        after :each do
+            ARGV.clear
+            ARGV << @argv_bak
+        end
+
+        it "should give options to Puppet.settings.addargs" do
+            options = []
+
+            Puppet.settings.expects(:addargs).with(options)
+
+            Puppet::Application.new(:test, options).parse_options
+        end
+
+        it "should scan command line arguments with Getopt" do
+            options = []
+
+            GetoptLong.expects(:new).returns(stub_everything)
+
+            Puppet::Application.new(:test, options).parse_options
+        end
+
+        it "should loop around one argument given on command line" do
+            options = [[ "--one", "-o", GetoptLong::NO_ARGUMENT ]]
+            ARGV << [ "--one" ]
+            Puppet.settings.stubs(:handlearg)
+
+            @opt.expects(:each).yields("--one", nil)
+
+            Puppet::Application.new(:test, options).parse_options
+        end
+
+        it "should loop around all arguments given on command line" do
+            options = [ [ "--one", "-o", GetoptLong::NO_ARGUMENT ],
+                        [ "--two", "-t", GetoptLong::NO_ARGUMENT ]
+                        ]
+            ARGV << [ "--one", "--two" ]
+            Puppet.settings.stubs(:handlearg)
+
+            @opt.expects(:each).multiple_yields(["--one", nil],["--two", nil])
+
+            Puppet::Application.new(:test, options).parse_options
+        end
+
+        it "should call the method named handle_<option> if it exists" do
+            options = [[ "--name", "-n", GetoptLong::NO_ARGUMENT ]]
+            ARGV << [ "--name" ]
+            @opt.stubs(:each).yields("--name", nil)
+
+            app = Puppet::Application.new(:test, options)
+            app.stubs(:respond_to).with(:handle_name).returns(true)
+
+            app.expects(:handle_name)
+
+            app.parse_options
+        end
+
+        it "should handle gracefully options containing '-'" do
+            options = [[ "--name-with-dash", "-n", GetoptLong::NO_ARGUMENT ]]
+            ARGV << [ "--name-with-dash" ]
+            @opt.stubs(:each).yields("--name-with-dash", nil)
+
+            app = Puppet::Application.new(:test, options)
+            app.stubs(:respond_to).with(:handle_name_with_dash).returns(true)
+
+            app.expects(:handle_name_with_dash)
+
+            app.parse_options
+        end
+
+        it "should call the method named handle_<option> if it exists and pass 
argument" do
+            options = [[ "--name", "-n", GetoptLong::REQUIRED_ARGUMENT ]]
+            ARGV << [ "--name" ]
+            arg = stub 'arg'
+            @opt.stubs(:each).yields("--name", arg)
+
+            app = Puppet::Application.new(:test, options)
+            app.stubs(:respond_to).with(:name).returns(true)
+
+            app.expects(:handle_name).with(arg)
+
+            app.parse_options
+        end
+
+        describe "with 'no argument' options" do
+            it "should store true in Application.options if present and no 
code blocks" do
+                options = [[ "--one", "-o", GetoptLong::NO_ARGUMENT ]]
+                ARGV << [ "--one" ]
+                @opt.stubs(:each).yields("--one", nil)
+
+                app = Puppet::Application.new(:test, options)
+                app.options.expects(:[]=).with(:one, true)
+
+                app.parse_options
+            end
+        end
+
+        describe "with options with an argument" do
+            it "should store the argument value in Application.options if 
present and no code blocks" do
+                options = [[ "--one", "-o", GetoptLong::REQUIRED_ARGUMENT ]]
+                argument = stub 'arg'
+                ARGV << [ "--one" ]
+                @opt.stubs(:each).yields("--one", argument)
+
+                app = Puppet::Application.new(:test, options)
+                app.options.expects(:[]=).with(:one, argument)
+
+                app.parse_options
+            end
+        end
+
+        describe "when using --help" do
+            confine "requires RDoc" => Puppet.features.usage?
+
+            it "should call RDoc::usage and exit" do
+                options = [[ "--help", "-h", GetoptLong::REQUIRED_ARGUMENT ]]
+                ARGV << [ "--help" ]
+                @opt.stubs(:each).yields("--help", nil)
+                app = Puppet::Application.new(:test, options)
+
+                app.expects(:exit)
+                RDoc.expects(:usage).returns(true)
+
+                app.parse_options
+            end
+
+        end
+
+        it "should pass unknown arguments to handle_unknown if it is defined" 
do
+            options = []
+            ARGV << [ "--not-handled" ]
+            @opt.stubs(:each).yields("--not-handled", nil)
+            app = Puppet::Application.new(:test, options)
+
+            app.expects(:handle_unknown).with("--not-handled", 
nil).returns(true)
+
+            app.parse_options
+        end
+
+        it "should pass back not directly or by handle_unknown handled 
arguments to Puppet.settings" do
+            options = []
+            ARGV << [ "--topuppet" ]
+            @opt.stubs(:each).yields("--topuppet", nil)
+            app = Puppet::Application.new(:test, options)
+            app.stubs(:handle_unknown).with("--topuppet", nil).returns(false)
+
+            Puppet.settings.expects(:handlearg).with("--topuppet", nil)
+
+            app.parse_options
+        end
+
+        it "should pass back unknown arguments to Puppet.settings if no 
handle_unknown method exists" do
+            options = []
+            ARGV << [ "--topuppet" ]
+            @opt.stubs(:each).yields("--topuppet", nil)
+            app = Puppet::Application.new(:test, options)
+
+            Puppet.settings.expects(:handlearg).with("--topuppet", nil)
+
+            app.parse_options
+        end
+
+        it "should exit if getopt raise an error" do
+            options = [[ "--pouet", "-p", GetoptLong::REQUIRED_ARGUMENT ]]
+            ARGV << [ "--do-not-exist" ]
+            @opt.stubs(:each).raises(GetoptLong::InvalidOption.new)
+            $stderr.stubs(:puts)
+            app = Puppet::Application.new(:test, options)
+
+            app.expects(:exit)
+
+            lambda { app.parse_options }.should_not raise_error
+        end
+
+    end
+
+    describe "when calling default setup" do
+
+        before :each do
+            @app = Puppet::Application.new(:test)
+            @app.stubs(:should_parse_config?).returns(false)
+            @app.options.stubs(:[])
+        end
+
+        [ :debug, :verbose ].each do |level|
+            it "should honor option #{level}" do
+                @app.options.stubs(:[]).with(level).returns(true)
+                Puppet::Util::Log.stubs(:newdestination)
+
+                Puppet::Util::Log.expects(:level=).with(level == :verbose ? 
:info : :debug)
+
+                @app.run_setup
+            end
+        end
+
+        it "should honor setdest option" do
+            @app.options.stubs(:[]).with(:setdest).returns(false)
+
+            Puppet::Util::Log.expects(:newdestination).with(:syslog)
+
+            @app.run_setup
+        end
+
+    end
+
+    describe "when running" do
+
+        before :each do
+            @app = Puppet::Application.new(:test)
+            @app.stubs(:run_preinit)
+            @app.stubs(:run_setup)
+            @app.stubs(:parse_options)
+        end
+
+        it "should call run_preinit" do
+            @app.stubs(:run_command)
+
+            @app.expects(:run_preinit)
+
+            @app.run
+        end
+
+        it "should call parse_options" do
+            @app.stubs(:run_command)
+
+            @app.expects(:parse_options)
+
+            @app.run
+        end
+
+        it "should call run_command" do
+
+            @app.expects(:run_command)
+
+            @app.run
+        end
+
+        it "should parse Puppet configuration if should_parse_config is 
called" do
+            @app.stubs(:run_command)
+            @app.should_parse_config
+
+            Puppet.expects(:parse_config)
+
+            @app.run
+        end
+
+        it "should not parse_option if should_not_parse_config is called" do
+            @app.stubs(:run_command)
+            @app.should_not_parse_config
+
+            Puppet.expects(:parse_config).never
+
+            @app.run
+        end
+
+        it "should parse Puppet configuration if needed" do
+            @app.stubs(:run_command)
+            @app.stubs(:should_parse_config?).returns(true)
+
+            Puppet.expects(:parse_config)
+
+            @app.run
+        end
+
+        it "should call the action matching what returned command" do
+            @app.stubs(:get_command).returns(:backup)
+            @app.stubs(:respond_to?).with(:backup).returns(true)
+
+            @app.expects(:backup)
+
+            @app.run
+        end
+
+        it "should call main as the default command" do
+            @app.expects(:main)
+
+            @app.run
+        end
+
+        it "should raise an error if no command can be called" do
+            lambda { @app.run }.should raise_error(NotImplementedError)
+        end
+
+        it "should raise an error if dispatch returns no command" do
+            @app.stubs(:get_command).returns(nil)
+
+            lambda { @app.run }.should raise_error(NotImplementedError)
+        end
+
+        it "should raise an error if dispatch returns an invalid command" do
+            @app.stubs(:get_command).returns(:this_function_doesnt_exist)
+
+            lambda { @app.run }.should raise_error(NotImplementedError)
+        end
+
+    end
+
+    describe "when metaprogramming" do
+
+        before :each do
+            @app = Puppet::Application.new(:test)
+        end
+
+        it "should create a new method with newcommand" do
+            @app.command(:test) do
+            end
+
+            @app.should respond_to(:test)
+        end
+
+        it "should create a new method with newoption" do
+            @app.option(:test) do
+            end
+
+            @app.should respond_to(:handle_test)
+        end
+
+        it "should create a method called run_setup with setup" do
+            @app.setup do
+            end
+
+            @app.should respond_to(:run_setup)
+        end
+
+        it "should create a method called get_command with dispatch" do
+            @app.dispatch do
+            end
+
+            @app.should respond_to(:get_command)
+        end
+    end
+end
\ No newline at end of file
-- 
1.6.0.2


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Puppet Developers" group.
To post to this group, send email to puppet-dev@googlegroups.com
To unsubscribe from this group, send email to 
puppet-dev+unsubscr...@googlegroups.com
For more options, visit this group at 
http://groups.google.com/group/puppet-dev?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to