diff --git a/README.markdown b/README.markdown index 5f720db62..29ff41430 100644 --- a/README.markdown +++ b/README.markdown @@ -1,113 +1,115 @@ Puppet Interfaces ================= A set of executables that provide complete CLI access to Puppet's core data types. They also provide Interface classes for each of the core data types, which are extensible via plugins. For instance, you can create a new action for catalogs at lib/puppet/interface/catalog/$action.rb. This is a Puppet module and should work fine if you install it in Puppet's module path. **Note that this only works with Puppet 2.6.next (and thus will work with 2.6.5), because there is otherwise a bug in finding Puppet applications. You also have to either install the lib files into your Puppet libdir, or you need to add this lib directory to your RUBYLIB.** This is meant to be tested and iterated upon, with the plan that it will be merged into Puppet core once we're satisfied with it. Usage ----- The general usage is: $ puppet So, e.g.: $ puppet facts find myhost.domain.com $ puppet node destroy myhost You can use it to list all known data types and the available terminus classes: $ puppet interface list catalog : active_record, compiler, queue, rest, yaml certificate : ca, file, rest certificate_request : ca, file, rest certificate_revocation_list : ca, file, rest file_bucket_file : file, rest inventory : yaml key : ca, file node : active_record, exec, ldap, memory, plain, rest, yaml report : processor, rest, yaml resource : ral, rest resource_type : parser, rest status : local, rest But most interestingly, you can use it for two main purposes: * As a client for any Puppet REST server, such as catalogs, facts, reports, etc. * As a local CLI for any local Puppet data A simple case is looking at the local facts: $ puppet facts find localhost If you're on the server, you can look in that server's fact collection: $ puppet facts --mode master --vardir /tmp/foo --terminus yaml find localhost Note that we're setting both the vardir and the 'mode', which switches from the default 'agent' mode to server mode (requires a patch in my branch). If you'd prefer the data be outputted in json instead of yaml, well, you can do that, too: $ puppet find --mode master facts --vardir /tmp/foo --terminus yaml --format pson localhost To test using it as an endpoint for compiling and retrieving catalogs from a remote server, (from my commit), try this: # Terminal 1 $ sbin/puppetmasterd --trace --confdir /tmp/foo --vardir /tmp/foo --debug --manifest ~/bin/test.pp --certname localhost --no-daemonize # Terminal 2 $ sbin/puppetd --trace --debug --confdir /tmp/foo --vardir /tmp/foo --certname localhost --server localhost --test --report # Terminal 3, actual testing $ puppet catalog find localhost --certname localhost --server localhost --mode master --confdir /tmp/foo --vardir /tmp/foo --trace --terminus rest This compiles a test catalog (assuming that ~/bin/test.pp exists) and returns it. With the right auth setup, you can also get facts: $ puppet facts find localhost --certname localhost --server localhost --mode master --confdir /tmp/foo --vardir /tmp/foo --trace --terminus rest Or use IRB to do the same thing: $ irb >> require 'puppet/interface' => true >> interface = Puppet::Interface[:facts, '1.0.0'] => # - >> facts = interface.find("myhost"); nil + >> facts = interface.find("myhost") Like I said, a prototype, but I'd love it if people would play it with some and make some recommendations. Extending --------- Like most parts of Puppet, these are easy to extend. Just drop a new action into a given interface's directory. E.g.: $ cat lib/puppet/interface/catalog/select.rb # Select and show a list of resources of a given type. Puppet::Interface.define(:catalog, '1.0.0') do action :select do invoke do |host,type| catalog = Puppet::Resource::Catalog.indirection.find(host) catalog.resources.reject { |res| res.type != type }.each { |res| puts res } end end end $ puppet catalog select localhost Class Class[main] Class[Settings] $ Notice that this gets loaded automatically when you try to use it. So, if you have a simple command you've written, such as for cleaning up nodes or diffing catalogs, you an port it to this framework and it should fit cleanly. + +Also note that interfaces are versioned. These version numbers are interpreted according to Semantic Versioning (http://semver.org). 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 64f1bfe49..a667c6b75 100644 --- a/lib/puppet/interface.rb +++ b/lib/puppet/interface.rb @@ -1,97 +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, &blk) + def define(name, version, &block) if interface?(name, version) interface = Puppet::Interface::InterfaceCollection[name, version] - interface.instance_eval(&blk) if blk else - interface = self.new(name, :version => version, &blk) + 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, options = {}, &block) - unless options[:version] - raise ArgumentError, "Interface #{name} declared without version!" + 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 - options.each { |opt, val| send(opt.to_s + "=", val) } - instance_eval(&block) if block + instance_eval(&block) if block_given? end # Try to find actions defined in other files. def load_actions - path = "puppet/interface/#{name}" + 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}, :version => #{version.inspect})" + "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 d626c4f72..92e2933fe 100644 --- a/lib/puppet/interface/interface_collection.rb +++ b/lib/puppet/interface/interface_collection.rb @@ -1,52 +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/*.rb").collect { |f| f.sub(/\.rb/, '') }.each do |file| + 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 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/action_builder_spec.rb b/spec/unit/interface/action_builder_spec.rb index 2c2f3b14c..27e817fe9 100644 --- a/spec/unit/interface/action_builder_spec.rb +++ b/spec/unit/interface/action_builder_spec.rb @@ -1,30 +1,30 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') require 'puppet/interface/action_builder' describe Puppet::Interface::ActionBuilder do describe "::build" do it "should build an action" do action = Puppet::Interface::ActionBuilder.build(nil,:foo) do end action.should be_a(Puppet::Interface::Action) action.name.should == "foo" end it "should define a method on the interface which invokes the action" do - interface = Puppet::Interface.new(:action_builder_test_interface, :version => '0.0.1') + interface = Puppet::Interface.new(:action_builder_test_interface, '0.0.1') action = Puppet::Interface::ActionBuilder.build(interface, :foo) do invoke do "invoked the method" end end interface.foo.should == "invoked the method" end it "should require a block" do lambda { Puppet::Interface::ActionBuilder.build(nil,:foo) }.should raise_error("Action 'foo' must specify a block") end end end diff --git a/spec/unit/interface/action_spec.rb b/spec/unit/interface/action_spec.rb index 246ae9622..292caabb9 100644 --- a/spec/unit/interface/action_spec.rb +++ b/spec/unit/interface/action_spec.rb @@ -1,75 +1,75 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') require 'puppet/interface/action' describe Puppet::Interface::Action do describe "when validating the action name" do it "should require a name" do lambda { Puppet::Interface::Action.new(nil,nil) }.should raise_error("'' is an invalid action name") end it "should not allow empty names" do lambda { Puppet::Interface::Action.new(nil,'') }.should raise_error("'' is an invalid action name") end it "should not allow names with whitespace" do lambda { Puppet::Interface::Action.new(nil,'foo bar') }.should raise_error("'foo bar' is an invalid action name") end it "should not allow names beginning with dashes" do lambda { Puppet::Interface::Action.new(nil,'-foobar') }.should raise_error("'-foobar' is an invalid action name") end end describe "when invoking" do it "should be able to call other actions on the same object" do - interface = Puppet::Interface.new(:my_interface, :version => '0.0.1') do + interface = Puppet::Interface.new(:my_interface, '0.0.1') do action(:foo) do invoke { 25 } end action(:bar) do invoke { "the value of foo is '#{foo}'" } end end interface.foo.should == 25 interface.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::Interface::MyInterfaceBaseClass < Puppet::Interface 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 - interface = Puppet::Interface::MyInterfaceBaseClass.new(:my_inherited_interface, :version => '0.0.1') do + interface = Puppet::Interface::MyInterfaceBaseClass.new(:my_inherited_interface, '0.0.1') do action(:baz) do invoke { "the value of foo in baz is '#{foo}'" } end action(:qux) do invoke { baz } end end interface.foo.should == 25 interface.bar.should == "the value of foo is '25'" interface.quux.should == "qux told me the value of foo in baz is '25'" interface.baz.should == "the value of foo in baz is '25'" interface.qux.should == "the value of foo in baz is '25'" end end end diff --git a/spec/unit/interface/indirector_spec.rb b/spec/unit/interface/indirector_spec.rb index b14058eca..4b2beaefc 100644 --- a/spec/unit/interface/indirector_spec.rb +++ b/spec/unit/interface/indirector_spec.rb @@ -1,55 +1,55 @@ #!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper.rb') require 'puppet/interface/indirector' describe Puppet::Interface::Indirector do before do - @instance = Puppet::Interface::Indirector.new(:test, :version => '0.0.1') + @instance = Puppet::Interface::Indirector.new(:test, '0.0.1') @indirection = stub 'indirection', :name => :stub_indirection @instance.stubs(:indirection).returns @indirection end it "should be able to return a list of indirections" do Puppet::Interface::Indirector.indirections.should be_include("catalog") end it "should be able to return a list of terminuses for a given indirection" do Puppet::Interface::Indirector.terminus_classes(:catalog).should be_include("compiler") end describe "as an instance" do it "should be able to determine its indirection" do # Loading actions here an get, um, complicated Puppet::Interface.stubs(:load_actions) - Puppet::Interface::Indirector.new(:catalog, :version => '0.0.1').indirection.should equal(Puppet::Resource::Catalog.indirection) + Puppet::Interface::Indirector.new(:catalog, '0.0.1').indirection.should equal(Puppet::Resource::Catalog.indirection) end end [:find, :search, :save, :destroy].each do |method| it "should define a '#{method}' action" do Puppet::Interface::Indirector.should be_action(method) end it "should just call the indirection method when the '#{method}' action is invoked" do @instance.indirection.expects(method).with(:test, "myargs") @instance.send(method, :test, "myargs") end end it "should be able to override its indirection name" do @instance.set_indirection_name :foo @instance.indirection_name.should == :foo end it "should be able to set its terminus class" do @instance.indirection.expects(:terminus_class=).with(:myterm) @instance.set_terminus(:myterm) end it "should define a class-level 'info' action" do Puppet::Interface::Indirector.should be_action(:info) end end diff --git a/spec/unit/interface/interface_collection_spec.rb b/spec/unit/interface/interface_collection_spec.rb index 193d31b1e..3e4b9d624 100644 --- a/spec/unit/interface/interface_collection_spec.rb +++ b/spec/unit/interface/interface_collection_spec.rb @@ -1,101 +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, :version => '0.0.1') + 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 520060cba..cf7d209da 100755 --- a/spec/unit/interface_spec.rb +++ b/spec/unit/interface_spec.rb @@ -1,83 +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 "#interface" do + describe "#define" do it "should register the interface" do - interface = Puppet::Interface[:interface_test_register, '0.0.1'] + 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[:interface_test_load_actions, '0.0.1'] + Puppet::Interface.define(:interface_test_load_actions, '0.0.1') end it "should require a version number" do - proc { Puppet::Interface[:no_version] }.should raise_error(ArgumentError) + 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(/declared without version/) + 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, :version => '0.0.1') 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, :version => '0.0.1').name.should == :me + Puppet::Interface.new(:me,'0.0.1').name.should == :me end it "should stringify with its own name" do - Puppet::Interface.new(:me, :version => '0.0.1').to_s.should =~ /\bme\b/ + 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, :version => '0.0.1') + 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, :version => '0.0.1').default_format.should == :pson + 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 set any provided options" do - Puppet::Interface.new(:me, :version => 1, :verb => "foo").verb.should == "foo" - 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