diff --git a/lib/puppet/util.rb b/lib/puppet/util.rb index de3fa2f05..a14a3fafc 100644 --- a/lib/puppet/util.rb +++ b/lib/puppet/util.rb @@ -1,563 +1,556 @@ # A module to collect utility functions. require 'English' require 'puppet/util/monkey_patches' require 'sync' require 'tempfile' require 'puppet/external/lock' require 'monitor' require 'puppet/util/execution_stub' require 'uri' module Puppet # A command failed to execute. require 'puppet/error' class ExecutionFailure < Puppet::Error end module Util require 'benchmark' # These are all for backward compatibility -- these are methods that used # to be in Puppet::Util but have been moved into external modules. require 'puppet/util/posix' extend Puppet::Util::POSIX @@sync_objects = {}.extend MonitorMixin - # This is a list of environment variables that we will set when we want to override the POSIX locale - POSIX_LOCALE_ENV_VARS = ['LANG', 'LC_ALL', 'LC_MESSAGES', 'LANGUAGE', - 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME'] - - # This is a list of user-related environment variables that we will unset when we want to provide a pristine - # environment for "exec" runs - POSIX_USER_ENV_VARS = ['HOME', 'USER', 'LOGNAME'] def self.activerecord_version if (defined?(::ActiveRecord) and defined?(::ActiveRecord::VERSION) and defined?(::ActiveRecord::VERSION::MAJOR) and defined?(::ActiveRecord::VERSION::MINOR)) ([::ActiveRecord::VERSION::MAJOR, ::ActiveRecord::VERSION::MINOR].join('.').to_f) else 0 end end def self.synchronize_on(x,type) sync_object,users = 0,1 begin @@sync_objects.synchronize { (@@sync_objects[x] ||= [Sync.new,0])[users] += 1 } @@sync_objects[x][sync_object].synchronize(type) { yield } ensure @@sync_objects.synchronize { @@sync_objects.delete(x) unless (@@sync_objects[x][users] -= 1) > 0 } end end # Change the process to a different user def self.chuser if group = Puppet[:group] begin Puppet::Util::SUIDManager.change_group(group, true) rescue => detail Puppet.warning "could not change to group #{group.inspect}: #{detail}" $stderr.puts "could not change to group #{group.inspect}" # Don't exit on failed group changes, since it's # not fatal #exit(74) end end if user = Puppet[:user] begin Puppet::Util::SUIDManager.change_user(user, true) rescue => detail $stderr.puts "Could not change to user #{user}: #{detail}" exit(74) end end end # Create instance methods for each of the log levels. This allows # the messages to be a little richer. Most classes will be calling this # method. def self.logmethods(klass, useself = true) Puppet::Util::Log.eachlevel { |level| klass.send(:define_method, level, proc { |args| args = args.join(" ") if args.is_a?(Array) if useself Puppet::Util::Log.create( :level => level, :source => self, :message => args ) else Puppet::Util::Log.create( :level => level, :message => args ) end }) } end # Proxy a bunch of methods to another object. def self.classproxy(klass, objmethod, *methods) classobj = class << klass; self; end methods.each do |method| classobj.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # Proxy a bunch of methods to another object. def self.proxy(klass, objmethod, *methods) methods.each do |method| klass.send(:define_method, method) do |*args| obj = self.send(objmethod) obj.send(method, *args) end end end # Execute a given chunk of code with a new umask. def self.withumask(mask) cur = File.umask(mask) begin yield ensure File.umask(cur) end end def benchmark(*args) msg = args.pop level = args.pop object = nil if args.empty? if respond_to?(level) object = self else object = Puppet end else object = args.pop end raise Puppet::DevError, "Failed to provide level to :benchmark" unless level unless level == :none or object.respond_to? level raise Puppet::DevError, "Benchmarked object does not respond to #{level}" end # Only benchmark if our log level is high enough if level != :none and Puppet::Util::Log.sendlevel?(level) result = nil seconds = Benchmark.realtime { yield } object.send(level, msg + (" in %0.2f seconds" % seconds)) return seconds else yield end end def which(bin) if absolute_path?(bin) return bin if FileTest.file? bin and FileTest.executable? bin else ENV['PATH'].split(File::PATH_SEPARATOR).each do |dir| dest = File.expand_path(File.join(dir, bin)) if Puppet.features.microsoft_windows? && File.extname(dest).empty? exts = ENV['PATHEXT'] exts = exts ? exts.split(File::PATH_SEPARATOR) : %w[.COM .EXE .BAT .CMD] exts.each do |ext| destext = File.expand_path(dest + ext) return destext if FileTest.file? destext and FileTest.executable? destext end end return dest if FileTest.file? dest and FileTest.executable? dest end end nil end module_function :which # Determine in a platform-specific way whether a path is absolute. This # defaults to the local platform if none is specified. def absolute_path?(path, platform=nil) # Escape once for the string literal, and once for the regex. slash = '[\\\\/]' name = '[^\\\\/]+' regexes = { :windows => %r!^(([A-Z]:#{slash})|(#{slash}#{slash}#{name}#{slash}#{name})|(#{slash}#{slash}\?#{slash}#{name}))!i, :posix => %r!^/!, } require 'puppet' platform ||= Puppet.features.microsoft_windows? ? :windows : :posix !! (path =~ regexes[platform]) end module_function :absolute_path? # Convert a path to a file URI def path_to_uri(path) return unless path params = { :scheme => 'file' } if Puppet.features.microsoft_windows? path = path.gsub(/\\/, '/') if unc = /^\/\/([^\/]+)(\/[^\/]+)/.match(path) params[:host] = unc[1] path = unc[2] elsif path =~ /^[a-z]:\//i path = '/' + path end end params[:path] = URI.escape(path) begin URI::Generic.build(params) rescue => detail raise Puppet::Error, "Failed to convert '#{path}' to URI: #{detail}" end end module_function :path_to_uri # Get the path component of a URI def uri_to_path(uri) return unless uri.is_a?(URI) path = URI.unescape(uri.path) if Puppet.features.microsoft_windows? and uri.scheme == 'file' if uri.host path = "//#{uri.host}" + path # UNC else path.sub!(/^\//, '') end end path end module_function :uri_to_path # Execute the provided command in a pipe, yielding the pipe object. def execpipe(command, failonfail = true) if respond_to? :debug debug "Executing '#{command}'" else Puppet.debug "Executing '#{command}'" end command_str = command.respond_to?(:join) ? command.join('') : command output = open("| #{command_str} 2>&1") do |pipe| yield pipe end if failonfail unless $CHILD_STATUS == 0 raise ExecutionFailure, output end end output end def execfail(command, exception) output = execute(command) return output rescue ExecutionFailure raise exception, output end def execute_posix(command, arguments, stdin, stdout, stderr) child_pid = Kernel.fork do # We can't just call Array(command), and rely on it returning # things like ['foo'], when passed ['foo'], because # Array(command) will call command.to_a internally, which when # given a string can end up doing Very Bad Things(TM), such as # turning "/tmp/foo;\r\n /bin/echo" into ["/tmp/foo;\r\n", " /bin/echo"] command = [command].flatten Process.setsid begin $stdin.reopen(stdin) $stdout.reopen(stdout) $stderr.reopen(stderr) # we are in a forked process, so we currently have access to all of the file descriptors # from the parent process... which, in this case, is bad because we don't want # to allow the user's command to have access to them. Therefore, we'll close them off. # (assumes that there are only 256 file descriptors used) 3.upto(256){|fd| IO::new(fd).close rescue nil} Puppet::Util::SUIDManager.change_group(arguments[:gid], true) if arguments[:gid] Puppet::Util::SUIDManager.change_user(arguments[:uid], true) if arguments[:uid] # if the caller has requested that we override locale environment variables, if (arguments[:override_locale]) then # loop over them and clear them - POSIX_LOCALE_ENV_VARS.each { |name| ENV.delete(name) } + Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |name| ENV.delete(name) } # set LANG and LC_ALL to 'C' so that the command will have consistent, predictable output # it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in # a forked process. ENV['LANG'] = 'C' ENV['LC_ALL'] = 'C' end # unset all of the user-related environment variables so that different methods of starting puppet # (automatic start during boot, via 'service', via /etc/init.d, etc.) won't have unexpected side # effects relating to user / home dir environment vars. # it's OK to manipulate these directly rather than, e.g., via "withenv", because we are in # a forked process. - POSIX_USER_ENV_VARS.each { |name| ENV.delete(name) } + Puppet::Util::POSIX::USER_ENV_VARS.each { |name| ENV.delete(name) } arguments[:custom_environment] ||= {} Puppet::Util::Execution.withenv(arguments[:custom_environment]) do Kernel.exec(*command) end rescue => detail puts detail.message puts detail.backtrace if Puppet[:trace] Puppet.err "Could not execute posix command: #{detail}" exit!(1) end end child_pid end module_function :execute_posix def execute_windows(command, arguments, stdin, stdout, stderr) command = command.map do |part| part.include?(' ') ? %Q["#{part.gsub(/"/, '\"')}"] : part end.join(" ") if command.is_a?(Array) arguments[:custom_environment] ||= {} process_info = Puppet::Util::Execution.withenv(arguments[:custom_environment]) do Process.create( :command_line => command, :startup_info => {:stdin => stdin, :stdout => stdout, :stderr => stderr} ) end process_info.process_id end module_function :execute_windows # Execute the desired command, and return the status and output. # def execute(command, arguments) # [arguments] a Hash optionally containing any of the following keys: # :failonfail (default true) -- if this value is set to true, then this method will raise an error if the # command is not executed successfully. # :uid (default nil) -- the user id of the user that the process should be run as # :gid (default nil) -- the group id of the group that the process should be run as # :combine (default true) -- sets whether or not to combine stdout/stderr in the output # :stdinfile (default nil) -- sets a file that can be used for stdin. Passing a string for stdin is not currently # supported. # :squelch (default false) -- if true, ignore stdout / stderr completely # :override_locale (default true) -- by default (and if this option is set to true), we will temporarily override # the user/system locale to "C" (via environment variables LANG and LC_*) while we are executing the command. # This ensures that the output of the command will be formatted consistently, making it predictable for parsing. # Passing in a value of false for this option will allow the command to be executed using the user/system locale. # :custom_environment (default {}) -- a hash of key/value pairs to set as environment variables for the duration # of the command def execute(command, arguments = {}) # specifying these here rather than in the method signature to allow callers to pass in a partial # set of overrides without affecting the default values for options that they don't pass in default_arguments = { :failonfail => true, :uid => nil, :gid => nil, :combine => true, :stdinfile => nil, :squelch => false, :override_locale => true, :custom_environment => {}, } arguments = default_arguments.merge(arguments) if command.is_a?(Array) command = command.flatten.map(&:to_s) str = command.join(" ") elsif command.is_a?(String) str = command end if respond_to? :debug debug "Executing '#{str}'" else Puppet.debug "Executing '#{str}'" end null_file = Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' stdin = File.open(arguments[:stdinfile] || null_file, 'r') stdout = arguments[:squelch] ? File.open(null_file, 'w') : Tempfile.new('puppet') stderr = arguments[:combine] ? stdout : File.open(null_file, 'w') exec_args = [command, arguments, stdin, stdout, stderr] if execution_stub = Puppet::Util::ExecutionStub.current_value return execution_stub.call(*exec_args) elsif Puppet.features.posix? child_pid = execute_posix(*exec_args) exit_status = Process.waitpid2(child_pid).last.exitstatus elsif Puppet.features.microsoft_windows? child_pid = execute_windows(*exec_args) exit_status = Process.waitpid2(child_pid).last # $CHILD_STATUS is not set when calling win32/process Process.create # and since it's read-only, we can't set it. But we can execute a # a shell that simply returns the desired exit status, which has the # desired effect. %x{#{ENV['COMSPEC']} /c exit #{exit_status}} end [stdin, stdout, stderr].each {|io| io.close rescue nil} # read output in if required unless arguments[:squelch] output = wait_for_output(stdout) Puppet.warning "Could not get output" unless output end if arguments[:failonfail] and exit_status != 0 raise ExecutionFailure, "Execution of '#{str}' returned #{exit_status}: #{output}" end output end module_function :execute def wait_for_output(stdout) # Make sure the file's actually been written. This is basically a race # condition, and is probably a horrible way to handle it, but, well, oh # well. # (If this method were treated as private / inaccessible from outside of this file, we shouldn't have to worry # about a race condition because all of the places that we call this from are preceded by a call to "waitpid2", # meaning that the processes responsible for writing the file have completed before we get here.) 2.times do |try| if File.exists?(stdout.path) output = stdout.open.read stdout.close(true) return output else time_to_sleep = try / 2.0 Puppet.warning "Waiting for output; will sleep #{time_to_sleep} seconds" sleep(time_to_sleep) end end nil end module_function :wait_for_output # Create an exclusive lock. def threadlock(resource, type = Sync::EX) Puppet::Util.synchronize_on(resource,type) { yield } end # Because some modules provide their own version of this method. alias util_execute execute module_function :benchmark def memory unless defined?(@pmap) @pmap = which('pmap') end if @pmap %x{#{@pmap} #{Process.pid}| grep total}.chomp.sub(/^\s*total\s+/, '').sub(/K$/, '').to_i else 0 end end def symbolize(value) if value.respond_to? :intern value.intern else value end end def symbolizehash(hash) newhash = {} hash.each do |name, val| if name.is_a? String newhash[name.intern] = val else newhash[name] = val end end end def symbolizehash!(hash) hash.each do |name, val| if name.is_a? String hash[name.intern] = val hash.delete(name) end end hash end module_function :symbolize, :symbolizehash, :symbolizehash! # Just benchmark, with no logging. def thinmark seconds = Benchmark.realtime { yield } seconds end module_function :memory, :thinmark def secure_open(file,must_be_w,&block) raise Puppet::DevError,"secure_open only works with mode 'w'" unless must_be_w == 'w' raise Puppet::DevError,"secure_open only requires a block" unless block_given? Puppet.warning "#{file} was a symlink to #{File.readlink(file)}" if File.symlink?(file) if File.exists?(file) or File.symlink?(file) wait = File.symlink?(file) ? 5.0 : 0.1 File.delete(file) sleep wait # give it a chance to reappear, just in case someone is actively trying something. end begin File.open(file,File::CREAT|File::EXCL|File::TRUNC|File::WRONLY,&block) rescue Errno::EEXIST desc = File.symlink?(file) ? "symlink to #{File.readlink(file)}" : File.stat(file).ftype puts "Warning: #{file} was apparently created by another process (as" puts "a #{desc}) as soon as it was deleted by this process. Someone may be trying" puts "to do something objectionable (such as tricking you into overwriting system" puts "files if you are running as root)." raise end end module_function :secure_open end end require 'puppet/util/errors' require 'puppet/util/methodhelper' require 'puppet/util/metaid' require 'puppet/util/classgen' require 'puppet/util/docs' require 'puppet/util/execution' require 'puppet/util/logging' require 'puppet/util/package' require 'puppet/util/warnings' diff --git a/lib/puppet/util/posix.rb b/lib/puppet/util/posix.rb index 4cabe24af..2707e10a3 100755 --- a/lib/puppet/util/posix.rb +++ b/lib/puppet/util/posix.rb @@ -1,137 +1,147 @@ # Utility methods for interacting with POSIX objects; mostly user and group module Puppet::Util::POSIX + # This is a list of environment variables that we will set when we want to override the POSIX locale + LOCALE_ENV_VARS = ['LANG', 'LC_ALL', 'LC_MESSAGES', 'LANGUAGE', + 'LC_COLLATE', 'LC_CTYPE', 'LC_MONETARY', 'LC_NUMERIC', 'LC_TIME'] + + # This is a list of user-related environment variables that we will unset when we want to provide a pristine + # environment for "exec" runs + USER_ENV_VARS = ['HOME', 'USER', 'LOGNAME'] + + + # Retrieve a field from a POSIX Etc object. The id can be either an integer # or a name. This only works for users and groups. It's also broken on # some platforms, unfortunately, which is why we fall back to the other # method search_posix_field in the gid and uid methods if a sanity check # fails def get_posix_field(space, field, id) raise Puppet::DevError, "Did not get id from caller" unless id if id.is_a?(Integer) if id > Puppet[:maximum_uid].to_i Puppet.err "Tried to get #{field} field for silly id #{id}" return nil end method = methodbyid(space) else method = methodbyname(space) end begin return Etc.send(method, id).send(field) rescue ArgumentError => detail # ignore it; we couldn't find the object return nil end end # A degenerate method of retrieving name/id mappings. The job of this method is # to retrieve all objects of a certain type, search for a specific entry # and then return a given field from that entry. def search_posix_field(type, field, id) idmethod = idfield(type) integer = false if id.is_a?(Integer) integer = true if id > Puppet[:maximum_uid].to_i Puppet.err "Tried to get #{field} field for silly id #{id}" return nil end end Etc.send(type) do |object| if integer and object.send(idmethod) == id return object.send(field) elsif object.name == id return object.send(field) end end # Apparently the group/passwd methods need to get reset; if we skip # this call, then new users aren't found. case type when :passwd; Etc.send(:endpwent) when :group; Etc.send(:endgrent) end nil end # Determine what the field name is for users and groups. def idfield(space) case Puppet::Util.symbolize(space) when :gr, :group; return :gid when :pw, :user, :passwd; return :uid else raise ArgumentError.new("Can only handle users and groups") end end # Determine what the method is to get users and groups by id def methodbyid(space) case Puppet::Util.symbolize(space) when :gr, :group; return :getgrgid when :pw, :user, :passwd; return :getpwuid else raise ArgumentError.new("Can only handle users and groups") end end # Determine what the method is to get users and groups by name def methodbyname(space) case Puppet::Util.symbolize(space) when :gr, :group; return :getgrnam when :pw, :user, :passwd; return :getpwnam else raise ArgumentError.new("Can only handle users and groups") end end # Get the GID of a given group, provided either a GID or a name def gid(group) begin group = Integer(group) rescue ArgumentError # pass end if group.is_a?(Integer) return nil unless name = get_posix_field(:group, :name, group) gid = get_posix_field(:group, :gid, name) check_value = gid else return nil unless gid = get_posix_field(:group, :gid, group) name = get_posix_field(:group, :name, gid) check_value = name end if check_value != group return search_posix_field(:group, :gid, group) else return gid end end # Get the UID of a given user, whether a UID or name is provided def uid(user) begin user = Integer(user) rescue ArgumentError # pass end if user.is_a?(Integer) return nil unless name = get_posix_field(:passwd, :name, user) uid = get_posix_field(:passwd, :uid, name) check_value = uid else return nil unless uid = get_posix_field(:passwd, :uid, user) name = get_posix_field(:passwd, :name, uid) check_value = name end if check_value != user return search_posix_field(:passwd, :uid, user) else return uid end end end diff --git a/spec/unit/provider/exec/posix_spec.rb b/spec/unit/provider/exec/posix_spec.rb index bf0036df9..ff872976e 100755 --- a/spec/unit/provider/exec/posix_spec.rb +++ b/spec/unit/provider/exec/posix_spec.rb @@ -1,204 +1,204 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:exec).provider(:posix) do include PuppetSpec::Files def make_exe command = tmpfile('my_command') FileUtils.touch(command) File.chmod(0755, command) command end let(:resource) { Puppet::Type.type(:exec).new(:title => File.expand_path('/foo'), :provider => :posix) } let(:provider) { described_class.new(resource) } describe "#validatecmd" do it "should fail if no path is specified and the command is not fully qualified" do expect { provider.validatecmd("foo") }.to raise_error( Puppet::Error, "'foo' is not qualified and no path was specified. Please qualify the command or specify a path." ) end it "should pass if a path is given" do provider.resource[:path] = ['/bogus/bin'] provider.validatecmd("../foo") end it "should pass if command is fully qualifed" do provider.resource[:path] = ['/bogus/bin'] provider.validatecmd(File.expand_path("/bin/blah/foo")) end end describe "#run" do describe "when the command is an absolute path" do let(:command) { tmpfile('foo') } it "should fail if the command doesn't exist" do expect { provider.run(command) }.to raise_error(ArgumentError, "Could not find command '#{command}'") end it "should fail if the command isn't a file" do FileUtils.mkdir(command) FileUtils.chmod(0755, command) expect { provider.run(command) }.to raise_error(ArgumentError, "'#{command}' is a directory, not a file") end it "should fail if the command isn't executable" do FileUtils.touch(command) File.stubs(:executable?).with(command).returns(false) expect { provider.run(command) }.to raise_error(ArgumentError, "'#{command}' is not executable") end end describe "when the command is a relative path" do it "should execute the command if it finds it in the path and is executable" do command = make_exe provider.resource[:path] = [File.dirname(command)] filename = File.basename(command) Puppet::Util.expects(:execute).with { |cmdline, arguments| (cmdline == filename) && (arguments.is_a? Hash) } provider.run(filename) end it "should fail if the command isn't in the path" do resource[:path] = ["/fake/path"] expect { provider.run('foo') }.to raise_error(ArgumentError, "Could not find command 'foo'") end it "should fail if the command is in the path but not executable" do command = tmpfile('foo') FileUtils.touch(command) FileTest.stubs(:executable?).with(command).returns(false) resource[:path] = [File.dirname(command)] filename = File.basename(command) expect { provider.run(filename) }.to raise_error(ArgumentError, "Could not find command '#{filename}'") end end it "should not be able to execute shell builtins" do provider.resource[:path] = ['/bin'] expect { provider.run("cd ..") }.to raise_error(ArgumentError, "Could not find command 'cd'") end it "should execute the command if the command given includes arguments or subcommands" do provider.resource[:path] = ['/bogus/bin'] command = make_exe Puppet::Util.expects(:execute).with { |cmdline, arguments| (cmdline == "#{command} bar --sillyarg=true --blah") && (arguments.is_a? Hash) } provider.run("#{command} bar --sillyarg=true --blah") end it "should fail if quoted command doesn't exist" do provider.resource[:path] = ['/bogus/bin'] command = "#{File.expand_path('/foo')} bar --sillyarg=true --blah" expect { provider.run(%Q["#{command}"]) }.to raise_error(ArgumentError, "Could not find command '#{command}'") end it "should warn if you're overriding something in environment" do provider.resource[:environment] = ['WHATEVER=/something/else', 'WHATEVER=/foo'] command = make_exe Puppet::Util.expects(:execute).with { |cmdline, arguments| (cmdline == command) && (arguments.is_a? Hash) } provider.run(command) @logs.map {|l| "#{l.level}: #{l.message}" }.should == ["warning: Overriding environment setting 'WHATEVER' with '/foo'"] end describe "posix locale settings", :unless => Puppet.features.microsoft_windows? do # a sentinel value that we can use to emulate what locale environment variables might be set to on an international # system. lang_sentinel_value = "es_ES.UTF-8" # a temporary hash that contains sentinel values for each of the locale environment variables that we override in # "exec" locale_sentinel_env = {} - Puppet::Util::POSIX_LOCALE_ENV_VARS.each { |var| locale_sentinel_env[var] = lang_sentinel_value } + Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |var| locale_sentinel_env[var] = lang_sentinel_value } command = "/bin/echo $%s" it "should not override user's locale during execution" do # we'll do this once without any sentinel values, to give us a little more test coverage orig_env = {} - Puppet::Util::POSIX_LOCALE_ENV_VARS.each { |var| orig_env[var] = ENV[var] if ENV[var] } + Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |var| orig_env[var] = ENV[var] if ENV[var] } orig_env.keys.each do |var| output, status = provider.run(command % var) output.strip.should == orig_env[var] end # now, once more... but with our sentinel values Puppet::Util::Execution.withenv(locale_sentinel_env) do - Puppet::Util::POSIX_LOCALE_ENV_VARS.each do |var| + Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| output, status = provider.run(command % var) output.strip.should == locale_sentinel_env[var] end end end it "should respect locale overrides in user's 'environment' configuration" do provider.resource[:environment] = ['LANG=foo', 'LC_ALL=bar'] output, status = provider.run(command % 'LANG') output.strip.should == 'foo' output, status = provider.run(command % 'LC_ALL') output.strip.should == 'bar' end end describe "posix user-related environment vars", :unless => Puppet.features.microsoft_windows? do # a temporary hash that contains sentinel values for each of the user-related environment variables that we # are expected to unset during an "exec" user_sentinel_env = {} - Puppet::Util::POSIX_USER_ENV_VARS.each { |var| user_sentinel_env[var] = "Abracadabra" } + Puppet::Util::POSIX::USER_ENV_VARS.each { |var| user_sentinel_env[var] = "Abracadabra" } command = "/bin/echo $%s" it "should unset user-related environment vars during execution" do # first we set up a temporary execution environment with sentinel values for the user-related environment vars # that we care about. Puppet::Util::Execution.withenv(user_sentinel_env) do # with this environment, we loop over the vars in question - Puppet::Util::POSIX_USER_ENV_VARS.each do |var| + Puppet::Util::POSIX::USER_ENV_VARS.each do |var| # ensure that our temporary environment is set up as we expect ENV[var].should == user_sentinel_env[var] # run an "exec" via the provider and ensure that it unsets the vars output, status = provider.run(command % var) output.strip.should == "" # ensure that after the exec, our temporary env is still intact ENV[var].should == user_sentinel_env[var] end end end it "should respect overrides to user-related environment vars in caller's 'environment' configuration" do sentinel_value = "Abracadabra" # set the "environment" property of the resource, populating it with a hash containing sentinel values for # each of the user-related posix environment variables - provider.resource[:environment] = Puppet::Util::POSIX_USER_ENV_VARS.collect { |var| "#{var}=#{sentinel_value}"} + provider.resource[:environment] = Puppet::Util::POSIX::USER_ENV_VARS.collect { |var| "#{var}=#{sentinel_value}"} # loop over the posix user-related environment variables - Puppet::Util::POSIX_USER_ENV_VARS.each do |var| + Puppet::Util::POSIX::USER_ENV_VARS.each do |var| # run an 'exec' to get the value of each variable output, status = provider.run(command % var) # ensure that it matches our expected sentinel value output.strip.should == sentinel_value end end end end end diff --git a/spec/unit/util_spec.rb b/spec/unit/util_spec.rb index 44092da5f..ec335a893 100755 --- a/spec/unit/util_spec.rb +++ b/spec/unit/util_spec.rb @@ -1,693 +1,693 @@ #!/usr/bin/env ruby require 'spec_helper' describe Puppet::Util do include PuppetSpec::Files def process_status(exitstatus) return exitstatus if Puppet.features.microsoft_windows? stub('child_status', :exitstatus => exitstatus) end describe "#absolute_path?" do it "should default to the platform of the local system" do Puppet.features.stubs(:posix?).returns(true) Puppet.features.stubs(:microsoft_windows?).returns(false) Puppet::Util.should be_absolute_path('/foo') Puppet::Util.should_not be_absolute_path('C:/foo') Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) Puppet::Util.should be_absolute_path('C:/foo') Puppet::Util.should_not be_absolute_path('/foo') end describe "when using platform :posix" do %w[/ /foo /foo/../bar //foo //Server/Foo/Bar //?/C:/foo/bar /\Server/Foo /foo//bar/baz].each do |path| it "should return true for #{path}" do Puppet::Util.should be_absolute_path(path, :posix) end end %w[. ./foo \foo C:/foo \\Server\Foo\Bar \\?\C:\foo\bar \/?/foo\bar \/Server/foo foo//bar/baz].each do |path| it "should return false for #{path}" do Puppet::Util.should_not be_absolute_path(path, :posix) end end end describe "when using platform :windows" do %w[C:/foo C:\foo \\\\Server\Foo\Bar \\\\?\C:\foo\bar //Server/Foo/Bar //?/C:/foo/bar /\?\C:/foo\bar \/Server\Foo/Bar c:/foo//bar//baz].each do |path| it "should return true for #{path}" do Puppet::Util.should be_absolute_path(path, :windows) end end %w[/ . ./foo \foo /foo /foo/../bar //foo C:foo/bar foo//bar/baz].each do |path| it "should return false for #{path}" do Puppet::Util.should_not be_absolute_path(path, :windows) end end end end describe "#path_to_uri" do %w[. .. foo foo/bar foo/../bar].each do |path| it "should reject relative path: #{path}" do lambda { Puppet::Util.path_to_uri(path) }.should raise_error(Puppet::Error) end end it "should perform URI escaping" do Puppet::Util.path_to_uri("/foo bar").path.should == "/foo%20bar" end describe "when using platform :posix" do before :each do Puppet.features.stubs(:posix).returns true Puppet.features.stubs(:microsoft_windows?).returns false end %w[/ /foo /foo/../bar].each do |path| it "should convert #{path} to URI" do Puppet::Util.path_to_uri(path).path.should == path end end end describe "when using platform :windows" do before :each do Puppet.features.stubs(:posix).returns false Puppet.features.stubs(:microsoft_windows?).returns true end it "should normalize backslashes" do Puppet::Util.path_to_uri('c:\\foo\\bar\\baz').path.should == '/' + 'c:/foo/bar/baz' end %w[C:/ C:/foo/bar].each do |path| it "should convert #{path} to absolute URI" do Puppet::Util.path_to_uri(path).path.should == '/' + path end end %w[share C$].each do |path| it "should convert UNC #{path} to absolute URI" do uri = Puppet::Util.path_to_uri("\\\\server\\#{path}") uri.host.should == 'server' uri.path.should == '/' + path end end end end describe ".uri_to_path" do require 'uri' it "should strip host component" do Puppet::Util.uri_to_path(URI.parse('http://foo/bar')).should == '/bar' end it "should accept puppet URLs" do Puppet::Util.uri_to_path(URI.parse('puppet:///modules/foo')).should == '/modules/foo' end it "should return unencoded path" do Puppet::Util.uri_to_path(URI.parse('http://foo/bar%20baz')).should == '/bar baz' end it "should be nil-safe" do Puppet::Util.uri_to_path(nil).should be_nil end describe "when using platform :posix",:if => Puppet.features.posix? do it "should accept root" do Puppet::Util.uri_to_path(URI.parse('file:/')).should == '/' end it "should accept single slash" do Puppet::Util.uri_to_path(URI.parse('file:/foo/bar')).should == '/foo/bar' end it "should accept triple slashes" do Puppet::Util.uri_to_path(URI.parse('file:///foo/bar')).should == '/foo/bar' end end describe "when using platform :windows", :if => Puppet.features.microsoft_windows? do it "should accept root" do Puppet::Util.uri_to_path(URI.parse('file:/C:/')).should == 'C:/' end it "should accept single slash" do Puppet::Util.uri_to_path(URI.parse('file:/C:/foo/bar')).should == 'C:/foo/bar' end it "should accept triple slashes" do Puppet::Util.uri_to_path(URI.parse('file:///C:/foo/bar')).should == 'C:/foo/bar' end it "should accept file scheme with double slashes as a UNC path" do Puppet::Util.uri_to_path(URI.parse('file://host/share/file')).should == '//host/share/file' end end end describe "execution methods" do let(:pid) { 5501 } let(:null_file) { Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' } describe "#execute_posix (stubs)" do before :each do # Most of the things this method does are bad to do during specs. :/ Kernel.stubs(:fork).returns(pid).yields Process.stubs(:setsid) Kernel.stubs(:exec) Puppet::Util::SUIDManager.stubs(:change_user) Puppet::Util::SUIDManager.stubs(:change_group) $stdin.stubs(:reopen) $stdout.stubs(:reopen) $stderr.stubs(:reopen) @stdin = File.open(null_file, 'r') @stdout = Tempfile.new('stdout') @stderr = File.open(null_file, 'w') end it "should fork a child process to execute the command" do Kernel.expects(:fork).returns(pid).yields Kernel.expects(:exec).with('test command') Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr) end it "should start a new session group" do Process.expects(:setsid) Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr) end it "should close all open file descriptors except stdin/stdout/stderr" do # This is ugly, but I can't really think of a better way to do it without # letting it actually close fds, which seems risky (0..2).each {|n| IO.expects(:new).with(n).never} (3..256).each {|n| IO.expects(:new).with(n).returns mock('io', :close) } Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr) end it "should permanently change to the correct user and group if specified" do Puppet::Util::SUIDManager.expects(:change_group).with(55, true) Puppet::Util::SUIDManager.expects(:change_user).with(50, true) Puppet::Util.execute_posix('test command', {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr) end it "should exit failure if there is a problem execing the command" do Kernel.expects(:exec).with('test command').raises("failed to execute!") Puppet::Util.stubs(:puts) Puppet::Util.expects(:exit!).with(1) Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr) end it "should properly execute commands specified as arrays" do Kernel.expects(:exec).with('test command', 'with', 'arguments') Puppet::Util.execute_posix(['test command', 'with', 'arguments'], {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr) end it "should properly execute string commands with embedded newlines" do Kernel.expects(:exec).with("/bin/echo 'foo' ; \n /bin/echo 'bar' ;") Puppet::Util.execute_posix("/bin/echo 'foo' ; \n /bin/echo 'bar' ;", {:uid => 50, :gid => 55}, @stdin, @stdout, @stderr) end it "should return the pid of the child process" do Puppet::Util.execute_posix('test command', {}, @stdin, @stdout, @stderr).should == pid end end describe "#execute_windows (stubs)" do let(:proc_info_stub) { stub 'processinfo', :process_id => pid } before :each do Process.stubs(:create).returns(proc_info_stub) Process.stubs(:waitpid2).with(pid).returns([pid, process_status(0)]) @stdin = File.open(null_file, 'r') @stdout = Tempfile.new('stdout') @stderr = File.open(null_file, 'w') end it "should create a new process for the command" do Process.expects(:create).with( :command_line => "test command", :startup_info => {:stdin => @stdin, :stdout => @stdout, :stderr => @stderr} ).returns(proc_info_stub) Puppet::Util.execute_windows('test command', {}, @stdin, @stdout, @stderr) end it "should return the pid of the child process" do Puppet::Util.execute_windows('test command', {}, @stdin, @stdout, @stderr).should == pid end it "should quote arguments containing spaces if command is specified as an array" do Process.expects(:create).with do |args| args[:command_line] == '"test command" with some "arguments \"with spaces"' end.returns(proc_info_stub) Puppet::Util.execute_windows(['test command', 'with', 'some', 'arguments "with spaces'], {}, @stdin, @stdout, @stderr) end end describe "#execute (stubs)" do before :each do Process.stubs(:waitpid2).with(pid).returns([pid, process_status(0)]) end describe "when an execution stub is specified" do before :each do Puppet::Util::ExecutionStub.set do |command,args,stdin,stdout,stderr| "execution stub output" end end it "should call the block on the stub" do Puppet::Util.execute("/usr/bin/run_my_execute_stub").should == "execution stub output" end it "should not actually execute anything" do Puppet::Util.expects(:execute_posix).never Puppet::Util.expects(:execute_windows).never Puppet::Util.execute("/usr/bin/run_my_execute_stub") end end describe "when setting up input and output files" do include PuppetSpec::Files let(:executor) { Puppet.features.microsoft_windows? ? 'execute_windows' : 'execute_posix' } before :each do Puppet::Util.stubs(:wait_for_output) end it "should set stdin to the stdinfile if specified" do input = tmpfile('stdin') FileUtils.touch(input) Puppet::Util.expects(executor).with do |_,_,stdin,_,_| stdin.path == input end.returns(pid) Puppet::Util.execute('test command', :stdinfile => input) end it "should set stdin to the null file if not specified" do Puppet::Util.expects(executor).with do |_,_,stdin,_,_| stdin.path == null_file end.returns(pid) Puppet::Util.execute('test command') end describe "when squelch is set" do it "should set stdout and stderr to the null file" do Puppet::Util.expects(executor).with do |_,_,_,stdout,stderr| stdout.path == null_file and stderr.path == null_file end.returns(pid) Puppet::Util.execute('test command', :squelch => true) end end describe "when squelch is not set" do it "should set stdout to a temporary output file" do outfile = Tempfile.new('stdout') Tempfile.stubs(:new).returns(outfile) Puppet::Util.expects(executor).with do |_,_,_,stdout,_| stdout.path == outfile.path end.returns(pid) Puppet::Util.execute('test command', :squelch => false) end it "should set stderr to the same file as stdout if combine is true" do outfile = Tempfile.new('stdout') Tempfile.stubs(:new).returns(outfile) Puppet::Util.expects(executor).with do |_,_,_,stdout,stderr| stdout.path == outfile.path and stderr.path == outfile.path end.returns(pid) Puppet::Util.execute('test command', :squelch => false, :combine => true) end it "should set stderr to the null device if combine is false" do outfile = Tempfile.new('stdout') Tempfile.stubs(:new).returns(outfile) Puppet::Util.expects(executor).with do |_,_,_,stdout,stderr| stdout.path == outfile.path and stderr.path == null_file end.returns(pid) Puppet::Util.execute('test command', :squelch => false, :combine => false) end end end end describe "#execute (posix locale)", :unless => Puppet.features.microsoft_windows? do # build up a printf-style string that contains a command to get the value of an environment variable # from the operating system. We can substitute into this with the names of the desired environment variables later. get_env_var_cmd = 'echo $%s' # a sentinel value that we can use to emulate what locale environment variables might be set to on an international # system. lang_sentinel_value = "es_ES.UTF-8" # a temporary hash that contains sentinel values for each of the locale environment variables that we override in # "execute" locale_sentinel_env = {} - Puppet::Util::POSIX_LOCALE_ENV_VARS.each { |var| locale_sentinel_env[var] = lang_sentinel_value } + Puppet::Util::POSIX::LOCALE_ENV_VARS.each { |var| locale_sentinel_env[var] = lang_sentinel_value } it "should override the locale environment variables when :override_locale is not set (defaults to true)" do # temporarily override the locale environment vars with a sentinel value, so that we can confirm that # execute is actually setting them. Puppet::Util::Execution.withenv(locale_sentinel_env) do - Puppet::Util::POSIX_LOCALE_ENV_VARS.each do |var| + Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| # we expect that all of the POSIX vars will have been cleared except for LANG and LC_ALL expected_value = (['LANG', 'LC_ALL'].include?(var)) ? "C" : "" Puppet::Util::execute(get_env_var_cmd % var).strip.should == expected_value end end end it "should override the LANG environment variable when :override_locale is set to true" do # temporarily override the locale environment vars with a sentinel value, so that we can confirm that # execute is actually setting them. Puppet::Util::Execution.withenv(locale_sentinel_env) do - Puppet::Util::POSIX_LOCALE_ENV_VARS.each do |var| + Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| # we expect that all of the POSIX vars will have been cleared except for LANG and LC_ALL expected_value = (['LANG', 'LC_ALL'].include?(var)) ? "C" : "" Puppet::Util::execute(get_env_var_cmd % var, {:override_locale => true}).strip.should == expected_value end end end it "should *not* override the LANG environment variable when :override_locale is set to false" do # temporarily override the locale environment vars with a sentinel value, so that we can confirm that # execute is not setting them. Puppet::Util::Execution.withenv(locale_sentinel_env) do - Puppet::Util::POSIX_LOCALE_ENV_VARS.each do |var| + Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| Puppet::Util::execute(get_env_var_cmd % var, {:override_locale => false}).strip.should == lang_sentinel_value end end end it "should have restored the LANG and locale environment variables after execution" do # we'll do this once without any sentinel values, to give us a little more test coverage orig_env_vals = {} - Puppet::Util::POSIX_LOCALE_ENV_VARS.each do |var| + Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| orig_env_vals[var] = ENV[var] end # now we can really execute any command--doesn't matter what it is... Puppet::Util::execute(get_env_var_cmd % 'anything', {:override_locale => true}) # now we check and make sure the original environment was restored - Puppet::Util::POSIX_LOCALE_ENV_VARS.each do |var| + Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| ENV[var].should == orig_env_vals[var] end # now, once more... but with our sentinel values Puppet::Util::Execution.withenv(locale_sentinel_env) do # now we can really execute any command--doesn't matter what it is... Puppet::Util::execute(get_env_var_cmd % 'anything', {:override_locale => true}) # now we check and make sure the original environment was restored - Puppet::Util::POSIX_LOCALE_ENV_VARS.each do |var| + Puppet::Util::POSIX::LOCALE_ENV_VARS.each do |var| ENV[var].should == locale_sentinel_env[var] end end end end describe "#execute (posix user env vars)", :unless => Puppet.features.microsoft_windows? do # build up a printf-style string that contains a command to get the value of an environment variable # from the operating system. We can substitute into this with the names of the desired environment variables later. get_env_var_cmd = 'echo $%s' # a sentinel value that we can use to emulate what locale environment variables might be set to on an international # system. user_sentinel_value = "Abracadabra" # a temporary hash that contains sentinel values for each of the locale environment variables that we override in # "execute" user_sentinel_env = {} - Puppet::Util::POSIX_USER_ENV_VARS.each { |var| user_sentinel_env[var] = user_sentinel_value } + Puppet::Util::POSIX::USER_ENV_VARS.each { |var| user_sentinel_env[var] = user_sentinel_value } it "should unset user-related environment vars during execution" do # first we set up a temporary execution environment with sentinel values for the user-related environment vars # that we care about. Puppet::Util::Execution.withenv(user_sentinel_env) do # with this environment, we loop over the vars in question - Puppet::Util::POSIX_USER_ENV_VARS.each do |var| + Puppet::Util::POSIX::USER_ENV_VARS.each do |var| # ensure that our temporary environment is set up as we expect ENV[var].should == user_sentinel_env[var] # run an "exec" via the provider and ensure that it unsets the vars Puppet::Util::execute(get_env_var_cmd % var).strip.should == "" # ensure that after the exec, our temporary env is still intact ENV[var].should == user_sentinel_env[var] end end end it "should have restored the user-related environment variables after execution" do # we'll do this once without any sentinel values, to give us a little more test coverage orig_env_vals = {} - Puppet::Util::POSIX_USER_ENV_VARS.each do |var| + Puppet::Util::POSIX::USER_ENV_VARS.each do |var| orig_env_vals[var] = ENV[var] end # now we can really execute any command--doesn't matter what it is... Puppet::Util::execute(get_env_var_cmd % 'anything') # now we check and make sure the original environment was restored - Puppet::Util::POSIX_USER_ENV_VARS.each do |var| + Puppet::Util::POSIX::USER_ENV_VARS.each do |var| ENV[var].should == orig_env_vals[var] end # now, once more... but with our sentinel values Puppet::Util::Execution.withenv(user_sentinel_env) do # now we can really execute any command--doesn't matter what it is... Puppet::Util::execute(get_env_var_cmd % 'anything') # now we check and make sure the original environment was restored - Puppet::Util::POSIX_USER_ENV_VARS.each do |var| + Puppet::Util::POSIX::USER_ENV_VARS.each do |var| ENV[var].should == user_sentinel_env[var] end end end end describe "after execution" do let(:executor) { Puppet.features.microsoft_windows? ? 'execute_windows' : 'execute_posix' } before :each do Process.stubs(:waitpid2).with(pid).returns([pid, process_status(0)]) Puppet::Util.stubs(executor).returns(pid) end it "should wait for the child process to exit" do Puppet::Util.stubs(:wait_for_output) Process.expects(:waitpid2).with(pid).returns([pid, process_status(0)]) Puppet::Util.execute('test command') end it "should close the stdin/stdout/stderr files used by the child" do stdin = mock 'file', :close stdout = mock 'file', :close stderr = mock 'file', :close File.expects(:open). times(3). returns(stdin). then.returns(stdout). then.returns(stderr) Puppet::Util.execute('test command', {:squelch => true, :combine => false}) end it "should read and return the output if squelch is false" do stdout = Tempfile.new('test') Tempfile.stubs(:new).returns(stdout) stdout.write("My expected command output") Puppet::Util.execute('test command').should == "My expected command output" end it "should not read the output if squelch is true" do stdout = Tempfile.new('test') Tempfile.stubs(:new).returns(stdout) stdout.write("My expected command output") Puppet::Util.execute('test command', :squelch => true).should == nil end it "should delete the file used for output if squelch is false" do stdout = Tempfile.new('test') path = stdout.path Tempfile.stubs(:new).returns(stdout) Puppet::Util.execute('test command') File.should_not be_exist(path) end it "should raise an error if failonfail is true and the child failed" do Process.expects(:waitpid2).with(pid).returns([pid, process_status(1)]) expect { Puppet::Util.execute('fail command', :failonfail => true) }.to raise_error(Puppet::ExecutionFailure, /Execution of 'fail command' returned 1/) end it "should not raise an error if failonfail is false and the child failed" do Process.expects(:waitpid2).with(pid).returns([pid, process_status(1)]) expect { Puppet::Util.execute('fail command', :failonfail => false) }.not_to raise_error end it "should not raise an error if failonfail is true and the child succeeded" do Process.expects(:waitpid2).with(pid).returns([pid, process_status(0)]) expect { Puppet::Util.execute('fail command', :failonfail => true) }.not_to raise_error end it "should respect default values for args that aren't overridden if a partial arg list is passed in" do Process.expects(:waitpid2).with(pid).returns([pid, process_status(1)]) expect { # here we are passing in a non-nil value for "arguments", but we aren't specifying a value for # :failonfail. We expect it to be set to its normal default value (true). Puppet::Util.execute('fail command', { :squelch => true }) }.to raise_error(Puppet::ExecutionFailure, /Execution of 'fail command' returned 1/) end end end describe "#which" do let(:base) { File.expand_path('/bin') } let(:path) { File.join(base, 'foo') } before :each do FileTest.stubs(:file?).returns false FileTest.stubs(:file?).with(path).returns true FileTest.stubs(:executable?).returns false FileTest.stubs(:executable?).with(path).returns true end it "should accept absolute paths" do Puppet::Util.which(path).should == path end it "should return nil if no executable found" do Puppet::Util.which('doesnotexist').should be_nil end it "should reject directories" do Puppet::Util.which(base).should be_nil end describe "on POSIX systems" do before :each do Puppet.features.stubs(:posix?).returns true Puppet.features.stubs(:microsoft_windows?).returns false end it "should walk the search PATH returning the first executable" do ENV.stubs(:[]).with('PATH').returns(File.expand_path('/bin')) Puppet::Util.which('foo').should == path end end describe "on Windows systems" do let(:path) { File.expand_path(File.join(base, 'foo.CMD')) } before :each do Puppet.features.stubs(:posix?).returns false Puppet.features.stubs(:microsoft_windows?).returns true end describe "when a file extension is specified" do it "should walk each directory in PATH ignoring PATHEXT" do ENV.stubs(:[]).with('PATH').returns(%w[/bar /bin].map{|dir| File.expand_path(dir)}.join(File::PATH_SEPARATOR)) FileTest.expects(:file?).with(File.join(File.expand_path('/bar'), 'foo.CMD')).returns false ENV.expects(:[]).with('PATHEXT').never Puppet::Util.which('foo.CMD').should == path end end describe "when a file extension is not specified" do it "should walk each extension in PATHEXT until an executable is found" do bar = File.expand_path('/bar') ENV.stubs(:[]).with('PATH').returns("#{bar}#{File::PATH_SEPARATOR}#{base}") ENV.stubs(:[]).with('PATHEXT').returns(".EXE#{File::PATH_SEPARATOR}.CMD") exts = sequence('extensions') FileTest.expects(:file?).in_sequence(exts).with(File.join(bar, 'foo.EXE')).returns false FileTest.expects(:file?).in_sequence(exts).with(File.join(bar, 'foo.CMD')).returns false FileTest.expects(:file?).in_sequence(exts).with(File.join(base, 'foo.EXE')).returns false FileTest.expects(:file?).in_sequence(exts).with(path).returns true Puppet::Util.which('foo').should == path end it "should walk the default extension path if the environment variable is not defined" do ENV.stubs(:[]).with('PATH').returns(base) ENV.stubs(:[]).with('PATHEXT').returns(nil) exts = sequence('extensions') %w[.COM .EXE .BAT].each do |ext| FileTest.expects(:file?).in_sequence(exts).with(File.join(base, "foo#{ext}")).returns false end FileTest.expects(:file?).in_sequence(exts).with(path).returns true Puppet::Util.which('foo').should == path end it "should fall back if no extension matches" do ENV.stubs(:[]).with('PATH').returns(base) ENV.stubs(:[]).with('PATHEXT').returns(".EXE") FileTest.stubs(:file?).with(File.join(base, 'foo.EXE')).returns false FileTest.stubs(:file?).with(File.join(base, 'foo')).returns true FileTest.stubs(:executable?).with(File.join(base, 'foo')).returns true Puppet::Util.which('foo').should == File.join(base, 'foo') end end end end end