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 -~----------~----~----~----~------~----~------~--~---