diff --git a/lib/puppet/application/interface_base.rb b/lib/puppet/application/interface_base.rb index c1c02040a..841f3ca12 100644 --- a/lib/puppet/application/interface_base.rb +++ b/lib/puppet/application/interface_base.rb @@ -1,97 +1,97 @@ require 'puppet/application' require 'puppet/interface' class Puppet::Application::InterfaceBase < Puppet::Application should_parse_config run_mode :agent def preinit super trap(:INT) do $stderr.puts "Cancelling Interface" 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 :interface, :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 = interface.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 setup Puppet::Util::Log.newdestination :console @verb = command_line.args.shift @arguments = command_line.args @arguments ||= [] @arguments = Array(@arguments) @type = self.class.name.to_s.sub(/.+:/, '').downcase.to_sym # TODO: These should be configurable versions. - unless Puppet::Interface.interface?(@type, '0.0.1') - raise "Could not find version #{1} of interface '#{@type}'" + unless Puppet::Interface.interface?(@type, :latest) + raise "Could not find any version of interface '#{@type}'" end - @interface = Puppet::Interface[@type, '0.0.1'] + @interface = Puppet::Interface[@type, :latest] @format ||= @interface.default_format # We copy all of the app options to the interface. # This allows each action to read in the options. @interface.options = options validate end def validate unless verb raise "You must specify #{interface.actions.join(", ")} as a verb; 'save' probably does not work right now" end unless interface.action?(verb) raise "Command '#{verb}' not found for #{type}" end end end diff --git a/lib/puppet/interface.rb b/lib/puppet/interface.rb index 27cbb7522..a667c6b75 100644 --- a/lib/puppet/interface.rb +++ b/lib/puppet/interface.rb @@ -1,94 +1,98 @@ require 'puppet' require 'puppet/util/autoload' class Puppet::Interface require 'puppet/interface/action_manager' require 'puppet/interface/interface_collection' include Puppet::Interface::ActionManager extend Puppet::Interface::ActionManager include Puppet::Util class << self # This is just so we can search for actions. We only use its # list of directories to search. # Can't we utilize an external autoloader, or simply use the $LOAD_PATH? -pvb def autoloader @autoloader ||= Puppet::Util::Autoload.new(:application, "puppet/interface") end def interfaces Puppet::Interface::InterfaceCollection.interfaces end def interface?(name, version) Puppet::Interface::InterfaceCollection.interface?(name, version) end def register(instance) Puppet::Interface::InterfaceCollection.register(instance) end def define(name, version, &block) if interface?(name, version) interface = Puppet::Interface::InterfaceCollection[name, version] else interface = self.new(name, version) Puppet::Interface::InterfaceCollection.register(interface) interface.load_actions end interface.instance_eval(&block) if block_given? return interface end alias :[] :define end attr_accessor :default_format def set_default_format(format) self.default_format = format.to_sym end attr_accessor :type, :verb, :version, :arguments, :options attr_reader :name def initialize(name, version, &block) + unless Puppet::Interface::InterfaceCollection.validate_version(version) + raise ArgumentError, "Cannot create interface with invalid version number '#{version}'!" + end + @name = Puppet::Interface::InterfaceCollection.underscorize(name) @version = version @default_format = :pson instance_eval(&block) if block_given? end # Try to find actions defined in other files. def load_actions path = "puppet/interface/v#{version}/#{name}" loaded = [] Puppet::Interface.autoloader.search_directories.each do |dir| fdir = ::File.join(dir, path) next unless FileTest.directory?(fdir) Dir.chdir(fdir) do Dir.glob("*.rb").each do |file| aname = file.sub(/\.rb/, '') if loaded.include?(aname) Puppet.debug "Not loading duplicate action '#{aname}' for '#{name}' from '#{fdir}/#{file}'" next end loaded << aname Puppet.debug "Loading action '#{aname}' for '#{name}' from '#{fdir}/#{file}'" require "#{path}/#{aname}" end end end end def to_s "Puppet::Interface[#{name.inspect}, #{version.inspect}]" end end diff --git a/lib/puppet/interface/interface_collection.rb b/lib/puppet/interface/interface_collection.rb index 115892397..92e2933fe 100644 --- a/lib/puppet/interface/interface_collection.rb +++ b/lib/puppet/interface/interface_collection.rb @@ -1,87 +1,98 @@ require 'puppet/interface' module Puppet::Interface::InterfaceCollection SEMVER_VERSION = /^(\d+)\.(\d+)\.(\d+)([A-Za-z][0-9A-Za-z-]*|)$/ @interfaces = Hash.new { |hash, key| hash[key] = {} } def self.interfaces unless @loaded @loaded = true $LOAD_PATH.each do |dir| next unless FileTest.directory?(dir) Dir.chdir(dir) do Dir.glob("puppet/interface/v*/*.rb").collect { |f| f.sub(/\.rb/, '') }.each do |file| iname = file.sub(/\.rb/, '') begin require iname rescue Exception => detail puts detail.backtrace if Puppet[:trace] raise "Could not load #{iname} from #{dir}/#{file}: #{detail}" end end end end end return @interfaces.keys end def self.versions(name) versions = [] $LOAD_PATH.each do |dir| next unless FileTest.directory?(dir) v_dir = File.join dir, %w[puppet interface v*] Dir.glob(File.join v_dir, "#{name}{.rb,/*.rb}").each do |f| v = f.sub(%r[.*/v([^/]+?)/#{name}(?:(?:/[^/]+)?.rb)$], '\1') - if v =~ SEMVER_VERSION + if validate_version(v) versions << v else warn "'#{v}' (#{f}) is not a valid version string; skipping" end end end return versions.uniq.sort { |a, b| compare_versions(a, b) } end + def self.validate_version(version) + !!(SEMVER_VERSION =~ version.to_s) + end + def self.compare_versions(a, b) a, b = [a, b].map do |x| parts = SEMVER_VERSION.match(x).to_a[1..4] parts[0..2] = parts[0..2].map { |e| e.to_i } parts end cmp = a[0..2] <=> b[0..2] if cmp == 0 cmp = a[3] <=> b[3] cmp = +1 if a[3].empty? && !b[3].empty? cmp = -1 if b[3].empty? && !a[3].empty? end cmp end def self.[](name, version) - @interfaces[underscorize(name)][version] if interface?(name, version) + version = versions(name).last if version == :latest + unless version.nil? + @interfaces[underscorize(name)][version] if interface?(name, version) + end end def self.interface?(name, version) + version = versions(name).last if version == :latest + return false if version.nil? + name = underscorize(name) + unless @interfaces.has_key?(name) && @interfaces[name].has_key?(version) require "puppet/interface/v#{version}/#{name}" end return @interfaces.has_key?(name) && @interfaces[name].has_key?(version) rescue LoadError return false end def self.register(interface) @interfaces[underscorize(interface.name)][interface.version] = interface end def self.underscorize(name) unless name.to_s =~ /^[-_a-z]+$/i then raise ArgumentError, "#{name.inspect} (#{name.class}) is not a valid interface name" end name.to_s.downcase.split(/[-_]/).join('_').to_sym end end diff --git a/spec/unit/application/interface_base_spec.rb b/spec/unit/application/interface_base_spec.rb index 15d465559..d82325bfd 100644 --- a/spec/unit/application/interface_base_spec.rb +++ b/spec/unit/application/interface_base_spec.rb @@ -1,61 +1,73 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') require 'puppet/application/interface_base' describe Puppet::Application::InterfaceBase do - base_interface = Puppet::Interface[:basetest, '0.0.1'] + before :all do + @dir = Dir.mktmpdir + $LOAD_PATH.push(@dir) + FileUtils.mkdir_p(File.join @dir, 'puppet', 'interface', 'v0.0.1') + FileUtils.touch(File.join @dir, 'puppet', 'interface', 'v0.0.1', 'basetest.rb') + end + + after :all do + FileUtils.remove_entry_secure @dir + $LOAD_PATH.pop + end + + base_interface = Puppet::Interface.define(:basetest, '0.0.1') class Puppet::Application::InterfaceBase::Basetest < Puppet::Application::InterfaceBase end before do @app = Puppet::Application::InterfaceBase::Basetest.new @app.stubs(:interface).returns base_interface @app.stubs(:exit) @app.stubs(:puts) Puppet::Util::Log.stubs(:newdestination) end describe "when calling main" do before do @app.verb = :find @app.arguments = ["myname", "myarg"] @app.interface.stubs(:find) end it "should send the specified verb and name to the interface" do @app.interface.expects(:find).with("myname", "myarg") @app.main end it "should use its render method to render any result" it "should exit with the current exit code" end describe "during setup" do before do @app.command_line.stubs(:args).returns(["find", "myname", "myarg"]) @app.stubs(:validate) end it "should set the verb from the command line arguments" do @app.setup @app.verb.should == "find" end it "should make sure arguments are an array" do @app.command_line.stubs(:args).returns(["find", "myname", "myarg"]) @app.setup @app.arguments.should == ["myname", "myarg"] end it "should set the options on the interface" do @app.options[:foo] = "bar" @app.setup @app.interface.options.should == @app.options end end end diff --git a/spec/unit/interface/interface_collection_spec.rb b/spec/unit/interface/interface_collection_spec.rb index a943c2ec2..3e4b9d624 100644 --- a/spec/unit/interface/interface_collection_spec.rb +++ b/spec/unit/interface/interface_collection_spec.rb @@ -1,203 +1,249 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') require 'tmpdir' describe Puppet::Interface::InterfaceCollection do before :all do @interfaces = subject.instance_variable_get("@interfaces").dup end before :each do subject.instance_variable_get("@interfaces").clear end after :all do subject.instance_variable_set("@interfaces", @interfaces) end describe "::interfaces" do end describe "::versions" do before :each do @dir = Dir.mktmpdir @lib = FileUtils.mkdir_p(File.join @dir, 'puppet', 'interface') $LOAD_PATH.push(@dir) end after :each do FileUtils.remove_entry_secure @dir $LOAD_PATH.pop end it "should return an empty array when no versions are loadable" do subject.versions(:fozzie).should == [] end it "should return versions loadable as puppet/interface/v{version}/{name}" do FileUtils.mkdir_p(File.join @lib, 'v1.0.0') FileUtils.touch(File.join @lib, 'v1.0.0', 'fozzie.rb') subject.versions(:fozzie).should == ['1.0.0'] end it "should an ordered list of all versions loadable as puppet/interface/v{version}/{name}" do %w[ 1.2.1rc2 1.2.1beta1 1.2.1rc1 1.2.1 1.2.2 ].each do |version| FileUtils.mkdir_p(File.join @lib, "v#{version}") FileUtils.touch(File.join @lib, "v#{version}", 'fozzie.rb') end subject.versions(:fozzie).should == %w[ 1.2.1beta1 1.2.1rc1 1.2.1rc2 1.2.1 1.2.2 ] end it "should not return a version for an empty puppet/interface/v{version}/{name}" do FileUtils.mkdir_p(File.join @lib, 'v1.0.0', 'fozzie') subject.versions(:fozzie).should == [] end it "should an ordered list of all versions loadable as puppet/interface/v{version}/{name}/*.rb" do %w[ 1.2.1rc2 1.2.1beta1 1.2.1rc1 1.2.1 1.2.2 ].each do |version| FileUtils.mkdir_p(File.join @lib, "v#{version}", "fozzie") FileUtils.touch(File.join @lib, "v#{version}", 'fozzie', 'action.rb') end subject.versions(:fozzie).should == %w[ 1.2.1beta1 1.2.1rc1 1.2.1rc2 1.2.1 1.2.2 ] end end + describe "::validate_version" do + it 'should permit three number versions' do + subject.validate_version('10.10.10').should == true + end + + it 'should permit versions with appended descriptions' do + subject.validate_version('10.10.10beta').should == true + end + + it 'should not permit versions with more than three numbers' do + subject.validate_version('1.2.3.4').should == false + end + + it 'should not permit versions with only two numbers' do + subject.validate_version('10.10').should == false + end + + it 'should not permit versions with only one number' do + subject.validate_version('123').should == false + end + + it 'should not permit versions with text in any position but at the end' do + subject.validate_version('v1.1.1').should == false + end + end + describe "::compare_versions" do # (a <=> b) should be: # -1 if a < b # 0 if a == b # 1 if a > b it 'should sort major version numbers numerically' do subject.compare_versions('1.0.0', '2.0.0').should == -1 subject.compare_versions('2.0.0', '1.1.1').should == 1 subject.compare_versions('2.0.0', '10.0.0').should == -1 end it 'should sort minor version numbers numerically' do subject.compare_versions('0.1.0', '0.2.0').should == -1 subject.compare_versions('0.2.0', '0.1.1').should == 1 subject.compare_versions('0.2.0', '0.10.0').should == -1 end it 'should sort tiny version numbers numerically' do subject.compare_versions('0.0.1', '0.0.2').should == -1 subject.compare_versions('0.0.2', '0.0.1').should == 1 subject.compare_versions('0.0.2', '0.0.10').should == -1 end it 'should sort major version before minor version' do subject.compare_versions('1.1.0', '1.2.0').should == -1 subject.compare_versions('1.2.0', '1.1.1').should == 1 subject.compare_versions('1.2.0', '1.10.0').should == -1 subject.compare_versions('1.1.0', '2.2.0').should == -1 subject.compare_versions('2.2.0', '1.1.1').should == 1 subject.compare_versions('2.2.0', '1.10.0').should == 1 end it 'should sort minor version before tiny version' do subject.compare_versions('0.1.1', '0.1.2').should == -1 subject.compare_versions('0.1.2', '0.1.1').should == 1 subject.compare_versions('0.1.2', '0.1.10').should == -1 subject.compare_versions('0.1.1', '0.2.2').should == -1 subject.compare_versions('0.2.2', '0.1.1').should == 1 subject.compare_versions('0.2.2', '0.1.10').should == 1 end it 'should sort appended strings asciibetically' do subject.compare_versions('0.0.0a', '0.0.0b').should == -1 subject.compare_versions('0.0.0beta1', '0.0.0beta2').should == -1 subject.compare_versions('0.0.0beta1', '0.0.0rc1').should == -1 subject.compare_versions('0.0.0beta1', '0.0.0alpha1').should == 1 subject.compare_versions('0.0.0beta1', '0.0.0beta1').should == 0 end it "should sort appended strings before 'whole' versions" do subject.compare_versions('0.0.1a', '0.0.1').should == -1 subject.compare_versions('0.0.1', '0.0.1beta').should == 1 end end describe "::[]" do before :each do subject.instance_variable_get("@interfaces")[:foo]['0.0.1'] = 10 end + before :each do + @dir = Dir.mktmpdir + @lib = FileUtils.mkdir_p(File.join @dir, 'puppet', 'interface') + $LOAD_PATH.push(@dir) + end + + after :each do + FileUtils.remove_entry_secure @dir + $LOAD_PATH.pop + end + it "should return the interface with the given name" do subject["foo", '0.0.1'].should == 10 end it "should attempt to load the interface if it isn't found" do subject.expects(:require).with('puppet/interface/v0.0.1/bar') subject["bar", '0.0.1'] end + + it "should attempt to load the interface with the greatest version for specified version :latest" do + %w[ 1.2.1 1.2.2 ].each do |version| + FileUtils.mkdir_p(File.join @lib, "v#{version}") + FileUtils.touch(File.join @lib, "v#{version}", 'fozzie.rb') + end + subject.expects(:require).with('puppet/interface/v1.2.2/fozzie') + subject['fozzie', :latest] + end end describe "::interface?" do before :each do subject.instance_variable_get("@interfaces")[:foo]['0.0.1'] = 10 end it "should return true if the interface specified is registered" do subject.interface?("foo", '0.0.1').should == true end it "should attempt to require the interface if it is not registered" do subject.expects(:require).with('puppet/interface/v0.0.1/bar') subject.interface?("bar", '0.0.1') end it "should return true if requiring the interface registered it" do subject.stubs(:require).with do subject.instance_variable_get("@interfaces")[:bar]['0.0.1'] = 20 end subject.interface?("bar", '0.0.1').should == true end it "should return false if the interface is not registered" do subject.stubs(:require).returns(true) subject.interface?("bar", '0.0.1').should == false end it "should return false if there is a LoadError requiring the interface" do subject.stubs(:require).raises(LoadError) subject.interface?("bar", '0.0.1').should == false end end describe "::register" do it "should store the interface by name" do interface = Puppet::Interface.new(:my_interface, '0.0.1') subject.register(interface) subject.instance_variable_get("@interfaces").should == {:my_interface => {'0.0.1' => interface}} end end describe "::underscorize" do faulty = [1, "#foo", "$bar", "sturm und drang", :"sturm und drang"] valid = { "Foo" => :foo, :Foo => :foo, "foo_bar" => :foo_bar, :foo_bar => :foo_bar, "foo-bar" => :foo_bar, :"foo-bar" => :foo_bar, } valid.each do |input, expect| it "should map #{input.inspect} to #{expect.inspect}" do result = subject.underscorize(input) result.should == expect end end faulty.each do |input| it "should fail when presented with #{input.inspect} (#{input.class})" do expect { subject.underscorize(input) }. should raise_error ArgumentError, /not a valid interface name/ end end end end diff --git a/spec/unit/interface_spec.rb b/spec/unit/interface_spec.rb index 060a71fb8..cf7d209da 100755 --- a/spec/unit/interface_spec.rb +++ b/spec/unit/interface_spec.rb @@ -1,79 +1,83 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../spec_helper.rb') describe Puppet::Interface do before :all do @interfaces = Puppet::Interface::InterfaceCollection.instance_variable_get("@interfaces").dup end before :each do Puppet::Interface::InterfaceCollection.instance_variable_get("@interfaces").clear end after :all do Puppet::Interface::InterfaceCollection.instance_variable_set("@interfaces", @interfaces) end describe "#define" do it "should register the interface" do interface = Puppet::Interface.define(:interface_test_register, '0.0.1') interface.should == Puppet::Interface[:interface_test_register, '0.0.1'] end it "should load actions" do Puppet::Interface.any_instance.expects(:load_actions) Puppet::Interface.define(:interface_test_load_actions, '0.0.1') end it "should require a version number" do proc { Puppet::Interface.define(:no_version) }.should raise_error(ArgumentError) end end describe "#initialize" do it "should require a version number" do proc { Puppet::Interface.new(:no_version) }.should raise_error(ArgumentError) end + it "should require a valid version number" do + proc { Puppet::Interface.new(:bad_version, 'Rasins') }.should raise_error(ArgumentError) + end + it "should instance-eval any provided block" do face = Puppet::Interface.new(:interface_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::Interface.new(:me,'0.0.1').name.should == :me end it "should stringify with its own name" do Puppet::Interface.new(:me,'0.0.1').to_s.should =~ /\bme\b/ end it "should allow overriding of the default format" do face = Puppet::Interface.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::Interface.new(:me, '0.0.1').default_format.should == :pson end # Why? it "should create a class-level autoloader" do Puppet::Interface.autoloader.should be_instance_of(Puppet::Util::Autoload) end it "should try to require interfaces that are not known" do Puppet::Interface::InterfaceCollection.expects(:require).with "puppet/interface/v0.0.1/foo" Puppet::Interface[:foo, '0.0.1'] end it "should be able to load all actions in all search paths" end