diff --git a/lib/puppet/file_serving/fileset.rb b/lib/puppet/file_serving/fileset.rb index 6e982708b..a1d9d54d7 100644 --- a/lib/puppet/file_serving/fileset.rb +++ b/lib/puppet/file_serving/fileset.rb @@ -1,172 +1,172 @@ require 'find' require 'puppet/file_serving' require 'puppet/file_serving/metadata' # Operate recursively on a path, returning a set of file paths. class Puppet::FileServing::Fileset attr_reader :path, :ignore, :links attr_accessor :recurse, :recurselimit, :checksum_type # Produce a hash of files, with merged so that earlier files # with the same postfix win. E.g., /dir1/subfile beats /dir2/subfile. # It's a hash because we need to know the relative path of each file, # and the base directory. # This will probably only ever be used for searching for plugins. def self.merge(*filesets) result = {} filesets.each do |fileset| fileset.files.each do |file| result[file] ||= fileset.path end end result end def initialize(path, options = {}) if Puppet.features.microsoft_windows? # REMIND: UNC path path = path.chomp(File::SEPARATOR) unless path =~ /^[A-Za-z]:\/$/ else path = path.chomp(File::SEPARATOR) unless path == File::SEPARATOR end raise ArgumentError.new("Fileset paths must be fully qualified: #{path}") unless Puppet::Util.absolute_path?(path) @path = path # Set our defaults. self.ignore = [] self.links = :manage @recurse = false @recurselimit = :infinite if options.is_a?(Puppet::Indirector::Request) initialize_from_request(options) else initialize_from_hash(options) end raise ArgumentError.new("Fileset paths must exist") unless valid?(path) raise ArgumentError.new("Fileset recurse parameter must not be a number anymore, please use recurselimit") if @recurse.is_a?(Integer) end # Return a list of all files in our fileset. This is different from the # normal definition of find in that we support specific levels # of recursion, which means we need to know when we're going another # level deep, which Find doesn't do. def files files = perform_recursion # Now strip off the leading path, so each file becomes relative, and remove # any slashes that might end up at the beginning of the path. result = files.collect { |file| file.sub(%r{^#{Regexp.escape(@path)}/*}, '') } # And add the path itself. result.unshift(".") result end def ignore=(values) values = [values] unless values.is_a?(Array) - @ignore = values + @ignore = values.collect(&:to_s) end def links=(links) links = links.to_sym raise(ArgumentError, "Invalid :links value '#{links}'") unless [:manage, :follow].include?(links) @links = links @stat_method = @links == :manage ? :lstat : :stat end private def initialize_from_hash(options) options.each do |option, value| method = option.to_s + "=" begin send(method, value) rescue NoMethodError raise ArgumentError, "Invalid option '#{option}'", $!.backtrace end end end def initialize_from_request(request) [:links, :ignore, :recurse, :recurselimit, :checksum_type].each do |param| if request.options.include?(param) # use 'include?' so the values can be false value = request.options[param] elsif request.options.include?(param.to_s) value = request.options[param.to_s] end next if value.nil? value = true if value == "true" value = false if value == "false" value = Integer(value) if value.is_a?(String) and value =~ /^\d+$/ send(param.to_s + "=", value) end end FileSetEntry = Struct.new(:depth, :path, :ignored, :stat_method) do def down_level(to) FileSetEntry.new(depth + 1, File.join(path, to), ignored, stat_method) end def basename File.basename(path) end def children return [] unless directory? Dir.entries(path). reject { |child| ignore?(child) }. collect { |child| down_level(child) } end def ignore?(child) return true if child == "." || child == ".." return false if ignored == [nil] ignored.any? { |pattern| File.fnmatch?(pattern, child) } end def directory? Puppet::FileSystem.send(stat_method, path).directory? rescue Errno::ENOENT, Errno::EACCES false end end # Pull the recursion logic into one place. It's moderately hairy, and this # allows us to keep the hairiness apart from what we do with the files. def perform_recursion current_dirs = [FileSetEntry.new(0, @path, @ignore, @stat_method)] result = [] while entry = current_dirs.shift if continue_recursion_at?(entry.depth + 1) entry.children.each do |child| result << child.path current_dirs << child end end end result end def valid?(path) Puppet::FileSystem.send(@stat_method, path) true rescue Errno::ENOENT, Errno::EACCES false end def continue_recursion_at?(depth) # recurse if told to, and infinite recursion or current depth not at the limit self.recurse && (self.recurselimit == :infinite || depth <= self.recurselimit) end end diff --git a/spec/unit/file_serving/fileset_spec.rb b/spec/unit/file_serving/fileset_spec.rb index 463d2a2b6..4fa36f6d8 100755 --- a/spec/unit/file_serving/fileset_spec.rb +++ b/spec/unit/file_serving/fileset_spec.rb @@ -1,337 +1,351 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/file_serving/fileset' describe Puppet::FileServing::Fileset do include PuppetSpec::Files let(:somefile) { make_absolute("/some/file") } context "when initializing" do it "requires a path" do expect { Puppet::FileServing::Fileset.new }.to raise_error(ArgumentError) end it "fails if its path is not fully qualified" do expect { Puppet::FileServing::Fileset.new("some/file") }.to raise_error(ArgumentError, "Fileset paths must be fully qualified: some/file") end it "removes a trailing file path separator" do path_with_separator = "#{somefile}#{File::SEPARATOR}" Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat') fileset = Puppet::FileServing::Fileset.new(path_with_separator) fileset.path.should == somefile end it "can be created from the root directory" do path = File.expand_path(File::SEPARATOR) Puppet::FileSystem.expects(:lstat).with(path).returns stub('stat') fileset = Puppet::FileServing::Fileset.new(path) fileset.path.should == path end it "fails if its path does not exist" do Puppet::FileSystem.expects(:lstat).with(somefile).raises(Errno::ENOENT) expect { Puppet::FileServing::Fileset.new(somefile) }.to raise_error(ArgumentError, "Fileset paths must exist") end it "accepts a 'recurse' option" do Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat') set = Puppet::FileServing::Fileset.new(somefile, :recurse => true) set.recurse.should be_true end it "accepts a 'recurselimit' option" do Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat') set = Puppet::FileServing::Fileset.new(somefile, :recurselimit => 3) set.recurselimit.should == 3 end it "accepts an 'ignore' option" do Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat') set = Puppet::FileServing::Fileset.new(somefile, :ignore => ".svn") set.ignore.should == [".svn"] end it "accepts a 'links' option" do Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat') set = Puppet::FileServing::Fileset.new(somefile, :links => :manage) set.links.should == :manage end it "accepts a 'checksum_type' option" do Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat') set = Puppet::FileServing::Fileset.new(somefile, :checksum_type => :test) set.checksum_type.should == :test end it "fails if 'links' is set to anything other than :manage or :follow" do expect { Puppet::FileServing::Fileset.new(somefile, :links => :whatever) }.to raise_error(ArgumentError, "Invalid :links value 'whatever'") end it "defaults to 'false' for recurse" do Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat') Puppet::FileServing::Fileset.new(somefile).recurse.should == false end it "defaults to :infinite for recurselimit" do Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat') Puppet::FileServing::Fileset.new(somefile).recurselimit.should == :infinite end it "defaults to an empty ignore list" do Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat') Puppet::FileServing::Fileset.new(somefile).ignore.should == [] end it "defaults to :manage for links" do Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat') Puppet::FileServing::Fileset.new(somefile).links.should == :manage end describe "using an indirector request" do let(:values) { { :links => :manage, :ignore => %w{a b}, :recurse => true, :recurselimit => 1234 } } let(:stub_file) { stub(somefile, :lstat => stub('stat')) } before :each do Puppet::FileSystem.expects(:lstat).with(somefile).returns stub('stat') end [:recurse, :recurselimit, :ignore, :links].each do |option| it "passes the #{option} option on to the fileset if present" do request = Puppet::Indirector::Request.new(:file_serving, :find, "foo", nil, {option => values[option]}) Puppet::FileServing::Fileset.new(somefile, request).send(option).should == values[option] end end it "converts the integer as a string to their integer counterpart when setting options" do request = Puppet::Indirector::Request.new(:file_serving, :find, "foo", nil, {:recurselimit => "1234"}) Puppet::FileServing::Fileset.new(somefile, request).recurselimit.should == 1234 end it "converts the string 'true' to the boolean true when setting options" do request = Puppet::Indirector::Request.new(:file_serving, :find, "foo", nil, {:recurse => "true"}) Puppet::FileServing::Fileset.new(somefile, request).recurse.should == true end it "converts the string 'false' to the boolean false when setting options" do request = Puppet::Indirector::Request.new(:file_serving, :find, "foo", nil, {:recurse => "false"}) Puppet::FileServing::Fileset.new(somefile, request).recurse.should == false end end end context "when recursing" do before do @path = make_absolute("/my/path") Puppet::FileSystem.stubs(:lstat).with(@path).returns stub('stat', :directory? => true) @fileset = Puppet::FileServing::Fileset.new(@path) @dirstat = stub 'dirstat', :directory? => true @filestat = stub 'filestat', :directory? => false end def mock_dir_structure(path, stat_method = :lstat) Puppet::FileSystem.stubs(stat_method).with(@path).returns @dirstat Dir.stubs(:entries).with(path).returns(%w{one two .svn CVS}) # Keep track of the files we're stubbing. @files = %w{.} %w{one two .svn CVS}.each do |subdir| @files << subdir # relative path subpath = File.join(path, subdir) Puppet::FileSystem.stubs(stat_method).with(subpath).returns @dirstat - Dir.stubs(:entries).with(subpath).returns(%w{.svn CVS file1 file2}) - %w{file1 file2 .svn CVS}.each do |file| + Dir.stubs(:entries).with(subpath).returns(%w{.svn CVS file1 file2 0 false}) + %w{file1 file2 .svn CVS 0 false}.each do |file| @files << File.join(subdir, file) # relative path subfile_path = File.join(subpath, file) Puppet::FileSystem.stubs(stat_method).with(subfile_path).returns(@filestat) end end end MockStat = Struct.new(:path, :directory) do # struct doesn't support thing ending in ? def directory? directory end end MockDirectory = Struct.new(:name, :entries) do def mock(base_path) extend Mocha::API path = File.join(base_path, name) Puppet::FileSystem.stubs(:lstat).with(path).returns MockStat.new(path, true) Dir.stubs(:entries).with(path).returns(['.', '..'] + entries.map(&:name)) entries.each do |entry| entry.mock(path) end end end MockFile = Struct.new(:name) do def mock(base_path) extend Mocha::API path = File.join(base_path, name) Puppet::FileSystem.stubs(:lstat).with(path).returns MockStat.new(path, false) end end it "doesn't ignore pending directories when the last entry at the top level is a file" do structure = MockDirectory.new('path', [MockDirectory.new('dir1', [MockDirectory.new('a', [MockFile.new('f')])]), MockFile.new('file')]) structure.mock(make_absolute('/your')) fileset = Puppet::FileServing::Fileset.new(make_absolute('/your/path')) fileset.recurse = true fileset.links = :manage fileset.files.should == [".", "dir1", "file", "dir1/a", "dir1/a/f"] end it "recurses through the whole file tree if :recurse is set to 'true'" do mock_dir_structure(@path) @fileset.recurse = true @fileset.files.sort.should == @files.sort end it "does not recurse if :recurse is set to 'false'" do mock_dir_structure(@path) @fileset.recurse = false @fileset.files.should == %w{.} end it "recurses to the level set by :recurselimit" do mock_dir_structure(@path) @fileset.recurse = true @fileset.recurselimit = 1 @fileset.files.should == %w{. one two .svn CVS} end it "ignores the '.' and '..' directories in subdirectories" do mock_dir_structure(@path) @fileset.recurse = true @fileset.files.sort.should == @files.sort end it "does not fail if the :ignore value provided is nil" do mock_dir_structure(@path) @fileset.recurse = true @fileset.ignore = nil expect { @fileset.files }.to_not raise_error end it "ignores files that match a single pattern in the ignore list" do mock_dir_structure(@path) @fileset.recurse = true @fileset.ignore = ".svn" @fileset.files.find { |file| file.include?(".svn") }.should be_nil end it "ignores files that match any of multiple patterns in the ignore list" do mock_dir_structure(@path) @fileset.recurse = true @fileset.ignore = %w{.svn CVS} @fileset.files.find { |file| file.include?(".svn") or file.include?("CVS") }.should be_nil end + it "ignores files that match a pattern given as a number" do + mock_dir_structure(@path) + @fileset.recurse = true + @fileset.ignore = [0] + @fileset.files.find { |file| file.include?("0") }.should be_nil + end + + it "ignores files that match a pattern given as a boolean" do + mock_dir_structure(@path) + @fileset.recurse = true + @fileset.ignore = [false] + @fileset.files.find { |file| file.include?("false") }.should be_nil + end + it "uses Puppet::FileSystem#stat if :links is set to :follow" do mock_dir_structure(@path, :stat) @fileset.recurse = true @fileset.links = :follow @fileset.files.sort.should == @files.sort end it "uses Puppet::FileSystem#lstat if :links is set to :manage" do mock_dir_structure(@path, :lstat) @fileset.recurse = true @fileset.links = :manage @fileset.files.sort.should == @files.sort end it "works when paths have regexp significant characters" do @path = make_absolute("/my/path/rV1x2DafFr0R6tGG+1bbk++++TM") stat = stub('dir_stat', :directory? => true) stub_file = stub(@path, :stat => stat, :lstat => stat) Puppet::FileSystem.expects(:lstat).with(@path).returns stub(@path, :stat => stat, :lstat => stat) @fileset = Puppet::FileServing::Fileset.new(@path) mock_dir_structure(@path) @fileset.recurse = true @fileset.files.sort.should == @files.sort end end it "manages the links to missing files" do path = make_absolute("/my/path") stat = stub 'stat', :directory? => true Puppet::FileSystem.expects(:stat).with(path).returns stat Puppet::FileSystem.expects(:lstat).with(path).returns stat link_path = File.join(path, "mylink") Puppet::FileSystem.expects(:stat).with(link_path).raises(Errno::ENOENT) Dir.stubs(:entries).with(path).returns(["mylink"]) fileset = Puppet::FileServing::Fileset.new(path) fileset.links = :follow fileset.recurse = true fileset.files.sort.should == %w{. mylink}.sort end context "when merging other filesets" do before do @paths = [make_absolute("/first/path"), make_absolute("/second/path"), make_absolute("/third/path")] Puppet::FileSystem.stubs(:lstat).returns stub('stat', :directory? => false) @filesets = @paths.collect do |path| Puppet::FileSystem.stubs(:lstat).with(path).returns stub('stat', :directory? => true) Puppet::FileServing::Fileset.new(path, :recurse => true) end Dir.stubs(:entries).returns [] end it "returns a hash of all files in each fileset with the value being the base path" do Dir.expects(:entries).with(make_absolute("/first/path")).returns(%w{one uno}) Dir.expects(:entries).with(make_absolute("/second/path")).returns(%w{two dos}) Dir.expects(:entries).with(make_absolute("/third/path")).returns(%w{three tres}) Puppet::FileServing::Fileset.merge(*@filesets).should == { "." => make_absolute("/first/path"), "one" => make_absolute("/first/path"), "uno" => make_absolute("/first/path"), "two" => make_absolute("/second/path"), "dos" => make_absolute("/second/path"), "three" => make_absolute("/third/path"), "tres" => make_absolute("/third/path"), } end it "includes the base directory from the first fileset" do Dir.expects(:entries).with(make_absolute("/first/path")).returns(%w{one}) Dir.expects(:entries).with(make_absolute("/second/path")).returns(%w{two}) Puppet::FileServing::Fileset.merge(*@filesets)["."].should == make_absolute("/first/path") end it "uses the base path of the first found file when relative file paths conflict" do Dir.expects(:entries).with(make_absolute("/first/path")).returns(%w{one}) Dir.expects(:entries).with(make_absolute("/second/path")).returns(%w{one}) Puppet::FileServing::Fileset.merge(*@filesets)["one"].should == make_absolute("/first/path") end end end