diff --git a/lib/puppet/file_system.rb b/lib/puppet/file_system.rb index 8a37e7e30..738b14090 100644 --- a/lib/puppet/file_system.rb +++ b/lib/puppet/file_system.rb @@ -1,366 +1,379 @@ module Puppet::FileSystem require 'puppet/file_system/path_pattern' require 'puppet/file_system/file_impl' require 'puppet/file_system/memory_file' require 'puppet/file_system/memory_impl' require 'puppet/file_system/uniquefile' # create instance of the file system implementation to use for the current platform @impl = if RUBY_VERSION =~ /^1\.8/ require 'puppet/file_system/file18' Puppet::FileSystem::File18 elsif Puppet::Util::Platform.windows? require 'puppet/file_system/file19windows' Puppet::FileSystem::File19Windows else require 'puppet/file_system/file19' Puppet::FileSystem::File19 end.new() # Allows overriding the filesystem for the duration of the given block. # The filesystem will only contain the given file(s). # # @param files [Puppet::FileSystem::MemoryFile] the files to have available # # @api private # def self.overlay(*files, &block) old_impl = @impl @impl = Puppet::FileSystem::MemoryImpl.new(*files) yield ensure @impl = old_impl end # Opens the given path with given mode, and options and optionally yields it to the given block. # # @api public # def self.open(path, mode, options, &block) @impl.open(assert_path(path), mode, options, &block) end # @return [Object] The directory of this file as an opaque handle # # @api public # def self.dir(path) @impl.dir(assert_path(path)) end # @return [String] The directory of this file as a String # # @api public # def self.dir_string(path) @impl.path_string(@impl.dir(assert_path(path))) end # @return [Boolean] Does the directory of the given path exist? def self.dir_exist?(path) @impl.exist?(@impl.dir(assert_path(path))) end # Creates all directories down to (inclusive) the dir of the given path def self.dir_mkpath(path) @impl.mkpath(@impl.dir(assert_path(path))) end # @return [Object] the name of the file as a opaque handle # # @api public # def self.basename(path) @impl.basename(assert_path(path)) end # @return [String] the name of the file # # @api public # def self.basename_string(path) @impl.path_string(@impl.basename(assert_path(path))) end # @return [Integer] the size of the file # # @api public # def self.size(path) @impl.size(assert_path(path)) end # Allows exclusive updates to a file to be made by excluding concurrent # access using flock. This means that if the file is on a filesystem that # does not support flock, this method will provide no protection. # # While polling to aquire the lock the process will wait ever increasing # amounts of time in order to prevent multiple processes from wasting # resources. # # @param path [Pathname] the path to the file to operate on # @param mode [Integer] The mode to apply to the file if it is created # @param options [Integer] Extra file operation mode information to use # (defaults to read-only mode) # @param timeout [Integer] Number of seconds to wait for the lock (defaults to 300) # @yield The file handle, in read-write mode # @return [Void] # @raise [Timeout::Error] If the timeout is exceeded while waiting to acquire the lock # # @api public # def self.exclusive_open(path, mode, options = 'r', timeout = 300, &block) @impl.exclusive_open(assert_path(path), mode, options, timeout, &block) end # Processes each line of the file by yielding it to the given block # # @api public # def self.each_line(path, &block) @impl.each_line(assert_path(path), &block) end # @return [String] The contents of the file # # @api public # def self.read(path) @impl.read(assert_path(path)) end + # Read a file keeping the original line endings intact. This + # attempts to open files using binary mode using some encoding + # overrides and falling back to IO.read when none of the + # encodings are valid. + # + # @return [String] The contents of the file + # + # @api public + # + def self.read_preserve_line_endings(path) + @impl.read_preserve_line_endings(assert_path(path)) + end + # @return [String] The binary contents of the file # # @api public # def self.binread(path) @impl.binread(assert_path(path)) end # Determines if a file exists by verifying that the file can be stat'd. # Will follow symlinks and verify that the actual target path exists. # # @return [Boolean] true if the named file exists. # # @api public # def self.exist?(path) @impl.exist?(assert_path(path)) end # Determines if a file is a directory. # # @return [Boolean] true if the given file is a directory. # # @api public def self.directory?(path) @impl.directory?(assert_path(path)) end # Determines if a file is a file. # # @return [Boolean] true if the given file is a file. # # @api public def self.file?(path) @impl.file?(assert_path(path)) end # Determines if a file is executable. # # @todo Should this take into account extensions on the windows platform? # # @return [Boolean] true if this file can be executed # # @api public # def self.executable?(path) @impl.executable?(assert_path(path)) end # @return [Boolean] Whether the file is writable by the current process # # @api public # def self.writable?(path) @impl.writable?(assert_path(path)) end # Touches the file. On most systems this updates the mtime of the file. # # @api public # def self.touch(path) @impl.touch(assert_path(path)) end # Creates directories for all parts of the given path. # # @api public # def self.mkpath(path) @impl.mkpath(assert_path(path)) end # @return [Array] references to all of the children of the given # directory path, excluding `.` and `..`. # @api public def self.children(path) @impl.children(assert_path(path)) end # Creates a symbolic link dest which points to the current file. # If dest already exists: # # * and is a file, will raise Errno::EEXIST # * and is a directory, will return 0 but perform no action # * and is a symlink referencing a file, will raise Errno::EEXIST # * and is a symlink referencing a directory, will return 0 but perform no action # # With the :force option set to true, when dest already exists: # # * and is a file, will replace the existing file with a symlink (DANGEROUS) # * and is a directory, will return 0 but perform no action # * and is a symlink referencing a file, will modify the existing symlink # * and is a symlink referencing a directory, will return 0 but perform no action # # @param dest [String] The path to create the new symlink at # @param [Hash] options the options to create the symlink with # @option options [Boolean] :force overwrite dest # @option options [Boolean] :noop do not perform the operation # @option options [Boolean] :verbose verbose output # # @raise [Errno::EEXIST] dest already exists as a file and, :force is not set # # @return [Integer] 0 # # @api public # def self.symlink(path, dest, options = {}) @impl.symlink(assert_path(path), dest, options) end # @return [Boolean] true if the file is a symbolic link. # # @api public # def self.symlink?(path) @impl.symlink?(assert_path(path)) end # @return [String] the name of the file referenced by the given link. # # @api public # def self.readlink(path) @impl.readlink(assert_path(path)) end # Deletes the given paths, returning the number of names passed as arguments. # See also Dir::rmdir. # # @raise an exception on any error. # # @return [Integer] the number of paths passed as arguments # # @api public # def self.unlink(*paths) @impl.unlink(*(paths.map {|p| assert_path(p) })) end # @return [File::Stat] object for the named file. # # @api public # def self.stat(path) @impl.stat(assert_path(path)) end # @return [Integer] the size of the file # # @api public # def self.size(path) @impl.size(assert_path(path)) end # @return [File::Stat] Same as stat, but does not follow the last symbolic # link. Instead, reports on the link itself. # # @api public # def self.lstat(path) @impl.lstat(assert_path(path)) end # Compares the contents of this file against the contents of a stream. # # @param stream [IO] The stream to compare the contents against # @return [Boolean] Whether the contents were the same # # @api public # def self.compare_stream(path, stream) @impl.compare_stream(assert_path(path), stream) end # Produces an opaque pathname "handle" object representing the given path. # Different implementations of the underlying file system may use different runtime # objects. The produced "handle" should be used in all other operations # that take a "path". No operation should be directly invoked on the returned opaque object # # @param path [String] The string representation of the path # @return [Object] An opaque path handle on which no operations should be directly performed # # @api public # def self.pathname(path) @impl.pathname(path) end # Asserts that the given path is of the expected type produced by #pathname # # @raise [ArgumentError] when path is not of the expected type # # @api public # def self.assert_path(path) @impl.assert_path(path) end # Produces a string representation of the opaque path handle. # # @param path [Object] a path handle produced by {#pathname} # @return [String] a string representation of the path # def self.path_string(path) @impl.path_string(path) end # Create and open a file for write only if it doesn't exist. # # @see Puppet::FileSystem::open # # @raise [Errno::EEXIST] path already exists. # # @api public # def self.exclusive_create(path, mode, &block) @impl.exclusive_create(assert_path(path), mode, &block) end # Changes permission bits on the named path to the bit pattern represented # by mode. # # @param mode [Integer] The mode to apply to the file if it is created # @param path [String] The path to the file, can also accept [PathName] # # @raise [Errno::ENOENT]: path doesn't exist # # @api public # def self.chmod(mode, path) @impl.chmod(mode, path) end end diff --git a/lib/puppet/file_system/file19windows.rb b/lib/puppet/file_system/file19windows.rb index 7ae984f48..9078d33ce 100644 --- a/lib/puppet/file_system/file19windows.rb +++ b/lib/puppet/file_system/file19windows.rb @@ -1,107 +1,115 @@ require 'puppet/file_system/file19' require 'puppet/util/windows' class Puppet::FileSystem::File19Windows < Puppet::FileSystem::File19 def exist?(path) if ! Puppet.features.manages_symlinks? return ::File.exist?(path) end path = path.to_str if path.respond_to?(:to_str) # support WatchedFile path = path.to_s # support String and Pathname begin if Puppet::Util::Windows::File.symlink?(path) path = Puppet::Util::Windows::File.readlink(path) end ! Puppet::Util::Windows::File.stat(path).nil? rescue # generally INVALID_HANDLE_VALUE which means 'file not found' false end end def symlink(path, dest, options = {}) raise_if_symlinks_unsupported dest_exists = exist?(dest) # returns false on dangling symlink dest_stat = Puppet::Util::Windows::File.stat(dest) if dest_exists # silent fail to preserve semantics of original FileUtils return 0 if dest_exists && dest_stat.ftype == 'directory' if dest_exists && dest_stat.ftype == 'file' && options[:force] != true raise(Errno::EEXIST, "#{dest} already exists and the :force option was not specified") end if options[:noop] != true ::File.delete(dest) if dest_exists # can only be file Puppet::Util::Windows::File.symlink(path, dest) end 0 end def symlink?(path) return false if ! Puppet.features.manages_symlinks? Puppet::Util::Windows::File.symlink?(path) end def readlink(path) raise_if_symlinks_unsupported Puppet::Util::Windows::File.readlink(path) end def unlink(*file_names) if ! Puppet.features.manages_symlinks? return ::File.unlink(*file_names) end file_names.each do |file_name| file_name = file_name.to_s # handle PathName stat = Puppet::Util::Windows::File.stat(file_name) rescue nil # sigh, Ruby + Windows :( if stat && stat.ftype == 'directory' if Puppet::Util::Windows::File.symlink?(file_name) Dir.rmdir(file_name) else raise Errno::EPERM.new(file_name) end else ::File.unlink(file_name) end end file_names.length end def stat(path) Puppet::Util::Windows::File.stat(path) end def lstat(path) if ! Puppet.features.manages_symlinks? return Puppet::Util::Windows::File.stat(path) end Puppet::Util::Windows::File.lstat(path) end def chmod(mode, path) Puppet::Util::Windows::Security.set_mode(mode, path.to_s) end + def read_preserve_line_endings(path) + contents = path.read( :mode => 'rb', :encoding => Encoding::UTF_8) + contents = path.read( :mode => 'rb', :encoding => Encoding::default_external) unless contents.valid_encoding? + contents = path.read unless contents.valid_encoding? + + contents + end + private def raise_if_symlinks_unsupported if ! Puppet.features.manages_symlinks? msg = "This version of Windows does not support symlinks. Windows Vista / 2008 or higher is required." raise Puppet::Util::Windows::Error.new(msg) end if ! Puppet::Util::Windows::Process.process_privilege_symlink? Puppet.warning "The current user does not have the necessary permission to manage symlinks." end end end diff --git a/lib/puppet/file_system/file_impl.rb b/lib/puppet/file_system/file_impl.rb index d4cd605b7..d89e78d62 100644 --- a/lib/puppet/file_system/file_impl.rb +++ b/lib/puppet/file_system/file_impl.rb @@ -1,145 +1,149 @@ # Abstract implementation of the Puppet::FileSystem # class Puppet::FileSystem::FileImpl def pathname(path) path.is_a?(Pathname) ? path : Pathname.new(path) end def assert_path(path) return path if path.is_a?(Pathname) # Some paths are string, or in the case of WatchedFile, it pretends to be # one by implementing to_str. if path.respond_to?(:to_str) Pathname.new(path) else raise ArgumentError, "FileSystem implementation expected Pathname, got: '#{path.class}'" end end def path_string(path) path.to_s end def open(path, mode, options, &block) ::File.open(path, options, mode, &block) end def dir(path) path.dirname end def basename(path) path.basename.to_s end def size(path) path.size end def exclusive_create(path, mode, &block) opt = File::CREAT | File::EXCL | File::WRONLY self.open(path, mode, opt, &block) end def exclusive_open(path, mode, options = 'r', timeout = 300, &block) wait = 0.001 + (Kernel.rand / 1000) written = false while !written ::File.open(path, options, mode) do |rf| if rf.flock(::File::LOCK_EX|::File::LOCK_NB) yield rf written = true else sleep wait timeout -= wait wait *= 2 if timeout < 0 raise Timeout::Error, "Timeout waiting for exclusive lock on #{@path}" end end end end end def each_line(path, &block) ::File.open(path) do |f| f.each_line do |line| yield line end end end def read(path) path.read end + def read_preserve_line_endings(path) + read(path) + end + def binread(path) raise NotImplementedError end def exist?(path) ::File.exist?(path) end def directory?(path) ::File.directory?(path) end def file?(path) ::File.file?(path) end def executable?(path) ::File.executable?(path) end def writable?(path) path.writable? end def touch(path) ::FileUtils.touch(path) end def mkpath(path) path.mkpath end def children(path) path.children end def symlink(path, dest, options = {}) FileUtils.symlink(path, dest, options) end def symlink?(path) File.symlink?(path) end def readlink(path) File.readlink(path) end def unlink(*paths) File.unlink(*paths) end def stat(path) File.stat(path) end def lstat(path) File.lstat(path) end def compare_stream(path, stream) open(path, 0, 'rb') { |this| FileUtils.compare_stream(this, stream) } end def chmod(mode, path) FileUtils.chmod(mode, path) end end diff --git a/lib/puppet/file_system/memory_impl.rb b/lib/puppet/file_system/memory_impl.rb index 6fa357823..12c63cd79 100644 --- a/lib/puppet/file_system/memory_impl.rb +++ b/lib/puppet/file_system/memory_impl.rb @@ -1,78 +1,82 @@ class Puppet::FileSystem::MemoryImpl def initialize(*files) @files = files + all_children_of(files) end def exist?(path) path.exist? end def directory?(path) path.directory? end def file?(path) path.file? end def executable?(path) path.executable? end def children(path) path.children end def each_line(path, &block) path.each_line(&block) end def pathname(path) find(path) || Puppet::FileSystem::MemoryFile.a_missing_file(path) end def basename(path) path.duplicate_as(File.basename(path_string(path))) end def path_string(object) object.path end def read(path) handle = assert_path(path).handle handle.read end + def read_preserve_line_endings(path) + read(path) + end + def open(path, *args, &block) handle = assert_path(path).handle if block_given? yield handle else return handle end end def assert_path(path) if path.is_a?(Puppet::FileSystem::MemoryFile) path else find(path) or raise ArgumentError, "Unable to find registered object for #{path.inspect}" end end private def find(path) @files.find { |file| file.path == path } end def all_children_of(files) children = files.collect(&:children).flatten if children.empty? [] else children + all_children_of(children) end end end diff --git a/lib/puppet/parser/functions/file.rb b/lib/puppet/parser/functions/file.rb index cde496ab4..e8ae32abc 100644 --- a/lib/puppet/parser/functions/file.rb +++ b/lib/puppet/parser/functions/file.rb @@ -1,31 +1,33 @@ +require 'puppet/file_system' + Puppet::Parser::Functions::newfunction( :file, :arity => -2, :type => :rvalue, :doc => "Loads a file from a module and returns its contents as a string. The argument to this function should be a `/` reference, which will load `` from a module's `files` directory. (For example, the reference `mysql/mysqltuner.pl` will load the file `/mysql/files/mysqltuner.pl`.) This function can also accept: * An absolute path, which can load a file from anywhere on disk. * Multiple arguments, which will return the contents of the **first** file found, skipping any files that don't exist. " ) do |vals| path = nil vals.each do |file| found = Puppet::Parser::Files.find_file(file, compiler.environment) if found && Puppet::FileSystem.exist?(found) path = found break end end if path - File.read(path) + Puppet::FileSystem.read_preserve_line_endings(path) else raise Puppet::ParseError, "Could not find any files from #{vals.join(", ")}" end end diff --git a/lib/puppet/parser/templatewrapper.rb b/lib/puppet/parser/templatewrapper.rb index e4426cdf9..662395510 100644 --- a/lib/puppet/parser/templatewrapper.rb +++ b/lib/puppet/parser/templatewrapper.rb @@ -1,127 +1,128 @@ require 'puppet/parser/files' require 'erb' +require 'puppet/file_system' # A simple wrapper for templates, so they don't have full access to # the scope objects. # # @api private class Puppet::Parser::TemplateWrapper include Puppet::Util Puppet::Util.logmethods(self) def initialize(scope) @__scope__ = scope end # @return [String] The full path name of the template that is being executed # @api public def file @__file__ end # @return [Puppet::Parser::Scope] The scope in which the template is evaluated # @api public def scope @__scope__ end # Find which line in the template (if any) we were called from. # @return [String] the line number # @api private def script_line identifier = Regexp.escape(@__file__ || "(erb)") (caller.find { |l| l =~ /#{identifier}:/ }||"")[/:(\d+):/,1] end private :script_line # Should return true if a variable is defined, false if it is not # @api public def has_variable?(name) scope.include?(name.to_s) end # @return [Array] The list of defined classes # @api public def classes scope.catalog.classes end # @return [Array] The tags defined in the current scope # @api public def tags scope.tags end # @return [Array] All the defined tags # @api public def all_tags scope.catalog.tags end # Ruby treats variables like methods, so we used to expose variables # within scope to the ERB code via method_missing. As per RedMine #1427, # though, this means that conflicts between methods in our inheritance # tree (Kernel#fork) and variable names (fork => "yes/no") could arise. # # Worse, /new/ conflicts could pop up when a new kernel or object method # was added to Ruby, causing templates to suddenly fail mysteriously when # Ruby was upgraded. # # To ensure that legacy templates using unqualified names work we retain # the missing_method definition here until we declare the syntax finally # dead. def method_missing(name, *args) line_number = script_line if scope.include?(name.to_s) Puppet.deprecation_warning("Variable access via '#{name}' is deprecated. Use '@#{name}' instead. #{to_s}:#{line_number}") return scope[name.to_s, { :file => @__file__, :line => line_number }] else # Just throw an error immediately, instead of searching for # other missingmethod things or whatever. raise Puppet::ParseError.new("Could not find value for '#{name}'", @__file__, line_number) end end # @api private def file=(filename) unless @__file__ = Puppet::Parser::Files.find_template(filename, scope.compiler.environment) raise Puppet::ParseError, "Could not find template '#{filename}'" end # We'll only ever not have a parser in testing, but, eh. scope.known_resource_types.watch_file(@__file__) end # @api private def result(string = nil) if string template_source = "inline template" else - string = File.read(@__file__) + string = Puppet::FileSystem.read_preserve_line_endings(@__file__) template_source = @__file__ end # Expose all the variables in our scope as instance variables of the # current object, making it possible to access them without conflict # to the regular methods. benchmark(:debug, "Bound template variables for #{template_source}") do scope.to_hash.each do |name, value| realname = name.gsub(/[^\w]/, "_") instance_variable_set("@#{realname}", value) end end result = nil benchmark(:debug, "Interpolated template #{template_source}") do template = ERB.new(string, 0, "-") template.filename = @__file__ result = template.result(binding) end result end def to_s "template[#{(@__file__ ? @__file__ : "inline")}]" end end diff --git a/spec/unit/file_system_spec.rb b/spec/unit/file_system_spec.rb index f3b36a66d..f06de53f3 100644 --- a/spec/unit/file_system_spec.rb +++ b/spec/unit/file_system_spec.rb @@ -1,508 +1,546 @@ require 'spec_helper' require 'puppet/file_system' require 'puppet/util/platform' describe "Puppet::FileSystem" do include PuppetSpec::Files + def with_file_content(content) + path = tmpfile('file-system') + file = File.new(path, 'wb') + file.sync = true + file.print content + + yield path + + ensure + file.close + end + context "#exclusive_open" do it "opens ands allows updating of an existing file" do file = file_containing("file_to_update", "the contents") Puppet::FileSystem.exclusive_open(file, 0660, 'r+') do |fh| old = fh.read fh.truncate(0) fh.rewind fh.write("updated #{old}") end expect(Puppet::FileSystem.read(file)).to eq("updated the contents") end it "opens, creates ands allows updating of a new file" do file = tmpfile("file_to_update") Puppet::FileSystem.exclusive_open(file, 0660, 'w') do |fh| fh.write("updated new file") end expect(Puppet::FileSystem.read(file)).to eq("updated new file") end it "excludes other processes from updating at the same time", :unless => Puppet::Util::Platform.windows? do file = file_containing("file_to_update", "0") increment_counter_in_multiple_processes(file, 5, 'r+') expect(Puppet::FileSystem.read(file)).to eq("5") end it "excludes other processes from updating at the same time even when creating the file", :unless => Puppet::Util::Platform.windows? do file = tmpfile("file_to_update") increment_counter_in_multiple_processes(file, 5, 'a+') expect(Puppet::FileSystem.read(file)).to eq("5") end it "times out if the lock cannot be aquired in a specified amount of time", :unless => Puppet::Util::Platform.windows? do file = tmpfile("file_to_update") child = spawn_process_that_locks(file) expect do Puppet::FileSystem.exclusive_open(file, 0666, 'a', 0.1) do |f| end end.to raise_error(Timeout::Error) Process.kill(9, child) end def spawn_process_that_locks(file) read, write = IO.pipe child = Kernel.fork do read.close Puppet::FileSystem.exclusive_open(file, 0666, 'a') do |fh| write.write(true) write.close sleep 10 end end write.close read.read read.close child end def increment_counter_in_multiple_processes(file, num_procs, options) children = [] num_procs.times do children << Kernel.fork do Puppet::FileSystem.exclusive_open(file, 0660, options) do |fh| fh.rewind contents = (fh.read || 0).to_i fh.truncate(0) fh.rewind fh.write((contents + 1).to_s) end exit(0) end end children.each { |pid| Process.wait(pid) } end end + context "read_preserve_line_endings" do + it "should read a file with line feed" do + with_file_content("file content \n") do |file| + expect(Puppet::FileSystem.read_preserve_line_endings(file)).to eq("file content \n") + end + end + + it "should read a file with carriage return line feed" do + with_file_content("file content \r\n") do |file| + expect(Puppet::FileSystem.read_preserve_line_endings(file)).to eq("file content \r\n") + end + end + + it "should read a mixed file using only the first line newline when lf" do + with_file_content("file content \nsecond line \r\n") do |file| + expect(Puppet::FileSystem.read_preserve_line_endings(file)).to eq("file content \nsecond line \r\n") + end + end + + it "should read a mixed file using only the first line newline when crlf" do + with_file_content("file content \r\nsecond line \n") do |file| + expect(Puppet::FileSystem.read_preserve_line_endings(file)).to eq("file content \r\nsecond line \n") + end + end + end + describe "symlink", :if => ! Puppet.features.manages_symlinks? && Puppet.features.microsoft_windows? do let(:file) { tmpfile("somefile") } let(:missing_file) { tmpfile("missingfile") } let(:expected_msg) { "This version of Windows does not support symlinks. Windows Vista / 2008 or higher is required." } before :each do FileUtils.touch(file) end it "should raise an error when trying to create a symlink" do expect { Puppet::FileSystem.symlink(file, 'foo') }.to raise_error(Puppet::Util::Windows::Error) end it "should return false when trying to check if a path is a symlink" do Puppet::FileSystem.symlink?(file).should be_false end it "should raise an error when trying to read a symlink" do expect { Puppet::FileSystem.readlink(file) }.to raise_error(Puppet::Util::Windows::Error) end it "should return a File::Stat instance when calling stat on an existing file" do Puppet::FileSystem.stat(file).should be_instance_of(File::Stat) end it "should raise Errno::ENOENT when calling stat on a missing file" do expect { Puppet::FileSystem.stat(missing_file) }.to raise_error(Errno::ENOENT) end it "should fall back to stat when trying to lstat a file" do Puppet::Util::Windows::File.expects(:stat).with(Puppet::FileSystem.assert_path(file)) Puppet::FileSystem.lstat(file) end end describe "symlink", :if => Puppet.features.manages_symlinks? do let(:file) { tmpfile("somefile") } let(:missing_file) { tmpfile("missingfile") } let(:dir) { tmpdir("somedir") } before :each do FileUtils.touch(file) end it "should return true for exist? on a present file" do Puppet::FileSystem.exist?(file).should be_true end it "should return true for file? on a present file" do Puppet::FileSystem.file?(file).should be_true end it "should return false for exist? on a non-existant file" do Puppet::FileSystem.exist?(missing_file).should be_false end it "should return true for exist? on a present directory" do Puppet::FileSystem.exist?(dir).should be_true end it "should return false for exist? on a dangling symlink" do symlink = tmpfile("somefile_link") Puppet::FileSystem.symlink(missing_file, symlink) Puppet::FileSystem.exist?(missing_file).should be_false Puppet::FileSystem.exist?(symlink).should be_false end it "should return true for exist? on valid symlinks" do [file, dir].each do |target| symlink = tmpfile("#{Puppet::FileSystem.basename(target).to_s}_link") Puppet::FileSystem.symlink(target, symlink) Puppet::FileSystem.exist?(target).should be_true Puppet::FileSystem.exist?(symlink).should be_true end end it "should not create a symlink when the :noop option is specified" do [file, dir].each do |target| symlink = tmpfile("#{Puppet::FileSystem.basename(target)}_link") Puppet::FileSystem.symlink(target, symlink, { :noop => true }) Puppet::FileSystem.exist?(target).should be_true Puppet::FileSystem.exist?(symlink).should be_false end end it "should raise Errno::EEXIST if trying to create a file / directory symlink when the symlink path already exists as a file" do existing_file = tmpfile("#{Puppet::FileSystem.basename(file)}_link") FileUtils.touch(existing_file) [file, dir].each do |target| expect { Puppet::FileSystem.symlink(target, existing_file) }.to raise_error(Errno::EEXIST) Puppet::FileSystem.exist?(existing_file).should be_true Puppet::FileSystem.symlink?(existing_file).should be_false end end it "should silently fail if trying to create a file / directory symlink when the symlink path already exists as a directory" do existing_dir = tmpdir("#{Puppet::FileSystem.basename(file)}_dir") [file, dir].each do |target| Puppet::FileSystem.symlink(target, existing_dir).should == 0 Puppet::FileSystem.exist?(existing_dir).should be_true File.directory?(existing_dir).should be_true Puppet::FileSystem.symlink?(existing_dir).should be_false end end it "should silently fail to modify an existing directory symlink to reference a new file or directory" do [file, dir].each do |target| existing_dir = tmpdir("#{Puppet::FileSystem.basename(target)}_dir") symlink = tmpfile("#{Puppet::FileSystem.basename(existing_dir)}_link") Puppet::FileSystem.symlink(existing_dir, symlink) Puppet::FileSystem.readlink(symlink).should == Puppet::FileSystem.path_string(existing_dir) # now try to point it at the new target, no error raised, but file system unchanged Puppet::FileSystem.symlink(target, symlink).should == 0 Puppet::FileSystem.readlink(symlink).should == existing_dir.to_s end end it "should raise Errno::EEXIST if trying to modify a file symlink to reference a new file or directory" do symlink = tmpfile("#{Puppet::FileSystem.basename(file)}_link") file_2 = tmpfile("#{Puppet::FileSystem.basename(file)}_2") FileUtils.touch(file_2) # symlink -> file_2 Puppet::FileSystem.symlink(file_2, symlink) [file, dir].each do |target| expect { Puppet::FileSystem.symlink(target, symlink) }.to raise_error(Errno::EEXIST) Puppet::FileSystem.readlink(symlink).should == file_2.to_s end end it "should delete the existing file when creating a file / directory symlink with :force when the symlink path exists as a file" do [file, dir].each do |target| existing_file = tmpfile("#{Puppet::FileSystem.basename(target)}_existing") FileUtils.touch(existing_file) Puppet::FileSystem.symlink?(existing_file).should be_false Puppet::FileSystem.symlink(target, existing_file, { :force => true }) Puppet::FileSystem.symlink?(existing_file).should be_true Puppet::FileSystem.readlink(existing_file).should == target.to_s end end it "should modify an existing file symlink when using :force to reference a new file or directory" do [file, dir].each do |target| existing_file = tmpfile("#{Puppet::FileSystem.basename(target)}_existing") FileUtils.touch(existing_file) existing_symlink = tmpfile("#{Puppet::FileSystem.basename(existing_file)}_link") Puppet::FileSystem.symlink(existing_file, existing_symlink) Puppet::FileSystem.readlink(existing_symlink).should == existing_file.to_s Puppet::FileSystem.symlink(target, existing_symlink, { :force => true }) Puppet::FileSystem.readlink(existing_symlink).should == target.to_s end end it "should silently fail if trying to overwrite an existing directory with a new symlink when using :force to reference a file or directory" do [file, dir].each do |target| existing_dir = tmpdir("#{Puppet::FileSystem.basename(target)}_existing") Puppet::FileSystem.symlink(target, existing_dir, { :force => true }).should == 0 Puppet::FileSystem.symlink?(existing_dir).should be_false end end it "should silently fail if trying to modify an existing directory symlink when using :force to reference a new file or directory" do [file, dir].each do |target| existing_dir = tmpdir("#{Puppet::FileSystem.basename(target)}_existing") existing_symlink = tmpfile("#{Puppet::FileSystem.basename(existing_dir)}_link") Puppet::FileSystem.symlink(existing_dir, existing_symlink) Puppet::FileSystem.readlink(existing_symlink).should == existing_dir.to_s Puppet::FileSystem.symlink(target, existing_symlink, { :force => true }).should == 0 Puppet::FileSystem.readlink(existing_symlink).should == existing_dir.to_s end end it "should accept a string, Pathname or object with to_str (Puppet::Util::WatchedFile) for exist?" do [ tmpfile('bogus1'), Pathname.new(tmpfile('bogus2')), Puppet::Util::WatchedFile.new(tmpfile('bogus3')) ].each { |f| Puppet::FileSystem.exist?(f).should be_false } end it "should return a File::Stat instance when calling stat on an existing file" do Puppet::FileSystem.stat(file).should be_instance_of(File::Stat) end it "should raise Errno::ENOENT when calling stat on a missing file" do expect { Puppet::FileSystem.stat(missing_file) }.to raise_error(Errno::ENOENT) end it "should be able to create a symlink, and verify it with symlink?" do symlink = tmpfile("somefile_link") Puppet::FileSystem.symlink(file, symlink) Puppet::FileSystem.symlink?(symlink).should be_true end it "should report symlink? as false on file, directory and missing files" do [file, dir, missing_file].each do |f| Puppet::FileSystem.symlink?(f).should be_false end end it "should return a File::Stat with ftype 'link' when calling lstat on a symlink pointing to existing file" do symlink = tmpfile("somefile_link") Puppet::FileSystem.symlink(file, symlink) stat = Puppet::FileSystem.lstat(symlink) stat.should be_instance_of(File::Stat) stat.ftype.should == 'link' end it "should return a File::Stat of ftype 'link' when calling lstat on a symlink pointing to missing file" do symlink = tmpfile("somefile_link") Puppet::FileSystem.symlink(missing_file, symlink) stat = Puppet::FileSystem.lstat(symlink) stat.should be_instance_of(File::Stat) stat.ftype.should == 'link' end it "should return a File::Stat of ftype 'file' when calling stat on a symlink pointing to existing file" do symlink = tmpfile("somefile_link") Puppet::FileSystem.symlink(file, symlink) stat = Puppet::FileSystem.stat(symlink) stat.should be_instance_of(File::Stat) stat.ftype.should == 'file' end it "should return a File::Stat of ftype 'directory' when calling stat on a symlink pointing to existing directory" do symlink = tmpfile("somefile_link") Puppet::FileSystem.symlink(dir, symlink) stat = Puppet::FileSystem.stat(symlink) stat.should be_instance_of(File::Stat) stat.ftype.should == 'directory' # on Windows, this won't get cleaned up if still linked Puppet::FileSystem.unlink(symlink) end it "should return a File::Stat of ftype 'file' when calling stat on a symlink pointing to another symlink" do # point symlink -> file symlink = tmpfile("somefile_link") Puppet::FileSystem.symlink(file, symlink) # point symlink2 -> symlink symlink2 = tmpfile("somefile_link2") Puppet::FileSystem.symlink(symlink, symlink2) Puppet::FileSystem.stat(symlink2).ftype.should == 'file' end it "should raise Errno::ENOENT when calling stat on a dangling symlink" do symlink = tmpfile("somefile_link") Puppet::FileSystem.symlink(missing_file, symlink) expect { Puppet::FileSystem.stat(symlink) }.to raise_error(Errno::ENOENT) end it "should be able to readlink to resolve the physical path to a symlink" do symlink = tmpfile("somefile_link") Puppet::FileSystem.symlink(file, symlink) Puppet::FileSystem.exist?(file).should be_true Puppet::FileSystem.readlink(symlink).should == file.to_s end it "should not resolve entire symlink chain with readlink on a symlink'd symlink" do # point symlink -> file symlink = tmpfile("somefile_link") Puppet::FileSystem.symlink(file, symlink) # point symlink2 -> symlink symlink2 = tmpfile("somefile_link2") Puppet::FileSystem.symlink(symlink, symlink2) Puppet::FileSystem.exist?(file).should be_true Puppet::FileSystem.readlink(symlink2).should == symlink.to_s end it "should be able to readlink to resolve the physical path to a dangling symlink" do symlink = tmpfile("somefile_link") Puppet::FileSystem.symlink(missing_file, symlink) Puppet::FileSystem.exist?(missing_file).should be_false Puppet::FileSystem.readlink(symlink).should == missing_file.to_s end it "should delete only the symlink and not the target when calling unlink instance method" do [file, dir].each do |target| symlink = tmpfile("#{Puppet::FileSystem.basename(target)}_link") Puppet::FileSystem.symlink(target, symlink) Puppet::FileSystem.exist?(target).should be_true Puppet::FileSystem.readlink(symlink).should == target.to_s Puppet::FileSystem.unlink(symlink).should == 1 # count of files Puppet::FileSystem.exist?(target).should be_true Puppet::FileSystem.exist?(symlink).should be_false end end it "should delete only the symlink and not the target when calling unlink class method" do [file, dir].each do |target| symlink = tmpfile("#{Puppet::FileSystem.basename(target)}_link") Puppet::FileSystem.symlink(target, symlink) Puppet::FileSystem.exist?(target).should be_true Puppet::FileSystem.readlink(symlink).should == target.to_s Puppet::FileSystem.unlink(symlink).should == 1 # count of files Puppet::FileSystem.exist?(target).should be_true Puppet::FileSystem.exist?(symlink).should be_false end end describe "unlink" do it "should delete files with unlink" do Puppet::FileSystem.exist?(file).should be_true Puppet::FileSystem.unlink(file).should == 1 # count of files Puppet::FileSystem.exist?(file).should be_false end it "should delete files with unlink class method" do Puppet::FileSystem.exist?(file).should be_true Puppet::FileSystem.unlink(file).should == 1 # count of files Puppet::FileSystem.exist?(file).should be_false end it "should delete multiple files with unlink class method" do paths = (1..3).collect do |i| f = tmpfile("somefile_#{i}") FileUtils.touch(f) Puppet::FileSystem.exist?(f).should be_true f.to_s end Puppet::FileSystem.unlink(*paths).should == 3 # count of files paths.each { |p| Puppet::FileSystem.exist?(p).should be_false } end it "should raise Errno::EPERM or Errno::EISDIR when trying to delete a directory with the unlink class method" do Puppet::FileSystem.exist?(dir).should be_true ex = nil begin Puppet::FileSystem.unlink(dir) rescue Exception => e ex = e end [ Errno::EPERM, # Windows and OSX Errno::EISDIR # Linux ].should include(ex.class) Puppet::FileSystem.exist?(dir).should be_true end end describe "exclusive_create" do it "should create a file that doesn't exist" do Puppet::FileSystem.exist?(missing_file).should be_false Puppet::FileSystem.exclusive_create(missing_file, nil) {} Puppet::FileSystem.exist?(missing_file).should be_true end it "should raise Errno::EEXIST creating a file that does exist" do Puppet::FileSystem.exist?(file).should be_true expect do Puppet::FileSystem.exclusive_create(file, nil) {} end.to raise_error(Errno::EEXIST) end end end end diff --git a/spec/unit/parser/functions/file_spec.rb b/spec/unit/parser/functions/file_spec.rb index c5f157300..54849b63e 100755 --- a/spec/unit/parser/functions/file_spec.rb +++ b/spec/unit/parser/functions/file_spec.rb @@ -1,98 +1,104 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet_spec/files' describe "the 'file' function" do include PuppetSpec::Files before :all do Puppet::Parser::Functions.autoloader.loadall end let :node do Puppet::Node.new('localhost') end let :compiler do Puppet::Parser::Compiler.new(node) end let :scope do Puppet::Parser::Scope.new(compiler) end def with_file_content(content) path = tmpfile('file-function') - file = File.new(path, 'w') + file = File.new(path, 'wb') file.sync = true file.print content yield path end it "should read a file" do with_file_content('file content') do |name| - scope.function_file([name]).should == "file content" + expect(scope.function_file([name])).to eq("file content") + end + end + + it "should read a file keeping line endings intact" do + with_file_content("file content\r\n") do |name| + expect(scope.function_file([name])).to eq("file content\r\n") end end it "should read a file from a module path" do with_file_content('file content') do |name| mod = mock 'module' mod.stubs(:file).with('myfile').returns(name) compiler.environment.stubs(:module).with('mymod').returns(mod) scope.function_file(['mymod/myfile']).should == 'file content' end end it "should return the first file if given two files with absolute paths" do with_file_content('one') do |one| with_file_content('two') do |two| scope.function_file([one, two]).should == "one" end end end it "should return the first file if given two files with module paths" do with_file_content('one') do |one| with_file_content('two') do |two| mod = mock 'module' compiler.environment.expects(:module).with('mymod').returns(mod) mod.expects(:file).with('one').returns(one) mod.stubs(:file).with('two').returns(two) scope.function_file(['mymod/one','mymod/two']).should == 'one' end end end it "should return the first file if given two files with mixed paths, absolute first" do with_file_content('one') do |one| with_file_content('two') do |two| mod = mock 'module' compiler.environment.stubs(:module).with('mymod').returns(mod) mod.stubs(:file).with('two').returns(two) scope.function_file([one,'mymod/two']).should == 'one' end end end it "should return the first file if given two files with mixed paths, module first" do with_file_content('one') do |one| with_file_content('two') do |two| mod = mock 'module' compiler.environment.expects(:module).with('mymod').returns(mod) mod.stubs(:file).with('two').returns(two) scope.function_file(['mymod/two',one]).should == 'two' end end end it "should not fail when some files are absent" do expect { with_file_content('one') do |one| scope.function_file([make_absolute("/should-not-exist"), one]).should == 'one' end }.to_not raise_error end it "should fail when all files are absent" do expect { scope.function_file([File.expand_path('one')]) }.to raise_error(Puppet::ParseError, /Could not find any files/) end end diff --git a/spec/unit/parser/functions/template_spec.rb b/spec/unit/parser/functions/template_spec.rb index c873932cd..cc407e991 100755 --- a/spec/unit/parser/functions/template_spec.rb +++ b/spec/unit/parser/functions/template_spec.rb @@ -1,89 +1,89 @@ #! /usr/bin/env ruby require 'spec_helper' describe "the template function" do before :all do Puppet::Parser::Functions.autoloader.loadall end let :node do Puppet::Node.new('localhost') end let :compiler do Puppet::Parser::Compiler.new(node) end let :scope do Puppet::Parser::Scope.new(compiler) end it "concatenates outputs for multiple templates" do tw1 = stub_everything "template_wrapper1" tw2 = stub_everything "template_wrapper2" Puppet::Parser::TemplateWrapper.stubs(:new).returns(tw1,tw2) tw1.stubs(:file=).with("1") tw2.stubs(:file=).with("2") tw1.stubs(:result).returns("result1") tw2.stubs(:result).returns("result2") scope.function_template(["1","2"]).should == "result1result2" end it "raises an error if the template raises an error" do tw = stub_everything 'template_wrapper' Puppet::Parser::TemplateWrapper.stubs(:new).returns(tw) tw.stubs(:result).raises expect { scope.function_template(["1"]) }.to raise_error(Puppet::ParseError, /Failed to parse template/) end context "when accessing scope variables via method calls (deprecated)" do it "raises an error when accessing an undefined variable" do expect { eval_template("template <%= deprecated %>") }.to raise_error(Puppet::ParseError, /Could not find value for 'deprecated'/) end it "looks up the value from the scope" do scope["deprecated"] = "deprecated value" eval_template("template <%= deprecated %>").should == "template deprecated value" end it "still has access to Kernel methods" do expect { eval_template("<%= binding %>") }.to_not raise_error end end context "when accessing scope variables as instance variables" do it "has access to values" do scope['scope_var'] = "value" eval_template("<%= @scope_var %>").should == "value" end it "get nil accessing a variable that does not exist" do eval_template("<%= @not_defined.nil? %>").should == "true" end it "get nil accessing a variable that is undef" do scope['undef_var'] = :undef eval_template("<%= @undef_var.nil? %>").should == "true" end end it "is not interfered with by having a variable named 'string' (#14093)" do scope['string'] = "this output should not be seen" eval_template("some text that is static").should == "some text that is static" end it "has access to a variable named 'string' (#14093)" do scope['string'] = "the string value" eval_template("string was: <%= @string %>").should == "string was: the string value" end it "does not have direct access to Scope#lookupvar" do expect { eval_template("<%= lookupvar('myvar') %>") }.to raise_error(Puppet::ParseError, /Could not find value for 'lookupvar'/) end def eval_template(content) - File.stubs(:read).with("template").returns(content) + Puppet::FileSystem.stubs(:read_preserve_line_endings).with("template").returns(content) Puppet::Parser::Files.stubs(:find_template).returns("template") scope.function_template(['template']) end end diff --git a/spec/unit/parser/templatewrapper_spec.rb b/spec/unit/parser/templatewrapper_spec.rb index 81e0d846d..554849dd8 100755 --- a/spec/unit/parser/templatewrapper_spec.rb +++ b/spec/unit/parser/templatewrapper_spec.rb @@ -1,118 +1,118 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/parser/templatewrapper' describe Puppet::Parser::TemplateWrapper do let(:known_resource_types) { Puppet::Resource::TypeCollection.new("env") } let(:scope) do compiler = Puppet::Parser::Compiler.new(Puppet::Node.new("mynode")) compiler.environment.stubs(:known_resource_types).returns known_resource_types Puppet::Parser::Scope.new compiler end let(:tw) { Puppet::Parser::TemplateWrapper.new(scope) } it "marks the file for watching" do full_file_name = given_a_template_file("fake_template", "content") known_resource_types.expects(:watch_file).with(full_file_name) tw.file = "fake_template" end it "fails if a template cannot be found" do Puppet::Parser::Files.expects(:find_template).returns nil expect { tw.file = "fake_template" }.to raise_error(Puppet::ParseError) end it "stringifies as template[] for a file based template" do Puppet::Parser::Files.stubs(:find_template).returns("/tmp/fake_template") tw.file = "fake_template" tw.to_s.should eql("template[/tmp/fake_template]") end it "stringifies as template[inline] for a string-based template" do tw.to_s.should eql("template[inline]") end it "reads and evaluates a file-based template" do given_a_template_file("fake_template", "template contents") tw.file = "fake_template" tw.result.should eql("template contents") end it "provides access to the name of the template via #file" do full_file_name = given_a_template_file("fake_template", "<%= file %>") tw.file = "fake_template" tw.result.should == full_file_name end it "evaluates a given string as a template" do tw.result("template contents").should eql("template contents") end it "provides the defined classes with #classes" do catalog = mock 'catalog', :classes => ["class1", "class2"] scope.expects(:catalog).returns( catalog ) tw.classes.should == ["class1", "class2"] end it "provides all the tags with #all_tags" do catalog = mock 'catalog', :tags => ["tag1", "tag2"] scope.expects(:catalog).returns( catalog ) tw.all_tags.should == ["tag1","tag2"] end it "provides the tags defined in the current scope with #tags" do scope.expects(:tags).returns( ["tag1", "tag2"] ) tw.tags.should == ["tag1","tag2"] end it "warns about deprecated access to in-scope variables via method calls" do Puppet.expects(:deprecation_warning).with("Variable access via 'in_scope_variable' is deprecated. Use '@in_scope_variable' instead. template[inline]:1") scope["in_scope_variable"] = "is good" tw.result("<%= in_scope_variable %>") end it "provides access to in-scope variables via method calls" do scope["in_scope_variable"] = "is good" tw.result("<%= in_scope_variable %>").should == "is good" end it "errors if accessing via method call a variable that does not exist" do expect { tw.result("<%= does_not_exist %>") }.to raise_error(Puppet::ParseError) end it "reports that variable is available when it is in scope" do scope["in_scope_variable"] = "is good" tw.result("<%= has_variable?('in_scope_variable') %>").should == "true" end it "reports that a variable is not available when it is not in scope" do tw.result("<%= has_variable?('not_in_scope_variable') %>").should == "false" end it "provides access to in-scope variables via instance variables" do scope["one"] = "foo" tw.result("<%= @one %>").should == "foo" end %w{! . ; :}.each do |badchar| it "translates #{badchar} to _ in instance variables" do scope["one#{badchar}"] = "foo" tw.result("<%= @one_ %>").should == "foo" end end def given_a_template_file(name, contents) full_name = "/full/path/to/#{name}" Puppet::Parser::Files.stubs(:find_template). with(name, anything()). returns(full_name) - File.stubs(:read).with(full_name).returns(contents) + Puppet::FileSystem.stubs(:read_preserve_line_endings).with(full_name).returns(contents) full_name end end