diff options
author | Luke Kanies <luke@madstop.com> | 2007-10-22 15:24:50 -0500 |
---|---|---|
committer | Luke Kanies <luke@madstop.com> | 2007-10-22 15:24:50 -0500 |
commit | 393a3e8743f503543ad34a874e9296d684b0d49b (patch) | |
tree | d05b94cb9424b0b9a41a2bf3a9b541e0af26d8b5 | |
parent | aab157e77bf471269ef0403576eca0e77e6f51ec (diff) | |
download | puppet-393a3e8743f503543ad34a874e9296d684b0d49b.tar.gz puppet-393a3e8743f503543ad34a874e9296d684b0d49b.tar.xz puppet-393a3e8743f503543ad34a874e9296d684b0d49b.zip |
Adding a Fileset class for managing sets of files. This
is the new server-side for file recursion, and I'll next be
hooking it to the fileserving 'search' methods. This is
basically a mechanism for abstracting that search functionality
into a single class.
-rw-r--r-- | lib/puppet/file_serving/fileset.rb | 138 | ||||
-rwxr-xr-x | spec/unit/file_serving/fileset.rb | 230 |
2 files changed, 368 insertions, 0 deletions
diff --git a/lib/puppet/file_serving/fileset.rb b/lib/puppet/file_serving/fileset.rb new file mode 100644 index 000000000..7f7b9fc2d --- /dev/null +++ b/lib/puppet/file_serving/fileset.rb @@ -0,0 +1,138 @@ +# +# Created by Luke Kanies on 2007-10-22. +# Copyright (c) 2007. All rights reserved. + +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 + + # Find our collection of files. 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 find + 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{^#{@path}/*}, '') } + + # And add the path itself. + result.unshift(".") + + result + end + + # Should we ignore this path? + def ignore?(path) + # 'detect' normally returns the found result, whereas we just want true/false. + ! @ignore.detect { |pattern| File.fnmatch?(pattern, path) }.nil? + end + + def ignore=(values) + values = [values] unless values.is_a?(Array) + @ignore = values + end + + def initialize(path, options = {}) + raise ArgumentError.new("Fileset paths must be fully qualified") unless path =~ /^#{::File::SEPARATOR}/ + + @path = path + + # Set our defaults. + @ignore = [] + @links = :manage + @recurse = false + + options.each do |option, value| + method = option.to_s + "=" + begin + send(method, value) + rescue NoMethodError + raise ArgumentError, "Invalid option '%s'" % option + end + end + + raise ArgumentError.new("Fileset paths must exist") unless stat = stat(path) + end + + def links=(links) + links = links.intern if links.is_a?(String) + raise(ArgumentError, "Invalid :links value '%s'" % links) unless [:manage, :follow].include?(links) + @links = links + @stat_method = links == :manage ? :lstat : :stat + end + + # Should we recurse further? This is basically a single + # place for all of the logic around recursion. + def recurse?(depth) + # If recurse is true, just return true + return true if self.recurse == true + + # Return false if the value is false or zero. + return false if [false, 0].include?(self.recurse) + + # Return true if our current depth is less than the allowed recursion depth. + return true if self.recurse.is_a?(Fixnum) and depth <= self.recurse + + # Else, return false. + return false + end + + private + + # 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 + # Start out with just our base directory. + current_dirs = [@path] + + next_dirs = [] + + depth = 1 + + result = [] + return result unless recurse?(depth) + + while dir_path = current_dirs.shift or ((depth += 1) and recurse?(depth) and current_dirs = next_dirs and next_dirs = [] and dir_path = current_dirs.shift) + next unless stat = stat(dir_path) + next unless stat.directory? + + Dir.entries(dir_path).each do |file_path| + next if [".", ".."].include?(file_path) + + # Note that this also causes matching directories not + # to be recursed into. + next if ignore?(file_path) + + # Add it to our list of files to return + result << File.join(dir_path, file_path) + + # And to our list of files/directories to iterate over. + next_dirs << File.join(dir_path, file_path) + end + end + + return result + end + + # Stat a given file, using the links-appropriate method. + def stat(path) + unless defined?(@stat_method) + @stat_method = self.links == :manage ? :lstat : :stat + end + + begin + return File.send(@stat_method, path) + rescue + # If this happens, it is almost surely because we're + # trying to manage a link to a file that does not exist. + return nil + end + end +end diff --git a/spec/unit/file_serving/fileset.rb b/spec/unit/file_serving/fileset.rb new file mode 100755 index 000000000..518d7298d --- /dev/null +++ b/spec/unit/file_serving/fileset.rb @@ -0,0 +1,230 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +require 'puppet/file_serving/fileset' + +describe Puppet::FileServing::Fileset, " when initializing" do + it "should require a path" do + proc { Puppet::FileServing::Fileset.new }.should raise_error(ArgumentError) + end + + it "should fail if its path is not fully qualified" do + proc { Puppet::FileServing::Fileset.new("some/file") }.should raise_error(ArgumentError) + end + + it "should fail if its path does not exist" do + File.expects(:lstat).with("/some/file").returns nil + proc { Puppet::FileServing::Fileset.new("/some/file") }.should raise_error(ArgumentError) + end + + it "should accept a 'recurse' option" do + File.expects(:lstat).with("/some/file").returns stub("stat") + set = Puppet::FileServing::Fileset.new("/some/file", :recurse => true) + set.recurse.should be_true + end + + it "should accept an 'ignore' option" do + File.expects(:lstat).with("/some/file").returns stub("stat") + set = Puppet::FileServing::Fileset.new("/some/file", :ignore => ".svn") + set.ignore.should == [".svn"] + end + + it "should accept a 'links' option" do + File.expects(:lstat).with("/some/file").returns stub("stat") + set = Puppet::FileServing::Fileset.new("/some/file", :links => :manage) + set.links.should == :manage + end + + it "should fail if 'links' is set to anything other than :manage or :follow" do + proc { Puppet::FileServing::Fileset.new("/some/file", :links => :whatever) }.should raise_error(ArgumentError) + end + + it "should default to 'false' for recurse" do + File.expects(:lstat).with("/some/file").returns stub("stat") + Puppet::FileServing::Fileset.new("/some/file").recurse.should == false + end + + it "should default to an empty ignore list" do + File.expects(:lstat).with("/some/file").returns stub("stat") + Puppet::FileServing::Fileset.new("/some/file").ignore.should == [] + end + + it "should default to :manage for links" do + File.expects(:lstat).with("/some/file").returns stub("stat") + Puppet::FileServing::Fileset.new("/some/file").links.should == :manage + end +end + +describe Puppet::FileServing::Fileset, " when determining whether to recurse" do + before do + @path = "/my/path" + File.expects(:lstat).with(@path).returns stub("stat") + @fileset = Puppet::FileServing::Fileset.new(@path) + end + + it "should always recurse if :recurse is set to 'true'" do + @fileset.recurse = true + @fileset.recurse?(0).should be_true + end + + it "should never recurse if :recurse is set to 'false'" do + @fileset.recurse = false + @fileset.recurse?(-1).should be_false + end + + it "should recurse if :recurse is set to an integer and the current depth is less than that integer" do + @fileset.recurse = 1 + @fileset.recurse?(0).should be_true + end + + it "should recurse if :recurse is set to an integer and the current depth is equal to that integer" do + @fileset.recurse = 1 + @fileset.recurse?(1).should be_true + end + + it "should not recurse if :recurse is set to an integer and the current depth is great than that integer" do + @fileset.recurse = 1 + @fileset.recurse?(2).should be_false + end + + it "should not recurse if :recurse is set to 0" do + @fileset.recurse = 0 + @fileset.recurse?(-1).should be_false + end +end + +describe Puppet::FileServing::Fileset, " when recursing" do + before do + @path = "/my/path" + File.expects(: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) + File.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) + File.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| + @files << File.join(subdir, file) # relative path + File.stubs(stat_method).with(File.join(subpath, file)).returns(@filestat) + end + end + end + + it "should recurse through the whole file tree if :recurse is set to 'true'" do + mock_dir_structure(@path) + @fileset.stubs(:recurse?).returns(true) + @fileset.find.sort.should == @files.sort + end + + it "should not recurse if :recurse is set to 'false'" do + mock_dir_structure(@path) + @fileset.stubs(:recurse?).returns(false) + @fileset.find.should == %w{.} + end + + # It seems like I should stub :recurse? here, or that I shouldn't stub the + # examples above, but... + it "should recurse to the level set if :recurse is set to an integer" do + mock_dir_structure(@path) + @fileset.recurse = 1 + @fileset.find.should == %w{. one two .svn CVS} + end + + it "should ignore the '.' and '..' directories in subdirectories" do + mock_dir_structure(@path) + @fileset.recurse = true + @fileset.find.sort.should == @files.sort + end + + it "should ignore files that match a single pattern in the ignore list" do + mock_dir_structure(@path) + @fileset.recurse = true + @fileset.ignore = ".svn" + @fileset.find.find { |file| file.include?(".svn") }.should be_nil + end + + it "should ignore 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.find.find { |file| file.include?(".svn") or file.include?("CVS") }.should be_nil + end + + it "should use File.stat if :links is set to :follow" do + mock_dir_structure(@path, :stat) + @fileset.recurse = true + @fileset.links = :follow + @fileset.find.sort.should == @files.sort + end + + it "should use File.lstat if :links is set to :manage" do + mock_dir_structure(@path, :lstat) + @fileset.recurse = true + @fileset.links = :manage + @fileset.find.sort.should == @files.sort + end +end + +describe Puppet::FileServing::Fileset, " when following links that point to missing files" do + before do + @path = "/my/path" + File.expects(:lstat).with(@path).returns stub("stat", :directory? => true) + @fileset = Puppet::FileServing::Fileset.new(@path) + @fileset.links = :follow + @fileset.recurse = true + + @stat = stub 'stat', :directory? => true + + File.expects(:stat).with(@path).returns(@stat) + File.expects(:stat).with(File.join(@path, "mylink")).raises(Errno::ENOENT) + Dir.stubs(:entries).with(@path).returns(["mylink"]) + end + + it "should not fail" do + proc { @fileset.find }.should_not raise_error + end + + it "should still manage the link" do + @fileset.find.sort.should == %w{. mylink}.sort + end +end + +describe Puppet::FileServing::Fileset, " when ignoring" do + before do + @path = "/my/path" + File.expects(:lstat).with(@path).returns stub("stat", :directory? => true) + @fileset = Puppet::FileServing::Fileset.new(@path) + end + + it "should use ruby's globbing to determine what files should be ignored" do + @fileset.ignore = ".svn" + File.expects(:fnmatch?).with(".svn", "my_file") + @fileset.ignore?("my_file") + end + + it "should ignore files whose paths match a single provided ignore value" do + @fileset.ignore = ".svn" + File.stubs(:fnmatch?).with(".svn", "my_file").returns true + @fileset.ignore?("my_file").should be_true + end + + it "should ignore files whose paths match any of multiple provided ignore values" do + @fileset.ignore = [".svn", "CVS"] + File.stubs(:fnmatch?).with(".svn", "my_file").returns false + File.stubs(:fnmatch?).with("CVS", "my_file").returns true + @fileset.ignore?("my_file").should be_true + end +end |