diff --git a/lib/puppet/interface/face_collection.rb b/lib/puppet/interface/face_collection.rb index 6e6afc545..baa424692 100644 --- a/lib/puppet/interface/face_collection.rb +++ b/lib/puppet/interface/face_collection.rb @@ -1,143 +1,138 @@ # -*- coding: utf-8 -*- require 'puppet/interface' module Puppet::Interface::FaceCollection SEMVER_VERSION = /^(\d+)\.(\d+)\.(\d+)([A-Za-z][0-9A-Za-z-]*|)$/ @faces = Hash.new { |hash, key| hash[key] = {} } def self.faces unless @loaded @loaded = true $LOAD_PATH.each do |dir| - next unless FileTest.directory?(dir) - Dir.chdir(dir) do - Dir.glob("puppet/face/*.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 + Dir.glob("#{dir}/puppet/face/*.rb"). + collect {|f| File.basename(f, '.rb') }. + each {|name| self[name, :current] } end end - return @faces.keys.select {|name| @faces[name].length > 0 } + @faces.keys.select {|name| @faces[name].length > 0 } end def self.validate_version(version) !!(SEMVER_VERSION =~ version.to_s) end def self.semver_to_array(v) parts = SEMVER_VERSION.match(v).to_a[1..4] parts[0..2] = parts[0..2].map { |e| e.to_i } parts end def self.cmp_semver(a, b) a, b = [a, b].map do |x| semver_to_array(x) 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.prefix_match?(desired, target) # Can't meaningfully do a prefix match with current on either side. return false if desired == :current return false if target == :current # REVISIT: Should probably fail if the matcher is not valid. prefix = desired.split('.').map {|x| x =~ /^\d+$/ and x.to_i } have = semver_to_array(target) while want = prefix.shift do return false unless want == have.shift end return true end def self.[](name, version) name = underscorize(name) get_face(name, version) or load_face(name, version) end # get face from memory, without loading. def self.get_face(name, desired_version) return nil unless @faces.has_key? name return @faces[name][:current] if desired_version == :current found = @faces[name].keys.select {|v| prefix_match?(desired_version, v) }.sort.last return @faces[name][found] end # try to load the face, and return it. def self.load_face(name, version) # We always load the current version file; the common case is that we have # the expected version and any compatibility versions in the same file, # the default. Which means that this is almost always the case. # # We use require to avoid executing the code multiple times, like any # other Ruby library that we might want to use. --daniel 2011-04-06 begin require "puppet/face/#{name}" # If we wanted :current, we need to index to find that; direct version # requests just work™ as they go. --daniel 2011-04-06 if version == :current then # We need to find current out of this. This is the largest version # number that doesn't have a dedicated on-disk file present; those # represent "experimental" versions of faces, which we don't fully # support yet. # # We walk the versions from highest to lowest and take the first version # that is not defined in an explicitly versioned file on disk as the # current version. # # This constrains us to only ship experimental versions with *one* # version in the file, not multiple, but given you can't reliably load # them except by side-effect when you ignore that rule this seems safe # enough... # # Given those constraints, and that we are not going to ship a versioned # interface that is not :current in this release, we are going to leave # these thoughts in place, and just punt on the actual versioning. # # When we upgrade the core to support multiple versions we can solve the # problems then; as lazy as possible. # # We do support multiple versions in the same file, though, so we sort # versions here and return the last item in that set. # # --daniel 2011-04-06 latest_ver = @faces[name].keys.sort {|a, b| cmp_semver(a, b) }.last @faces[name][:current] = @faces[name][latest_ver] end rescue LoadError => e raise unless e.message =~ %r{-- puppet/face/#{name}$} # ...guess we didn't find the file; return a much better problem. + rescue SyntaxError => e + raise unless e.message =~ %r{puppet/face/#{name}\.rb:\d+: } + Puppet.err "Failed to load face #{name}:\n#{e}" + # ...but we just carry on after complaining. end return get_face(name, version) end def self.register(face) @faces[underscorize(face.name)][face.version] = face end def self.underscorize(name) unless name.to_s =~ /^[-_a-z]+$/i then raise ArgumentError, "#{name.inspect} (#{name.class}) is not a valid face name" end name.to_s.downcase.split(/[-_]/).join('_').to_sym end end diff --git a/spec/fixtures/faulty_face/puppet/face/syntax.rb b/spec/fixtures/faulty_face/puppet/face/syntax.rb new file mode 100644 index 000000000..3b1e36c3f --- /dev/null +++ b/spec/fixtures/faulty_face/puppet/face/syntax.rb @@ -0,0 +1,8 @@ +Puppet::Face.define(:syntax, '1.0.0') do + action :foo do + when_invoked do |whom| + "hello, #{whom}" + end + # This 'end' is deliberately omitted, to induce a syntax error. + # Please don't fix that, as it is used for testing. --daniel 2011-05-02 +end diff --git a/spec/unit/interface/face_collection_spec.rb b/spec/unit/interface/face_collection_spec.rb index 4358ef4b6..4ad8787c5 100755 --- a/spec/unit/interface/face_collection_spec.rb +++ b/spec/unit/interface/face_collection_spec.rb @@ -1,163 +1,180 @@ #!/usr/bin/env rspec require 'spec_helper' require 'tmpdir' require 'puppet/interface/face_collection' describe Puppet::Interface::FaceCollection do # To avoid cross-pollution we have to save and restore both the hash # containing all the interface data, and the array used by require. Restoring # both means that we don't leak side-effects across the code. --daniel 2011-04-06 # # Worse luck, we *also* need to flush $" of anything defining a face, # because otherwise we can cross-pollute from other test files and end up # with no faces loaded, but the require value set true. --daniel 2011-04-10 before :each do @original_faces = subject.instance_variable_get("@faces").dup @original_required = $".dup $".delete_if do |path| path =~ %r{/face/.*\.rb$} end subject.instance_variable_get(:@faces).clear subject.instance_variable_set(:@loaded, false) end after :each do subject.instance_variable_set(:@faces, @original_faces) - $".clear ; @original_required.each do |item| $" << item end + @original_required.each {|f| $".push f unless $".include? f } end describe "::prefix_match?" do # want have { ['1.0.0', '1.0.0'] => true, ['1.0', '1.0.0'] => true, ['1', '1.0.0'] => true, ['1.0.0', '1.1.0'] => false, ['1.0', '1.1.0'] => false, ['1', '1.1.0'] => true, ['1.0.1', '1.0.0'] => false, }.each do |data, result| it "should return #{result.inspect} for prefix_match?(#{data.join(', ')})" do subject.prefix_match?(*data).should == result end end end describe "::validate_version" do { '10.10.10' => true, '1.2.3.4' => false, '10.10.10beta' => true, '10.10' => false, '123' => false, 'v1.1.1' => false, }.each do |input, result| it "should#{result ? '' : ' not'} permit #{input.inspect}" do subject.validate_version(input).should(result ? be_true : be_false) end end end describe "::[]" do before :each do subject.instance_variable_get("@faces")[:foo]['0.0.1'] = 10 end it "should return the face with the given name" do subject["foo", '0.0.1'].should == 10 end it "should attempt to load the face if it isn't found" do subject.expects(:require).with('puppet/face/bar') subject["bar", '0.0.1'] end it "should attempt to load the default face for the specified version :current" do subject.expects(:require).with('puppet/face/fozzie') subject['fozzie', :current] end it "should return true if the face specified is registered" do subject.instance_variable_get("@faces")[:foo]['0.0.1'] = 10 subject["foo", '0.0.1'].should == 10 end it "should attempt to require the face if it is not registered" do subject.expects(:require).with do |file| subject.instance_variable_get("@faces")[:bar]['0.0.1'] = true file == 'puppet/face/bar' end subject["bar", '0.0.1'].should be_true end it "should return false if the face is not registered" do subject.stubs(:require).returns(true) subject["bar", '0.0.1'].should be_false end it "should return false if the face file itself is missing" do subject.stubs(:require). raises(LoadError, 'no such file to load -- puppet/face/bar') subject["bar", '0.0.1'].should be_false end it "should register the version loaded by `:current` as `:current`" do subject.expects(:require).with do |file| subject.instance_variable_get("@faces")[:huzzah]['2.0.1'] = :huzzah_face file == 'puppet/face/huzzah' end subject["huzzah", :current] subject.instance_variable_get("@faces")[:huzzah][:current].should == :huzzah_face end context "with something on disk" do it "should register the version loaded from `puppet/face/{name}` as `:current`" do subject["huzzah", '2.0.1'].should be subject["huzzah", :current].should be Puppet::Face[:huzzah, '2.0.1'].should == Puppet::Face[:huzzah, :current] end it "should index :current when the code was pre-required" do subject.instance_variable_get("@faces")[:huzzah].should_not be_key :current require 'puppet/face/huzzah' subject[:huzzah, :current].should be_true end end it "should not cause an invalid face to be enumerated later" do subject[:there_is_no_face, :current].should be_false subject.faces.should_not include :there_is_no_face end end describe "::register" do it "should store the face by name" do face = Puppet::Face.new(:my_face, '0.0.1') subject.register(face) subject.instance_variable_get("@faces").should == {:my_face => {'0.0.1' => face}} 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 face name/ end end end + + context "faulty faces" do + before :each do + $:.unshift "#{PuppetSpec::FIXTURE_DIR}/faulty_face" + end + + after :each do + $:.delete_if {|x| x == "#{PuppetSpec::FIXTURE_DIR}/faulty_face"} + end + + it "should not die if a face has a syntax error" do + subject.faces.should be_include :help + subject.faces.should_not be_include :syntax + @logs.should_not be_empty + @logs.first.message.should =~ /syntax error/ + end + end end