diff --git a/lib/puppet/interface/interface_collection.rb b/lib/puppet/interface/interface_collection.rb index 51b7534a0..115892397 100644 --- a/lib/puppet/interface/interface_collection.rb +++ b/lib/puppet/interface/interface_collection.rb @@ -1,52 +1,87 @@ 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 + 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.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) end def self.interface?(name, version) 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/interface/interface_collection_spec.rb b/spec/unit/interface/interface_collection_spec.rb index a404d85a6..a943c2ec2 100644 --- a/spec/unit/interface/interface_collection_spec.rb +++ b/spec/unit/interface/interface_collection_spec.rb @@ -1,101 +1,203 @@ #!/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 "::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 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 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