diff --git a/lib/puppet/application/indirection_base.rb b/lib/puppet/application/indirection_base.rb index da61f408d..61cfb435e 100644 --- a/lib/puppet/application/indirection_base.rb +++ b/lib/puppet/application/indirection_base.rb @@ -1,19 +1,16 @@ require 'puppet/application/string_base' class Puppet::Application::IndirectionBase < Puppet::Application::StringBase - option("--terminus TERMINUS") do |arg| - @terminus = arg - end - attr_accessor :terminus, :indirection def setup super + # REVISIT: need to implement this in terms of the string options, eh. if string.respond_to?(:indirection) raise "Could not find data type #{type} for application #{self.class.name}" unless string.indirection string.set_terminus(terminus) if terminus end end end diff --git a/lib/puppet/application/string_base.rb b/lib/puppet/application/string_base.rb index 762fbfda8..ffd49e8c0 100644 --- a/lib/puppet/application/string_base.rb +++ b/lib/puppet/application/string_base.rb @@ -1,94 +1,117 @@ require 'puppet/application' require 'puppet/string' class Puppet::Application::StringBase < Puppet::Application should_parse_config run_mode :agent def preinit super trap(:INT) do $stderr.puts "Cancelling String" exit(0) end end 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 :string, :type, :verb, :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 def main # Call the method associated with the provided action (e.g., 'find'). if result = string.send(verb, *arguments) puts render(result) end exit(exit_code) 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 + # 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. + # + # This requires a partial parse first, and removing the options that we + # understood, then identification of the next item, then another round of + # the same until we have the string and action all set. --daniel 2011-03-29 + # + # NOTE: We can't use the Puppet::Application implementation of option + # parsing because it is (*ahem*) going to puts on $stderr and exit when it + # hits a parse problem, not actually let us reuse stuff. --daniel 2011-03-29 + + # TODO: 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 + @string = Puppet::String[@type, :current] + @format = @string.default_format + + # Now, collect the global and string options and parse the command line. + begin + @string.options.inject OptionParser.new do |options, option| + option = @string.get_option option # turn it into the object, bleh + options.on(*option.to_optparse) do |value| + puts "REVISIT: do something with #{value.inspect}" + end + end.parse! command_line.args.dup + rescue OptionParser::InvalidOption => e + puts e.inspect # ...and ignore?? + end + + fail "REVISIT: Finish this code, eh..." + end + def setup Puppet::Util::Log.newdestination :console # 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 @verb = command_line.args.shift @arguments = Array(command_line.args) << options - - @type = self.class.name.to_s.sub(/.+:/, '').downcase.to_sym - - # TODO: These should be configurable versions. - unless Puppet::String.string?(@type, :current) - raise "Could not find any version of string '#{@type}'" - end - @string = Puppet::String[@type, :current] - @format ||= @string.default_format - validate end def validate unless verb raise "You must specify #{string.actions.join(", ")} as a verb; 'save' probably does not work right now" end unless string.action?(verb) raise "Command '#{verb}' not found for #{type}" end end end diff --git a/lib/puppet/string/action.rb b/lib/puppet/string/action.rb index 4219aca0a..692e467b4 100644 --- a/lib/puppet/string/action.rb +++ b/lib/puppet/string/action.rb @@ -1,52 +1,58 @@ require 'puppet/string' require 'puppet/string/option' class Puppet::String::Action attr_reader :name def to_s "#{@string}##{@name}" end def initialize(string, name, attrs = {}) raise "#{name.inspect} is an invalid action name" unless name.to_s =~ /^[a-z]\w*$/ @string = string @name = name.to_sym @options = {} attrs.each do |k,v| send("#{k}=", v) end end def invoke(*args, &block) @string.method(name).call(*args,&block) end def invoke=(block) if @string.is_a?(Class) @string.define_method(@name, &block) else @string.meta_def(@name, &block) end end def add_option(option) - if option? option.name then - raise ArgumentError, "#{option.name} duplicates an existing option on #{self}" - elsif @string.option? option.name then - raise ArgumentError, "#{option.name} duplicates an existing option on #{@string}" + option.aliases.each do |name| + if conflict = get_option(name) then + raise ArgumentError, "Option #{option} conflicts with existing option #{conflict}" + elsif conflict = @string.get_option(name) then + raise ArgumentError, "Option #{option} conflicts with existing option #{conflict} on #{@string}" + end end - @options[option.name] = option + option.aliases.each do |name| + @options[name] = option + end + + option end def option?(name) @options.include? name.to_sym end def options (@options.keys + @string.options).sort end def get_option(name) @options[name.to_sym] || @string.get_option(name) end end diff --git a/lib/puppet/string/action_builder.rb b/lib/puppet/string/action_builder.rb index fb2a749ae..e76044470 100644 --- a/lib/puppet/string/action_builder.rb +++ b/lib/puppet/string/action_builder.rb @@ -1,30 +1,30 @@ require 'puppet/string' require 'puppet/string/action' class Puppet::String::ActionBuilder attr_reader :action def self.build(string, name, &block) raise "Action #{name.inspect} must specify a block" unless block new(string, name, &block).action end def initialize(string, name, &block) @string = string @action = Puppet::String::Action.new(string, name) instance_eval(&block) end # Ideally the method we're defining here would be added to the action, and a # method on the string would defer to it, but we can't get scope correct, # so we stick with this. --daniel 2011-03-24 def invoke(&block) raise "Invoke called on an ActionBuilder with no corresponding Action" unless @action @action.invoke = block end - def option(name, attrs = {}, &block) - option = Puppet::String::OptionBuilder.build(@action, name, attrs, &block) + def option(*declaration, &block) + option = Puppet::String::OptionBuilder.build(@action, *declaration, &block) @action.add_option(option) end end diff --git a/lib/puppet/string/indirector.rb b/lib/puppet/string/indirector.rb index 48ec32680..71b1f3b12 100644 --- a/lib/puppet/string/indirector.rb +++ b/lib/puppet/string/indirector.rb @@ -1,79 +1,85 @@ require 'puppet' require 'puppet/string' class Puppet::String::Indirector < Puppet::String + warn "REVISIT: Need to redefine this to take arguments again, eh." + option "--terminus TERMINUS" do + desc "REVISIT: You can select a terminus, which has some bigger effect +that we should describe in this file somehow." + end + def self.indirections Puppet::Indirector::Indirection.instances.collect { |t| t.to_s }.sort end def self.terminus_classes(indirection) Puppet::Indirector::Terminus.terminus_classes(indirection.to_sym).collect { |t| t.to_s }.sort end action :destroy do invoke { |*args| call_indirection_method(:destroy, *args) } end action :find do invoke { |*args| call_indirection_method(:find, *args) } end action :save do invoke { |*args| call_indirection_method(:save, *args) } end action :search do invoke { |*args| call_indirection_method(:search, *args) } end # Print the configuration for the current terminus class action :info do invoke do |*args| if t = indirection.terminus_class puts "Run mode '#{Puppet.run_mode.name}': #{t}" else $stderr.puts "No default terminus class for run mode '#{Puppet.run_mode.name}'" end end end attr_accessor :from def indirection_name @indirection_name || name.to_sym end # Here's your opportunity to override the indirection name. By default # it will be the same name as the string. def set_indirection_name(name) @indirection_name = name end # Return an indirection associated with an string, if one exists # One usually does. def indirection unless @indirection Puppet.info("Could not find terminus for #{indirection_name}") unless @indirection = Puppet::Indirector::Indirection.instance(indirection_name) end @indirection end def set_terminus(from) begin indirection.terminus_class = from rescue => detail raise "Could not set '#{indirection.name}' terminus to '#{from}' (#{detail}); valid terminus types are #{self.class.terminus_classes(indirection.name).join(", ") }" end end def call_indirection_method(method, *args) begin result = indirection.send(method, *args) rescue => detail puts detail.backtrace if Puppet[:trace] raise "Could not call '#{method}' on '#{indirection_name}': #{detail}" end result end end diff --git a/lib/puppet/string/option.rb b/lib/puppet/string/option.rb index bdc3e07c5..26b769c2e 100644 --- a/lib/puppet/string/option.rb +++ b/lib/puppet/string/option.rb @@ -1,24 +1,69 @@ class Puppet::String::Option - attr_reader :name, :string + attr_reader :string + attr_reader :name + attr_reader :aliases + attr_accessor :desc - def initialize(string, name, attrs = {}) - raise "#{name.inspect} is an invalid option name" unless name.to_s =~ /^[a-z]\w*$/ - @string = string - @name = name.to_sym - attrs.each do |k,v| send("#{k}=", v) end + def takes_argument? + !!@argument + end + def optional_argument? + !!@optional_argument end + def initialize(string, *declaration, &block) + @string = string + @optparse = [] + + # Collect and sort the arguments in the declaration. + declaration.each do |item| + if item.is_a? String and item.to_s =~ /^-/ then + unless item =~ /^-[a-z]\b/ or item =~ /^--[^-]/ then + raise ArgumentError, "#{item.inspect}: long options need two dashes (--)" + end + @optparse << item + else + raise ArgumentError, "#{item.inspect} is not valid for an option argument" + end + end + + if @optparse.empty? then + raise ArgumentError, "No option declarations found while building" + end + + # Now, infer the name from the options; we prefer the first long option as + # the name, rather than just the first option. + @name = optparse_to_name(@optparse.find do |a| a =~ /^--/ end || @optparse.first) + @aliases = @optparse.map { |o| optparse_to_name(o) } + + # Do we take an argument? If so, are we consistent about it, because + # incoherence here makes our life super-difficult, and we can more easily + # relax this rule later if we find a valid use case for it. --daniel 2011-03-30 + @argument = @optparse.any? { |o| o =~ /[ =]/ } + if @argument and not @optparse.all? { |o| o =~ /[ =]/ } then + raise ArgumentError, "Option #{@name} is inconsistent about taking an argument" + end + + # Is our argument optional? The rules about consistency apply here, also, + # just like they do to taking arguments at all. --daniel 2011-03-30 + @optional_argument = @optparse.any? { |o| o.include? "[" } + if @optional_argument and not @optparse.all? { |o| o.include? "[" } then + raise ArgumentError, "Option #{@name} is inconsistent about the argument being optional" + end + end + + # to_s and optparse_to_name are roughly mirrored, because they are used to + # transform strings to name symbols, and vice-versa. def to_s @name.to_s.tr('_', '-') end - Types = [:boolean, :string] - def type - @type ||= :boolean - end - def type=(input) - value = begin input.to_sym rescue nil end - Types.include?(value) or raise ArgumentError, "#{input.inspect} is not a valid type" - @type = value + def optparse_to_name(declaration) + unless found = declaration.match(/^-+([^= ]+)/) or found.length != 1 then + raise ArgumentError, "Can't find a name in the declaration #{declaration.inspect}" + end + name = found.captures.first.tr('-', '_') + raise "#{name.inspect} is an invalid option name" unless name.to_s =~ /^[a-z]\w*$/ + name.to_sym end end diff --git a/lib/puppet/string/option_builder.rb b/lib/puppet/string/option_builder.rb index 2087cbc99..da0d213fb 100644 --- a/lib/puppet/string/option_builder.rb +++ b/lib/puppet/string/option_builder.rb @@ -1,25 +1,25 @@ require 'puppet/string/option' class Puppet::String::OptionBuilder attr_reader :option - def self.build(string, name, attrs = {}, &block) - new(string, name, attrs, &block).option + def self.build(string, *declaration, &block) + new(string, *declaration, &block).option end private - def initialize(string, name, attrs, &block) + def initialize(string, *declaration, &block) @string = string - @option = Puppet::String::Option.new(string, name, attrs) + @option = Puppet::String::Option.new(string, *declaration) block and instance_eval(&block) @option end # Metaprogram the simple DSL from the option class. Puppet::String::Option.instance_methods.grep(/=$/).each do |setter| next if setter =~ /^=/ # special case, darn it... dsl = setter.sub(/=$/, '') define_method(dsl) do |value| @option.send(setter, value) end end end diff --git a/lib/puppet/string/option_manager.rb b/lib/puppet/string/option_manager.rb index df3ae6b4b..f952ad4f0 100644 --- a/lib/puppet/string/option_manager.rb +++ b/lib/puppet/string/option_manager.rb @@ -1,46 +1,56 @@ require 'puppet/string/option_builder' module Puppet::String::OptionManager # Declare that this app can take a specific option, and provide # the code to do so. - def option(name, attrs = {}, &block) - @options ||= {} - raise ArgumentError, "Option #{name} already defined for #{self}" if option?(name) - actions.each do |action| - if get_action(action).option?(name) then - raise ArgumentError, "Option #{name} already defined on action #{action} for #{self}" + def option(*declaration, &block) + add_option Puppet::String::OptionBuilder.build(self, *declaration, &block) + 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}" + end + + actions.each do |action| + action = get_action(action) + if conflict = action.get_option(name) then + raise ArgumentError, "Option #{option} conflicts with existing option #{conflict} on #{action}" + end end end - option = Puppet::String::OptionBuilder.build(self, name, &block) - @options[option.name] = option + + option.aliases.each { |name| @options[name] = option } + option end def options @options ||= {} result = @options.keys if self.is_a?(Class) and superclass.respond_to?(:options) result += superclass.options elsif self.class.respond_to?(:options) result += self.class.options end result.sort end def get_option(name) @options ||= {} result = @options[name.to_sym] unless result then if self.is_a?(Class) and superclass.respond_to?(:get_option) result = superclass.get_option(name) elsif self.class.respond_to?(:get_option) result = self.class.get_option(name) end end return result end def option?(name) options.include? name.to_sym end end diff --git a/spec/shared_behaviours/things_that_declare_options.rb b/spec/shared_behaviours/things_that_declare_options.rb new file mode 100644 index 000000000..6abce99e3 --- /dev/null +++ b/spec/shared_behaviours/things_that_declare_options.rb @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +shared_examples_for "things that declare options" do + it "should support options without arguments" do + subject = add_options_to { option "--bar" } + subject.should be_option :bar + end + + it "should support options with an empty block" do + subject = add_options_to do + option "--foo" do + # this section deliberately left blank + end + end + subject.should be + subject.should be_option :foo + end + + it "should support option documentation" do + text = "Sturm und Drang (German pronunciation: [ˈʃtʊʁm ʊnt ˈdʁaŋ]) …" + + subject = add_options_to do + option "--foo" do + desc text + end + end + + subject.get_option(:foo).desc.should == text + end + + it "should list all the options" do + subject = add_options_to do + option "--foo" + option "--bar" + end + subject.options.should =~ [:foo, :bar] + end + + it "should detect conflicts in long options" do + expect { + add_options_to do + option "--foo" + option "--foo" + end + }.should raise_error ArgumentError, /Option foo conflicts with existing option foo/i + end + + it "should detect conflicts in short options" do + expect { + add_options_to do + option "-f" + option "-f" + end + }.should raise_error ArgumentError, /Option f conflicts with existing option f/ + end + + # Verify the range of interesting conflicts to check for ordering causing + # the behaviour to change, or anything exciting like that. + [ %w{--foo}, %w{-f}, %w{-f --foo}, %w{--baz -f}, + %w{-f --baz}, %w{-b --foo}, %w{--foo -b} + ].each do |conflict| + base = %w{--foo -f} + it "should detect conflicts between #{base.inspect} and #{conflict.inspect}" do + expect { + add_options_to do + option *base + option *conflict + end + }.should raise_error ArgumentError, /conflicts with existing option/ + end + end + + it "should fail if we are not consistent about taking an argument" do + expect { add_options_to do option "--foo=bar", "--bar" end }. + should raise_error ArgumentError, /inconsistent about taking an argument/ + end + + it "should accept optional arguments" do + subject = add_options_to do option "--foo=[baz]", "--bar=[baz]" end + [:foo, :bar].each do |name| + subject.should be_option name + end + end + + describe "#takes_argument?" do + it "should detect an argument being absent" do + subject = add_options_to do option "--foo" end + subject.get_option(:foo).should_not be_takes_argument + end + ["=FOO", " FOO", "=[FOO]", " [FOO]"].each do |input| + it "should detect an argument given #{input.inspect}" do + subject = add_options_to do option "--foo#{input}" end + subject.get_option(:foo).should be_takes_argument + end + end + end + + describe "#optional_argument?" do + it "should be false if no argument is present" do + option = add_options_to do option "--foo" end.get_option(:foo) + option.should_not be_takes_argument + option.should_not be_optional_argument + end + + ["=FOO", " FOO"].each do |input| + it "should be false if the argument is mandatory (like #{input.inspect})" do + option = add_options_to do option "--foo#{input}" end.get_option(:foo) + option.should be_takes_argument + option.should_not be_optional_argument + end + end + + ["=[FOO]", " [FOO]"].each do |input| + it "should be true if the argument is optional (like #{input.inspect})" do + option = add_options_to do option "--foo#{input}" end.get_option(:foo) + option.should be_takes_argument + option.should be_optional_argument + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 4e54d7235..4073cb60b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,38 +1,42 @@ require 'pathname' dir = Pathname.new(__FILE__).parent $LOAD_PATH.unshift(dir, dir + 'lib', dir + '../lib') require 'mocha' require 'puppet' require 'puppet/string' require 'rspec' +Pathname.glob("#{dir}/shared_behaviours/**/*.rb") do |behaviour| + require behaviour.relative_path_from(dir) +end + RSpec.configure do |config| config.mock_with :mocha config.before :each do # Set the confdir and vardir to gibberish so that tests # have to be correctly mocked. Puppet[:confdir] = "/dev/null" Puppet[:vardir] = "/dev/null" # Avoid opening ports to the outside world Puppet.settings[:bindaddress] = "127.0.0.1" @logs = [] Puppet::Util::Log.newdestination(@logs) end config.after :each do Puppet.settings.clear @logs.clear Puppet::Util::Log.close_all end end # We need this because the RAL uses 'should' as a method. This # allows us the same behaviour but with a different method name. class Object alias :must :should end diff --git a/spec/unit/application/indirection_base_spec.rb b/spec/unit/application/indirection_base_spec.rb index 53e5f7e76..f636613c4 100755 --- a/spec/unit/application/indirection_base_spec.rb +++ b/spec/unit/application/indirection_base_spec.rb @@ -1,22 +1,39 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') require 'puppet/application/indirection_base' +require 'puppet/string/indirector' + +######################################################################## +# Stub for testing; the names are critical, sadly. --daniel 2011-03-30 +class Puppet::Application::TestIndirection < Puppet::Application::IndirectionBase +end + +string = Puppet::String::Indirector.define(:testindirection, '0.0.1') do +end +# REVISIT: This horror is required because we don't allow anything to be +# :current except for if it lives on, and is loaded from, disk. --daniel 2011-03-29 +string.version = :current +Puppet::String.register(string) +######################################################################## + describe Puppet::Application::IndirectionBase do - it "should support a 'terminus' accessor" do - test = subject - expect { test.terminus = :foo }.should_not raise_error - test.terminus.should == :foo - end + subject { Puppet::Application::TestIndirection.new } - it "should have a 'terminus' CLI option" do - subject.class.option_parser_commands.select do |options, function| - options.index { |o| o =~ /terminus/ } - end.should_not be_empty - end + it "should accept a terminus command line option" do + # It would be nice not to have to stub this, but whatever... writing an + # entire indirection stack would cause us more grief. --daniel 2011-03-31 + terminus = mock("test indirection terminus") + Puppet::Indirector::Indirection.expects(:instance). + with(:testindirection).returns() + + subject.command_line. + instance_variable_set('@args', %w{--terminus foo save}) + + # Not a very nice thing. :( + $stderr.stubs(:puts) - describe "setup" do - it "should fail if its string does not support an indirection" + expect { subject.run }.should raise_error SystemExit end end diff --git a/spec/unit/string/action_builder_spec.rb b/spec/unit/string/action_builder_spec.rb index 946244cbf..0229fe44d 100755 --- a/spec/unit/string/action_builder_spec.rb +++ b/spec/unit/string/action_builder_spec.rb @@ -1,59 +1,59 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') require 'puppet/string/action_builder' describe Puppet::String::ActionBuilder do describe "::build" do it "should build an action" do action = Puppet::String::ActionBuilder.build(nil,:foo) do end action.should be_a(Puppet::String::Action) action.name.should == :foo end it "should define a method on the string which invokes the action" do string = Puppet::String.new(:action_builder_test_string, '0.0.1') action = Puppet::String::ActionBuilder.build(string, :foo) do invoke do "invoked the method" end end string.foo.should == "invoked the method" end it "should require a block" do lambda { Puppet::String::ActionBuilder.build(nil,:foo) }. should raise_error("Action :foo must specify a block") end describe "when handling options" do let :string do Puppet::String.new(:option_handling, '0.0.1') end it "should have a #option DSL function" do method = nil Puppet::String::ActionBuilder.build(string, :foo) do method = self.method(:option) end method.should be end it "should define an option without a block" do action = Puppet::String::ActionBuilder.build(string, :foo) do - option :bar + option "--bar" end action.should be_option :bar end it "should accept an empty block" do action = Puppet::String::ActionBuilder.build(string, :foo) do - option :bar do + option "--bar" do # This space left deliberately blank. end end action.should be_option :bar end end end end diff --git a/spec/unit/string/action_spec.rb b/spec/unit/string/action_spec.rb index d182f0abe..258ad5aa6 100755 --- a/spec/unit/string/action_spec.rb +++ b/spec/unit/string/action_spec.rb @@ -1,148 +1,135 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') require 'puppet/string/action' describe Puppet::String::Action do describe "when validating the action name" do [nil, '', 'foo bar', '-foobar'].each do |input| it "should treat #{input.inspect} as an invalid name" do expect { Puppet::String::Action.new(nil, input) }. should raise_error(/is an invalid action name/) end end end describe "when invoking" do it "should be able to call other actions on the same object" do string = Puppet::String.new(:my_string, '0.0.1') do action(:foo) do invoke { 25 } end action(:bar) do invoke { "the value of foo is '#{foo}'" } end end string.foo.should == 25 string.bar.should == "the value of foo is '25'" end # bar is a class action calling a class action # quux is a class action calling an instance action # baz is an instance action calling a class action # qux is an instance action calling an instance action it "should be able to call other actions on the same object when defined on a class" do class Puppet::String::MyStringBaseClass < Puppet::String action(:foo) do invoke { 25 } end action(:bar) do invoke { "the value of foo is '#{foo}'" } end action(:quux) do invoke { "qux told me #{qux}" } end end string = Puppet::String::MyStringBaseClass.new(:my_inherited_string, '0.0.1') do action(:baz) do invoke { "the value of foo in baz is '#{foo}'" } end action(:qux) do invoke { baz } end end string.foo.should == 25 string.bar.should == "the value of foo is '25'" string.quux.should == "qux told me the value of foo in baz is '25'" string.baz.should == "the value of foo in baz is '25'" string.qux.should == "the value of foo in baz is '25'" end end describe "with action-level options" do - it "should support options without arguments" do - string = Puppet::String.new(:action_level_options, '0.0.1') do - action(:foo) do - option :bar - end - end - - string.should_not be_option :bar - string.get_action(:foo).should be_option :bar - end - it "should support options with an empty block" do string = Puppet::String.new(:action_level_options, '0.0.1') do action :foo do - option :bar do + option "--bar" do # this line left deliberately blank end end end string.should_not be_option :bar string.get_action(:foo).should be_option :bar end it "should return only action level options when there are no string options" do string = Puppet::String.new(:action_level_options, '0.0.1') do - action :foo do option :bar end + action :foo do option "--bar" end end string.get_action(:foo).options.should =~ [:bar] end describe "with both string and action options" do let :string do Puppet::String.new(:action_level_options, '0.0.1') do - action :foo do option :bar end - action :baz do option :bim end - option :quux + action :foo do option "--bar" end + action :baz do option "--bim" end + option "--quux" end end it "should return combined string and action options" do string.get_action(:foo).options.should =~ [:bar, :quux] end it "should get an action option when asked" do string.get_action(:foo).get_option(:bar). should be_an_instance_of Puppet::String::Option end it "should get a string option when asked" do string.get_action(:foo).get_option(:quux). should be_an_instance_of Puppet::String::Option end it "should return options only for this action" do string.get_action(:baz).options.should =~ [:bim, :quux] end end - it "should fail when a duplicate option is added" do - expect { - Puppet::String.new(:action_level_options, '0.0.1') do - action :foo do - option :foo - option :foo - end + it_should_behave_like "things that declare options" do + def add_options_to(&block) + string = Puppet::String.new(:with_options, '0.0.1') do + action(:foo, &block) end - }.should raise_error ArgumentError, /foo duplicates an existing option/ + string.get_action(:foo) + end end it "should fail when a string option duplicates an action option" do expect { Puppet::String.new(:action_level_options, '0.0.1') do - option :foo - action :bar do option :foo end + option "--foo" + action :bar do option "--foo" end end - }.should raise_error ArgumentError, /duplicates an existing option .*action_level/i + }.should raise_error ArgumentError, /Option foo conflicts with existing option foo/i end end end diff --git a/spec/unit/string/option_builder_spec.rb b/spec/unit/string/option_builder_spec.rb index 685787808..9e913c29c 100644 --- a/spec/unit/string/option_builder_spec.rb +++ b/spec/unit/string/option_builder_spec.rb @@ -1,57 +1,29 @@ require 'puppet/string/option_builder' describe Puppet::String::OptionBuilder do let :string do Puppet::String.new(:option_builder_testing, '0.0.1') end it "should be able to construct an option without a block" do - Puppet::String::OptionBuilder.build(string, :foo). + Puppet::String::OptionBuilder.build(string, "--foo"). should be_an_instance_of Puppet::String::Option end - it "should set attributes during construction" do - # Walk all types, since at least one of them should be non-default... - Puppet::String::Option::Types.each do |type| - option = Puppet::String::OptionBuilder.build(string, :foo, :type => type) - option.should be_an_instance_of Puppet::String::Option - option.type.should == type - end - end - describe "when using the DSL block" do it "should work with an empty block" do - option = Puppet::String::OptionBuilder.build(string, :foo) do + option = Puppet::String::OptionBuilder.build(string, "--foo") do # This block deliberately left blank. end option.should be_an_instance_of Puppet::String::Option end - describe "#type" do - Puppet::String::Option::Types.each do |valid| - it "should accept #{valid.inspect}" do - option = Puppet::String::OptionBuilder.build(string, :foo) do - type valid - end - option.should be_an_instance_of Puppet::String::Option - end - - it "should accept #{valid.inspect} as a string" do - option = Puppet::String::OptionBuilder.build(string, :foo) do - type valid.to_s - end - option.should be_an_instance_of Puppet::String::Option - end - - [:foo, nil, true, false, 12, '12', 'whatever', ::String, URI].each do |input| - it "should reject #{input.inspect}" do - expect { - Puppet::String::OptionBuilder.build(string, :foo) do - type input - end - }.should raise_error ArgumentError, /not a valid type/ - end - end + it "should support documentation declarations" do + text = "this is the description" + option = Puppet::String::OptionBuilder.build(string, "--foo") do + desc text end + option.should be_an_instance_of Puppet::String::Option + option.desc.should == text end end end diff --git a/spec/unit/string/option_spec.rb b/spec/unit/string/option_spec.rb index 9bb4309cd..fc7b8329b 100644 --- a/spec/unit/string/option_spec.rb +++ b/spec/unit/string/option_spec.rb @@ -1,61 +1,75 @@ require 'puppet/string/option' describe Puppet::String::Option do let :string do Puppet::String.new(:option_testing, '0.0.1') end + describe "#optparse_to_name" do + ["", "=BAR", " BAR", "=bar", " bar"].each do |postfix| + { "--foo" => :foo, "-f" => :f,}.each do |base, expect| + input = base + postfix + it "should map #{input.inspect} to #{expect.inspect}" do + option = Puppet::String::Option.new(string, input) + option.name.should == expect + end + end + end + + [:foo, 12, nil, {}, []].each do |input| + it "should fail sensible when given #{input.inspect}" do + expect { Puppet::String::Option.new(string, input) }. + should raise_error ArgumentError, /is not valid for an option argument/ + end + end + + ["-foo", "-foo=BAR", "-foo BAR"].each do |input| + it "should fail with a single dash for long option #{input.inspect}" do + expect { Puppet::String::Option.new(string, input) }. + should raise_error ArgumentError, /long options need two dashes \(--\)/ + end + end + end + it "requires a string when created" do expect { Puppet::String::Option.new }. should raise_error ArgumentError, /wrong number of arguments/ end - it "also requires a name when created" do + it "also requires some declaration arguments when created" do expect { Puppet::String::Option.new(string) }. - should raise_error ArgumentError, /wrong number of arguments/ + should raise_error ArgumentError, /No option declarations found/ end - it "should create an instance when given a string and name" do - Puppet::String::Option.new(string, :foo). - should be_instance_of Puppet::String::Option + it "should infer the name from an optparse string" do + option = Puppet::String::Option.new(string, "--foo") + option.name.should == :foo end - describe "#to_s" do - it "should transform a symbol into a string" do - Puppet::String::Option.new(string, :foo).to_s.should == "foo" - end + it "should infer the name when multiple optparse strings are given" do + option = Puppet::String::Option.new(string, "--foo", "-f") + option.name.should == :foo + end - it "should use - rather than _ to separate words" do - Puppet::String::Option.new(string, :foo_bar).to_s.should == "foo-bar" - end + it "should prefer the first long option name over a short option name" do + option = Puppet::String::Option.new(string, "-f", "--foo") + option.name.should == :foo end - describe "#type" do - Puppet::String::Option::Types.each do |type| - it "should accept #{type.inspect}" do - Puppet::String::Option.new(string, :foo, :type => type). - should be_an_instance_of Puppet::String::Option - end + it "should create an instance when given a string and name" do + Puppet::String::Option.new(string, "--foo"). + should be_instance_of Puppet::String::Option + end - it "should accept #{type.inspect} when given as a string" do - Puppet::String::Option.new(string, :foo, :type => type.to_s). - should be_an_instance_of Puppet::String::Option - end + describe "#to_s" do + it "should transform a symbol into a string" do + option = Puppet::String::Option.new(string, "--foo") + option.name.should == :foo + option.to_s.should == "foo" end - [:foo, nil, true, false, 12, '12', 'whatever', ::String, URI].each do |input| - it "should reject #{input.inspect}" do - expect { Puppet::String::Option.new(string, :foo, :type => input) }. - should raise_error ArgumentError, /not a valid type/ - end + it "should use - rather than _ to separate words in strings but not symbols" do + option = Puppet::String::Option.new(string, "--foo-bar") + option.name.should == :foo_bar + option.to_s.should == "foo-bar" end end - - - # name short value type - # ca-location CA_LOCATION string - # debug d ---- boolean - # verbose v ---- boolean - # terminus TERMINUS string - # format FORMAT symbol - # mode r RUNMODE limited set of symbols - # server URL URL end diff --git a/spec/unit/string_spec.rb b/spec/unit/string_spec.rb index 577505186..7f7489e2e 100755 --- a/spec/unit/string_spec.rb +++ b/spec/unit/string_spec.rb @@ -1,173 +1,145 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb') describe Puppet::String do before :all do @strings = Puppet::String::StringCollection.instance_variable_get("@strings").dup end before :each do Puppet::String::StringCollection.instance_variable_get("@strings").clear end after :all do Puppet::String::StringCollection.instance_variable_set("@strings", @strings) end describe "#define" do it "should register the string" do string = Puppet::String.define(:string_test_register, '0.0.1') string.should == Puppet::String[:string_test_register, '0.0.1'] end it "should load actions" do Puppet::String.any_instance.expects(:load_actions) Puppet::String.define(:string_test_load_actions, '0.0.1') end it "should require a version number" do proc { Puppet::String.define(:no_version) }.should raise_error(ArgumentError) end end describe "#initialize" do it "should require a version number" do proc { Puppet::String.new(:no_version) }.should raise_error(ArgumentError) end it "should require a valid version number" do proc { Puppet::String.new(:bad_version, 'Rasins') }.should raise_error(ArgumentError) end it "should instance-eval any provided block" do face = Puppet::String.new(:string_test_block,'0.0.1') do action(:something) do invoke { "foo" } end end face.something.should == "foo" end end it "should have a name" do Puppet::String.new(:me,'0.0.1').name.should == :me end it "should stringify with its own name" do Puppet::String.new(:me,'0.0.1').to_s.should =~ /\bme\b/ end it "should allow overriding of the default format" do face = Puppet::String.new(:me,'0.0.1') face.set_default_format :foo face.default_format.should == :foo end it "should default to :pson for its format" do Puppet::String.new(:me, '0.0.1').default_format.should == :pson end # Why? it "should create a class-level autoloader" do Puppet::String.autoloader.should be_instance_of(Puppet::Util::Autoload) end it "should try to require strings that are not known" do Puppet::String::StringCollection.expects(:require).with "puppet/string/foo" Puppet::String::StringCollection.expects(:require).with "foo@0.0.1/puppet/string/foo" Puppet::String[:foo, '0.0.1'] end it "should be able to load all actions in all search paths" - describe "with string-level options" do - it "should support options without arguments" do - string = Puppet::String.new(:with_options, '0.0.1') do - option :foo - end - string.should be_an_instance_of Puppet::String - string.should be_option :foo - end - it "should support options with an empty block" do - string = Puppet::String.new(:with_options, '0.0.1') do - option :foo do - # this section deliberately left blank - end - end - string.should be_an_instance_of Puppet::String - string.should be_option :foo - end - - it "should return all the string-level options" do - string = Puppet::String.new(:with_options, '0.0.1') do - option :foo - option :bar - end - string.options.should =~ [:foo, :bar] + it_should_behave_like "things that declare options" do + def add_options_to(&block) + Puppet::String.new(:with_options, '0.0.1', &block) end + end + describe "with string-level options" do it "should not return any action-level options" do string = Puppet::String.new(:with_options, '0.0.1') do - option :foo - option :bar + option "--foo" + option "--bar" action :baz do - option :quux + option "--quux" end end string.options.should =~ [:foo, :bar] end - it "should fail when a duplicate option is added" do - expect { - Puppet::String.new(:action_level_options, '0.0.1') do - option :foo - option :foo - end - }.should raise_error ArgumentError, /option foo already defined for/i - end - it "should fail when a string option duplicates an action option" do expect { Puppet::String.new(:action_level_options, '0.0.1') do - action :bar do option :foo end - option :foo + action :bar do option "--foo" end + option "--foo" end - }.should raise_error ArgumentError, /foo already defined on action bar/i + }.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 string = Puppet::String.new(:with_options, '0.0.1') do - action :foo do option :quux end - action :bar do option :quux end + action :foo do option "--quux" end + action :bar do option "--quux" end end string.get_action(:foo).options.should =~ [:quux] string.get_action(:bar).options.should =~ [:quux] end end describe "with inherited options" do let :string do parent = Class.new(Puppet::String) - parent.option(:inherited, :type => :string) + parent.option("--inherited") string = parent.new(:example, '0.2.1') - string.option(:local) + string.option("--local") string end describe "#options" do it "should list inherited options" do string.options.should =~ [:inherited, :local] end end describe "#get_option" do it "should return an inherited option object" do string.get_option(:inherited).should be_an_instance_of Puppet::String::Option end end end end