From: Daniel Pittman <dan...@rimspace.net>

This is in preparation for allowing other new providers to handle exec
commands differently.

Reviewed-by: Max Martin and Matt Robinson

Signed-off-by: Max Martin <m...@puppetlabs.com>
---
 lib/puppet/provider/exec/posix.rb     |  112 ++++++
 lib/puppet/type/exec.rb               |  179 ++--------
 spec/unit/provider/exec/posix_spec.rb |  115 ++++++
 spec/unit/type/exec_spec.rb           |  674 +++++++++++++++++++++++++++++----
 4 files changed, 841 insertions(+), 239 deletions(-)
 create mode 100644 lib/puppet/provider/exec/posix.rb
 create mode 100755 spec/unit/provider/exec/posix_spec.rb

diff --git a/lib/puppet/provider/exec/posix.rb 
b/lib/puppet/provider/exec/posix.rb
new file mode 100644
index 0000000..92dbd8c
--- /dev/null
+++ b/lib/puppet/provider/exec/posix.rb
@@ -0,0 +1,112 @@
+Puppet::Type.type(:exec).provide :posix do
+  include Puppet::Util::Execution
+
+  confine :feature => :posix
+  defaultfor :feature => :posix
+
+  desc "Execute external binaries directly, on POSIX systems.
+This does not pass through a shell, or perform any interpolation, but
+only directly calls the command with the arguments given."
+
+  def run(command, check = false)
+    output = nil
+    status = nil
+    dir = nil
+
+    checkexe(command)
+
+    if dir = resource[:cwd]
+      unless File.directory?(dir)
+        if check
+          dir = nil
+        else
+          self.fail "Working directory '#{dir}' does not exist"
+        end
+      end
+    end
+
+    dir ||= Dir.pwd
+
+    debug "Executing#{check ? " check": ""} '#{command}'"
+    begin
+      # Do our chdir
+      Dir.chdir(dir) do
+        environment = {}
+
+        environment[:PATH] = resource[:path].join(":") if resource[:path]
+
+        if envlist = resource[:environment]
+          envlist = [envlist] unless envlist.is_a? Array
+          envlist.each do |setting|
+            if setting =~ /^(\w+)=((.|\n)+)$/
+              env_name = $1
+              value = $2
+              if environment.include?(env_name) || 
environment.include?(env_name.to_sym)
+                warning "Overriding environment setting '#{env_name}' with 
'#{value}'"
+              end
+              environment[env_name] = value
+            else
+              warning "Cannot understand environment setting 
#{setting.inspect}"
+            end
+          end
+        end
+
+        withenv environment do
+          Timeout::timeout(resource[:timeout]) do
+            output, status = Puppet::Util::SUIDManager.
+              run_and_capture([command], resource[:user], resource[:group])
+          end
+          # The shell returns 127 if the command is missing.
+          if status.exitstatus == 127
+            raise ArgumentError, output
+          end
+        end
+      end
+    rescue Errno::ENOENT => detail
+      self.fail detail.to_s
+    end
+
+    return output, status
+  end
+
+  # Verify that we have the executable
+  def checkexe(command)
+    exe = extractexe(command)
+
+    if resource[:path]
+      if Puppet.features.posix? and !File.exists?(exe)
+        withenv :PATH => resource[:path].join(File::PATH_SEPARATOR) do
+          exe = which(exe) || raise(ArgumentError,"Could not find command 
'#{exe}'")
+        end
+      elsif Puppet.features.microsoft_windows? and !File.exists?(exe)
+        resource[:path].each do |path|
+          [".exe", ".ps1", ".bat", ".com", ""].each do |extension|
+            file = File.join(path, exe+extension)
+            return if File.exists?(file)
+          end
+        end
+      end
+    end
+
+    raise ArgumentError, "Could not find command '#{exe}'" unless 
File.exists?(exe)
+    unless File.executable?(exe)
+      raise ArgumentError,
+      "'#{exe}' is not executable"
+    end
+  end
+
+  def extractexe(command)
+    # easy case: command was quoted
+    if command =~ /^"([^"]+)"/
+      $1
+    else
+      command.split(/ /)[0]
+    end
+  end
+
+  def validatecmd(command)
+    exe = extractexe(command)
+    # if we're not fully qualified, require a path
+    self.fail "'#{command}' is not qualified and no path was specified. Please 
qualify the command or specify a path." if File.expand_path(exe) != exe and 
resource[:path].nil?
+  end
+end
diff --git a/lib/puppet/type/exec.rb b/lib/puppet/type/exec.rb
index 387c592..67d40a7 100755
--- a/lib/puppet/type/exec.rb
+++ b/lib/puppet/type/exec.rb
@@ -28,10 +28,10 @@ module Puppet
 
     # Create a new check mechanism.  It's basically just a parameter that
     # provides one extra 'check' method.
-    def self.newcheck(name, &block)
+    def self.newcheck(name, options = {}, &block)
       @checks ||= {}
 
-      check = newparam(name, &block)
+      check = newparam(name, options, &block)
       @checks[name] = check
     end
 
@@ -63,9 +63,11 @@ module Puppet
 
       # First verify that all of our checks pass.
       def retrieve
-        # Default to somethinng
-
-        if @resource.check
+        # We need to return :notrun to trigger evaluation; when that isn't
+        # true, we *LIE* about what happened and return a "success" for the
+        # value, which causes us to be treated as in_sync?, which means we
+        # don't actually execute anything.  I think. --daniel 2011-03-10
+        if @resource.check_all_attributes
           return :notrun
         else
           return self.should
@@ -87,7 +89,7 @@ module Puppet
           tries.times do |try|
             # Only add debug messages for tries > 1 to reduce log spam.
             debug("Exec try #{try+1}/#{tries}") if tries > 1
-            @output, @status = @resource.run(self.resource[:command])
+            @output, @status = provider.run(self.resource[:command])
             break if self.should.include?(@status.exitstatus.to_s)
             if try_sleep > 0 and tries > 1
               debug("Sleeping for #{try_sleep} seconds between tries")
@@ -137,7 +139,7 @@ module Puppet
     newparam(:path) do
       desc "The search path used for command execution.
         Commands must be fully qualified if no path is specified.  Paths
-        can be specified as an array or as a colon-separated list."
+        can be specified as an array or as a colon or semi-colon separated 
list."
 
       # Support both arrays and colon-separated fields.
       def value=(*values)
@@ -174,21 +176,9 @@ module Puppet
       # Validation is handled by the SUIDManager class.
     end
 
-    newparam(:cwd) do
+    newparam(:cwd, :parent => Puppet::Parameter::Path) do
       desc "The directory from which to run the command.  If
         this directory does not exist, the command will fail."
-
-      validate do |dir|
-        unless dir =~ /^#{File::SEPARATOR}/
-          self.fail("CWD must be a fully qualified path")
-        end
-      end
-
-      munge do |dir|
-        dir = dir[0] if dir.is_a?(Array)
-
-        dir
-      end
     end
 
     newparam(:logoutput) do
@@ -207,7 +197,7 @@ module Puppet
         for refreshing."
 
       validate do |command|
-        @resource.validatecmd(command)
+        provider.validatecmd(command)
       end
     end
 
@@ -331,7 +321,7 @@ module Puppet
       end
     end
 
-    newcheck(:creates) do
+    newcheck(:creates, :parent => Puppet::Parameter::Path) do
       desc "A file that this command creates.  If this
         parameter is provided, then the command will only be run
         if the specified file does not exist:
@@ -344,19 +334,7 @@ module Puppet
 
         "
 
-      # FIXME if they try to set this and fail, then we should probably
-      # fail the entire exec, right?
-      validate do |files|
-        files = [files] unless files.is_a? Array
-
-        files.each do |file|
-          self.fail("'creates' must be set to a fully qualified path") unless 
file
-
-          unless file =~ %r{^#{File::SEPARATOR}}
-            self.fail "'creates' files must be fully qualified."
-          end
-        end
-      end
+      accept_arrays
 
       # If the file exists, return false (i.e., don't run the command),
       # else return true
@@ -384,15 +362,15 @@ module Puppet
       validate do |cmds|
         cmds = [cmds] unless cmds.is_a? Array
 
-        cmds.each do |cmd|
-          @resource.validatecmd(cmd)
+        cmds.each do |command|
+          provider.validatecmd(command)
         end
       end
 
       # Return true if the command does not return 0.
       def check(value)
         begin
-          output, status = @resource.run(value, true)
+          output, status = provider.run(value, true)
         rescue Timeout::Error
           err "Check #{value.inspect} exceeded timeout"
           return false
@@ -426,15 +404,15 @@ module Puppet
       validate do |cmds|
         cmds = [cmds] unless cmds.is_a? Array
 
-        cmds.each do |cmd|
-          @resource.validatecmd(cmd)
+        cmds.each do |command|
+          provider.validatecmd(command)
         end
       end
 
       # Return true if the command returns 0.
       def check(value)
         begin
-          output, status = @resource.run(value, true)
+          output, status = provider.run(value, true)
         rescue Timeout::Error
           err "Check #{value.inspect} exceeded timeout"
           return false
@@ -448,7 +426,7 @@ module Puppet
     @isomorphic = false
 
     validate do
-      validatecmd(self[:command])
+      provider.validatecmd(self[:command])
     end
 
     # FIXME exec should autorequire any exec that 'creates' our cwd
@@ -501,7 +479,7 @@ module Puppet
     # Verify that we pass all of the checks.  The argument determines whether
     # we skip the :refreshonly check, which is necessary because we now check
     # within refresh
-    def check(refreshing = false)
+    def check_all_attributes(refreshing = false)
       self.class.checks.each { |check|
         next if refreshing and check == :refreshonly
         if @parameters.include?(check)
@@ -516,32 +494,6 @@ module Puppet
       true
     end
 
-    # Verify that we have the executable
-    def checkexe(cmd)
-      exe = extractexe(cmd)
-
-      if self[:path]
-        if Puppet.features.posix? and !File.exists?(exe)
-          withenv :PATH => self[:path].join(File::PATH_SEPARATOR) do
-            exe = which(exe) || raise(ArgumentError,"Could not find command 
'#{exe}'")
-          end
-        elsif Puppet.features.microsoft_windows? and !File.exists?(exe)
-          self[:path].each do |path|
-            [".exe", ".ps1", ".bat", ".com", ""].each do |extension|
-              file = File.join(path, exe+extension)
-              return if File.exists?(file)
-            end
-          end
-        end
-      end
-
-      raise ArgumentError, "Could not find executable '#{exe}'" unless 
FileTest.exists?(exe)
-      unless FileTest.executable?(exe)
-        raise ArgumentError,
-          "'#{exe}' is not executable"
-      end
-    end
-
     def output
       if self.property(:returns).nil?
         return nil
@@ -552,98 +504,13 @@ module Puppet
 
     # Run the command, or optionally run a separately-specified command.
     def refresh
-      if self.check(true)
+      if self.check_all_attributes(true)
         if cmd = self[:refresh]
-          self.run(cmd)
+          provider.run(cmd)
         else
           self.property(:returns).sync
         end
       end
     end
-
-    # Run a command.
-    def run(command, check = false)
-      output = nil
-      status = nil
-
-      dir = nil
-
-      checkexe(command)
-
-      if dir = self[:cwd]
-        unless File.directory?(dir)
-          if check
-            dir = nil
-          else
-            self.fail "Working directory '#{dir}' does not exist"
-          end
-        end
-      end
-
-      dir ||= Dir.pwd
-
-      if check
-        debug "Executing check '#{command}'"
-      else
-        debug "Executing '#{command}'"
-      end
-      begin
-        # Do our chdir
-        Dir.chdir(dir) do
-          environment = {}
-
-          environment[:PATH] = self[:path].join(":") if self[:path]
-
-          if envlist = self[:environment]
-            envlist = [envlist] unless envlist.is_a? Array
-            envlist.each do |setting|
-              if setting =~ /^(\w+)=((.|\n)+)$/
-                name = $1
-                value = $2
-                if environment.include? name
-                  warning(
-                  "Overriding environment setting '#{name}' with '#{value}'"
-                  )
-                end
-                environment[name] = value
-              else
-                warning "Cannot understand environment setting 
#{setting.inspect}"
-              end
-            end
-          end
-
-          withenv environment do
-            Timeout::timeout(self[:timeout]) do
-              output, status = Puppet::Util::SUIDManager.run_and_capture(
-                [command], self[:user], self[:group]
-              )
-            end
-            # The shell returns 127 if the command is missing.
-            if status.exitstatus == 127
-              raise ArgumentError, output
-            end
-          end
-        end
-      rescue Errno::ENOENT => detail
-        self.fail detail.to_s
-      end
-
-      return output, status
-    end
-
-    def validatecmd(cmd)
-      exe = extractexe(cmd)
-      # if we're not fully qualified, require a path
-      self.fail "'#{cmd}' is not qualified and no path was specified. Please 
qualify the command or specify a path." if File.expand_path(exe) != exe and 
self[:path].nil?
-    end
-
-    def extractexe(cmd)
-      # easy case: command was quoted
-      if cmd =~ /^"([^"]+)"/
-        $1
-      else
-        cmd.split(/ /)[0]
-      end
-    end
   end
 end
diff --git a/spec/unit/provider/exec/posix_spec.rb 
b/spec/unit/provider/exec/posix_spec.rb
new file mode 100755
index 0000000..a9b2075
--- /dev/null
+++ b/spec/unit/provider/exec/posix_spec.rb
@@ -0,0 +1,115 @@
+#!/usr/bin/env ruby
+require File.dirname(__FILE__) + '/../../../spec_helper'
+
+provider_class = Puppet::Type.type(:exec).provider(:posix)
+
+describe provider_class do
+  before :each do
+    @resource = Puppet::Resource.new(:exec, 'foo')
+    @provider = provider_class.new(@resource)
+  end
+
+  ["posix", "microsoft_windows"].each do |feature|
+    describe "when in #{feature} environment" do
+      before :each do
+        if feature == "microsoft_windows"
+          Puppet.features.stubs(:microsoft_windows?).returns(true)
+          Puppet.features.stubs(:posix?).returns(false)
+        else
+          Puppet.features.stubs(:posix?).returns(true)
+          Puppet.features.stubs(:microsoft_windows?).returns(false)
+        end
+      end
+
+      describe "#validatecmd" do
+        it "should fail if no path is specified and the command is not fully 
qualified" do
+          lambda { @provider.validatecmd("foo") }.should 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("/bin/blah/foo")
+        end
+      end
+
+      describe "#run" do
+        it "should fail if no path is specified and command does not exist" do
+          lambda { @provider.run("foo") }.should raise_error(ArgumentError, 
"Could not find command 'foo'")
+        end
+
+        it "should fail if the command isn't in the path" do
+          @provider.resource[:path] = ['/bogus/bin']
+          lambda { @provider.run("foo") }.should raise_error(ArgumentError, 
"Could not find command 'foo'")
+        end
+
+        it "should fail if the command isn't executable" do
+          @provider.resource[:path] = ['/bogus/bin']
+          File.stubs(:exists?).with("foo").returns(true)
+
+          lambda { @provider.run("foo") }.should raise_error(ArgumentError, 
"'foo' is not executable")
+        end
+
+        it "should execute the command if the command given includes arguments 
or subcommands" do
+          @provider.resource[:path] = ['/bogus/bin']
+          File.stubs(:exists?).returns(false)
+          File.stubs(:exists?).with("foo").returns(true)
+          File.stubs(:executable?).with("foo").returns(true)
+
+          Puppet::Util.expects(:execute).with(['foo bar --sillyarg=true 
--blah'], {:uid => nil, :gid => nil, :combine => true, :failonfail => false})
+          @provider.run("foo bar --sillyarg=true --blah")
+        end
+
+        it "should fail if quoted command doesn't exist" do
+          @provider.resource[:path] = ['/bogus/bin']
+          File.stubs(:exists?).returns(false)
+          File.stubs(:exists?).with("foo").returns(true)
+          File.stubs(:executable?).with("foo").returns(true)
+
+          lambda { @provider.run('"foo bar --sillyarg=true --blah"') }.should 
raise_error(ArgumentError, "Could not find command 'foo bar --sillyarg=true 
--blah'")
+        end
+
+        it "should execute the command if it finds it in the path and is 
executable" do
+          @provider.resource[:path] = ['/bogus/bin']
+          File.stubs(:exists?).with("foo").returns(true)
+          File.stubs(:executable?).with("foo").returns(true)
+          Puppet::Util.expects(:execute).with(['foo'], {:uid => nil, :gid => 
nil, :combine => true, :failonfail => false})
+
+          @provider.run("foo")
+        end
+
+        if feature == "microsoft_windows"
+          [".exe", ".ps1", ".bat", ".com", ""].each do |extension|
+            it "should check file extension #{extension} when it can't find 
the executable" do
+              @provider.resource[:path] = ['/bogus/bin']
+              File.stubs(:exists?).returns(false)
+              
File.stubs(:exists?).with("/bogus/bin/foo#{extension}").returns(true)
+              File.stubs(:executable?).with("foo").returns(true)
+              Puppet::Util.expects(:execute).with(['foo'], {:uid => nil, :gid 
=> nil, :combine => true, :failonfail => false})
+
+              @provider.run("foo")
+            end
+          end
+        end
+
+        it "should warn if you're overriding something in environment" do
+          @provider.resource[:environment] = ['WHATEVER=/something/else', 
'WHATEVER=/foo']
+          File.stubs(:exists?).returns(false)
+          File.stubs(:exists?).with("foo").returns(true)
+          File.stubs(:executable?).with("foo").returns(true)
+
+          Puppet::Util.expects(:execute).with(['foo'], {:uid => nil, :gid => 
nil, :combine => true, :failonfail => false})
+          @provider.run("foo")
+          @logs.map {|l| "#{l.level}: #{l.message}" }.should == ["warning: 
Overriding environment setting 'WHATEVER' with '/foo'"]
+        end
+      end
+    end
+  end
+end
diff --git a/spec/unit/type/exec_spec.rb b/spec/unit/type/exec_spec.rb
index e04cfc0..4d691d5 100755
--- a/spec/unit/type/exec_spec.rb
+++ b/spec/unit/type/exec_spec.rb
@@ -1,31 +1,32 @@
 #!/usr/bin/env ruby
-
 require File.dirname(__FILE__) + '/../../spec_helper'
 
 describe Puppet::Type.type(:exec) do
-
-  def create_resource(command, output, exitstatus, returns = 0)
-    @user_name = 'some_user_name'
+  def exec_tester(command, exitstatus = 0, rest = {})
+    @user_name  = 'some_user_name'
     @group_name = 'some_group_name'
     Puppet.features.stubs(:root?).returns(true)
-    @execer = Puppet::Type.type(:exec).new(:name => command, :path => 
@example_path, :user => @user_name, :group => @group_name, :returns => returns)
 
-    status = stub "process"
-    status.stubs(:exitstatus).returns(exitstatus)
+    output = rest.delete(:output) || ''
+    tries  = rest[:tries] || 1
 
-    Puppet::Util::SUIDManager.expects(:run_and_capture).with([command], 
@user_name, @group_name).returns([output, status])
-  end
+    args = {
+      :name      => command,
+      :path      => @example_path,
+      :user      => @user_name,
+      :group     => @group_name,
+      :logoutput => false,
+      :loglevel  => :err,
+      :returns   => 0
+    }.merge(rest)
 
-  def create_logging_resource(command, output, exitstatus, logoutput, 
loglevel, returns = 0)
-    create_resource(command, output, exitstatus, returns)
-    @execer[:logoutput] = logoutput
-    @execer[:loglevel] = loglevel
-  end
+    exec = Puppet::Type.type(:exec).new(args)
 
-  def expect_output(output, loglevel)
-    output.split(/\n/).each do |line|
-      @execer.property(:returns).expects(loglevel).with(line)
-    end
+    status = stub "process", :exitstatus => exitstatus
+    Puppet::Util::SUIDManager.expects(:run_and_capture).times(tries).
+      with([command], @user_name, @group_name).returns([output, status])
+
+    return exec
   end
 
   before do
@@ -44,119 +45,626 @@ describe Puppet::Type.type(:exec) do
   end
 
   describe "when execing" do
-
     it "should use the 'run_and_capture' method to exec" do
-      command = "true"
-      create_resource(command, "", 0)
-
-      @execer.refresh.should == :executed_command
+      exec_tester("true").refresh.should == :executed_command
     end
 
     it "should report a failure" do
-      command = "false"
-      create_resource(command, "", 1)
-
-      proc { @execer.refresh }.should raise_error(Puppet::Error)
+      proc { exec_tester('false', 1).refresh }.
+        should raise_error(Puppet::Error, /^false returned 1 instead of/)
     end
 
     it "should not report a failure if the exit status is specified in a 
returns array" do
-      command = "false"
-      create_resource(command, "", 1, [0,1])
-      proc { @execer.refresh }.should_not raise_error(Puppet::Error)
+      proc { exec_tester("false", 1, :returns => [0, 1]).refresh }.should_not 
raise_error
     end
 
     it "should report a failure if the exit status is not specified in a 
returns array" do
-      command = "false"
-      create_resource(command, "", 1, [0,100])
-      proc { @execer.refresh }.should raise_error(Puppet::Error)
+      proc { exec_tester('false', 1, :returns => [0, 100]).refresh }.
+        should raise_error(Puppet::Error, /^false returned 1 instead of/)
     end
 
     it "should log the output on success" do
-      #Puppet::Util::Log.newdestination :console
-      command = "false"
       output = "output1\noutput2\n"
-      create_logging_resource(command, output, 0, true, :err)
-      expect_output(output, :err)
-      @execer.refresh
+      exec_tester('false', 0, :output => output, :logoutput => true).refresh
+      output.split("\n").each do |line|
+        log = @logs.shift
+        log.level.should == :err
+        log.message.should == line
+      end
     end
 
     it "should log the output on failure" do
-      #Puppet::Util::Log.newdestination :console
-      command = "false"
       output = "output1\noutput2\n"
-      create_logging_resource(command, output, 1, true, :err)
-      expect_output(output, :err)
+      proc { exec_tester('false', 1, :output => output, :logoutput => 
true).refresh }.
+        should raise_error(Puppet::Error)
 
-      proc { @execer.refresh }.should raise_error(Puppet::Error)
+      output.split("\n").each do |line|
+        log = @logs.shift
+        log.level.should == :err
+        log.message.should == line
+      end
     end
-
   end
 
   describe "when logoutput=>on_failure is set" do
-
     it "should log the output on failure" do
-      #Puppet::Util::Log.newdestination :console
-      command = "false"
       output = "output1\noutput2\n"
-      create_logging_resource(command, output, 1, :on_failure, :err)
-      expect_output(output, :err)
+      proc { exec_tester('false', 1, :output => output, :logoutput => 
:on_failure).refresh }.
+        should raise_error(Puppet::Error, /^false returned 1 instead of/)
 
-      proc { @execer.refresh }.should raise_error(Puppet::Error)
+      output.split("\n").each do |line|
+        log = @logs.shift
+        log.level.should == :err
+        log.message.should == line
+      end
     end
 
     it "should log the output on failure when returns is specified as an 
array" do
-      #Puppet::Util::Log.newdestination :console
-      command = "false"
       output = "output1\noutput2\n"
-      create_logging_resource(command, output, 1, :on_failure, :err, [0, 100])
-      expect_output(output, :err)
 
-      proc { @execer.refresh }.should raise_error(Puppet::Error)
+      proc {
+        exec_tester('false', 1, :output => output, :returns => [0, 100],
+             :logoutput => :on_failure).refresh
+      }.should raise_error(Puppet::Error, /^false returned 1 instead of/)
+
+      output.split("\n").each do |line|
+        log = @logs.shift
+        log.level.should == :err
+        log.message.should == line
+      end
     end
 
     it "shouldn't log the output on success" do
-      #Puppet::Util::Log.newdestination :console
-      command = "true"
-      output = "output1\noutput2\n"
-      create_logging_resource(command, output, 0, :on_failure, :err)
-      @execer.property(:returns).expects(:err).never
-      @execer.refresh
+      exec_tester('true', 0, :output => "a\nb\nc\n", :logoutput => 
:on_failure).refresh
+      @logs.should == []
     end
   end
 
   it "shouldn't log the output on success when non-zero exit status is in a 
returns array" do
-    #Puppet::Util::Log.newdestination :console
-    command = "true"
-    output = "output1\noutput2\n"
-    create_logging_resource(command, output, 100, :on_failure, :err, [1,100])
-    @execer.property(:returns).expects(:err).never
-    @execer.refresh
+    exec_tester("true", 100, :output => "a\n", :logoutput => :on_failure, 
:returns => [1, 100]).refresh
+    @logs.should == []
   end
 
   describe " when multiple tries are set," do
-
     it "should repeat the command attempt 'tries' times on failure and produce 
an error" do
-      Puppet.features.stubs(:root?).returns(true)
-      command = "false"
-      user = "user"
-      group = "group"
       tries = 5
-      retry_exec = Puppet::Type.type(:exec).new(:name => command, :path => 
%w{/usr/bin /bin}, :user => user, :group => group, :returns => 0, :tries => 
tries, :try_sleep => 0)
-      status = stub "process"
-      status.stubs(:exitstatus).returns(1)
-      Puppet::Util::SUIDManager.expects(:run_and_capture).with([command], 
user, group).times(tries).returns(["", status])
-      proc { retry_exec.refresh }.should raise_error(Puppet::Error)
+      resource = exec_tester("false", 1, :tries => tries, :try_sleep => 0)
+      proc { resource.refresh }.should raise_error(Puppet::Error)
     end
   end
 
   it "should be able to autorequire files mentioned in the command" do
     catalog = Puppet::Resource::Catalog.new
-    catalog.add_resource Puppet::Type.type(:file).new(:name => @executable)
-    @execer = Puppet::Type.type(:exec).new(:name => @command)
-    catalog.add_resource @execer
+    tmp = Puppet::Type.type(:file).new(:name => "/bin/foo")
+    catalog.add_resource tmp
+    execer = Puppet::Type.type(:exec).new(:name => "/bin/foo")
+    catalog.add_resource execer
+
+    catalog.relationship_graph.dependencies(execer).should == [tmp]
+  end
+
+  describe "when handling the path parameter" do
+    expect = %w{one two three four}
+    { "an array"                        => expect,
+      "a colon separated list"          => "one:two:three:four",
+      "a semi-colon separated list"     => "one;two;three;four",
+      "both array and colon lists"      => ["one", "two:three", "four"],
+      "both array and semi-colon lists" => ["one", "two;three", "four"],
+      "colon and semi-colon lists"      => ["one:two", "three;four"]
+    }.each do |test, input|
+      it "should accept #{test}" do
+        type = Puppet::Type.type(:exec).new(:name => @command, :path => input)
+        type[:path].should == expect
+      end
+    end
+  end
+
+  describe "when setting user" do
+    it "should fail if we are not root" do
+      Puppet.features.stubs(:root?).returns(false)
+      expect { Puppet::Type.type(:exec).new(:name => @command, :user => 
'input') }.
+        should raise_error Puppet::Error, /Parameter user failed/
+    end
+
+    ['one', 2, 'root', 4294967295, 4294967296].each do |value|
+      it "should accept '#{value}' as user if we are root" do
+        Puppet.features.stubs(:root?).returns(true)
+        type = Puppet::Type.type(:exec).new(:name => @command, :user => value)
+        type[:user].should == value
+      end
+    end
+  end
+
+  describe "when setting group" do
+    shared_examples_for "exec[:group]" do
+      ['one', 2, 'wheel', 4294967295, 4294967296].each do |value|
+        it "should accept '#{value}' without error or judgement" do
+          type = Puppet::Type.type(:exec).new(:name => @command, :group => 
value)
+          type[:group].should == value
+        end
+      end
+    end
+
+    describe "when running as root" do
+      before :each do Puppet.features.stubs(:root?).returns(true) end
+      it_behaves_like "exec[:group]"
+    end
+
+    describe "when not running as root" do
+      before :each do Puppet.features.stubs(:root?).returns(false) end
+      it_behaves_like "exec[:group]"
+    end
+  end
+
+  describe "when setting cwd" do
+    it_should_behave_like "all path parameters", :cwd, :array => false do
+      def instance(path)
+        Puppet::Type.type(:exec).new(:name => '/bin/true', :cwd => path)
+      end
+    end
+  end
+
+  shared_examples_for "all exec command parameters" do |param|
+    { "relative" => "example", "absolute" => "/bin/example" }.sort.each do 
|name, command|
+      describe "if command is #{name}" do
+        before :each do
+          @param = param
+        end
+
+        def test(command, valid)
+          if @param == :name then
+            instance = Puppet::Type.type(:exec).new()
+          else
+            instance = Puppet::Type.type(:exec).new(:name => "/bin/true")
+          end
+          if valid then
+            instance.provider.expects(:validatecmd).returns(true)
+          else
+            instance.provider.expects(:validatecmd).raises(Puppet::Error, 
"from a stub")
+          end
+          instance[@param] = command
+        end
+
+        it "should work if the provider calls the command valid" do
+          expect { test(command, true) }.should_not raise_error
+        end
+
+        it "should fail if the provider calls the command invalid" do
+          expect { test(command, false) }.
+            should raise_error Puppet::Error, /Parameter #{@param} failed: 
from a stub/
+        end
+      end
+    end
+  end
+
+  shared_examples_for "all exec command parameters that take arrays" do |param|
+    describe "when given an array of inputs" do
+      before :each do
+        @test = Puppet::Type.type(:exec).new(:name => "/bin/true")
+      end
+
+      it "should accept the array when all commands return valid" do
+        input = %w{one two three}
+        @test.provider.expects(:validatecmd).times(input.length).returns(true)
+        @test[param] = input
+        @test[param].should == input
+      end
+
+      it "should reject the array when any commands return invalid" do
+        input = %w{one two three}
+        @test.provider.expects(:validatecmd).with(input.first).returns(false)
+        input[1..-1].each do |cmd|
+          @test.provider.expects(:validatecmd).with(cmd).returns(true)
+        end
+        @test[param] = input
+        @test[param].should == input
+      end
+
+      it "should reject the array when all commands return invalid" do
+        input = %w{one two three}
+        @test.provider.expects(:validatecmd).times(input.length).returns(false)
+        @test[param] = input
+        @test[param].should == input
+      end
+    end
+  end
+
+  describe "when setting refresh" do
+    it_should_behave_like "all exec command parameters", :refresh
+  end
+
+  describe "for simple parameters" do
+    before :each do
+      @exec = Puppet::Type.type(:exec).new(:name => '/bin/true')
+    end
+
+    describe "when setting env" do
+      it "should issue a deprecation warning" do
+        expect { @exec[:env] = 'foo=bar' }.should_not raise_error
+        @logs.first.message.should =~ /deprecate.*environment/
+      end
+
+      it "should update the value of env" do
+        data = ['foo=bar']
+        @exec[:env] = data
+        @exec[:env].should == data
+      end
+
+      it "should forward to environment" do
+        data = ['foo=bar']
+        @exec[:env] = data
+        @exec[:environment].should == data
+      end
+
+      it "should not override environment if both are set" do
+        pending "can't fix: too disruptive for 2.6, removed in 2.7"
+        # ...so this test is here to validate that we know about the problem.
+        # This ensures correct order of evaluation to trigger the bug; don't
+        # count on this happening in the constructor. --daniel 2011-03-01
+        @exec[:environment] = 'environment=true'
+        @exec[:env]         = 'env=true'
+
+        @exec[:environment].should == "environment=true"
+      end
+    end
+
+    describe "when setting environment" do
+      { "single values"   => "foo=bar",
+        "multiple values" => ["foo=bar", "baz=quux"],
+      }.each do |name, data|
+        it "should accept #{name}" do
+          @exec[:environment] = data
+          @exec[:environment].should == data
+        end
+      end
+
+      { "single values" => "foo",
+        "only values"   => ["foo", "bar"],
+        "any values"    => ["foo=bar", "baz"]
+      }.each do |name, data|
+        it "should reject #{name} without assignment" do
+          expect { @exec[:environment] = data }.
+            should raise_error Puppet::Error, /Invalid environment setting/
+        end
+      end
+    end
+
+    describe "when setting timeout" do
+      [-3.5, -1, 0, 0.1, 1, 10, 4294967295].each do |valid|
+        it "should accept '#{valid}' as valid" do
+          @exec[:timeout] = valid
+          @exec[:timeout].should == valid
+        end
+
+        it "should accept '#{valid}' in an array as valid" do
+          @exec[:timeout] = [valid]
+          @exec[:timeout].should == valid
+        end
+      end
+
+      ['1/2', '1_000_000', '+12', '', 'foo'].each do |invalid|
+        it "should reject '#{invalid}' as invalid" do
+          expect { @exec[:timeout] = invalid }.
+            should raise_error Puppet::Error, /The timeout must be a number/
+        end
+
+        it "should reject '#{invalid}' in an array as invalid" do
+          expect { @exec[:timeout] = [invalid] }.
+            should raise_error Puppet::Error, /The timeout must be a number/
+        end
+      end
+
+      it "should fail if timeout is exceeded" do
+        File.stubs(:exists?).with('/bin/sleep').returns(true)
+        sleep_exec = Puppet::Type.type(:exec).new(:name => 'sleep 1', :path => 
['/bin'], :timeout => '0.2')
+        lambda { sleep_exec.refresh }.should raise_error Puppet::Error, 
"Command exceeded timeout"
+      end
+    end
+
+    describe "when setting tries" do
+      [1, 10, 4294967295].each do |valid|
+        it "should accept '#{valid}' as valid" do
+          @exec[:tries] = valid
+          @exec[:tries].should == valid
+        end
+
+        if "REVISIT: too much test log spam" == "a good thing" then
+          it "should accept '#{valid}' in an array as valid" do
+            pending "inconsistent, but this is not supporting arrays, unlike 
timeout"
+            @exec[:tries] = [valid]
+            @exec[:tries].should == valid
+          end
+        end
+      end
+
+      [-3.5, -1, 0, 0.2, '1/2', '1_000_000', '+12', '', 'foo'].each do 
|invalid|
+        it "should reject '#{invalid}' as invalid" do
+          expect { @exec[:tries] = invalid }.
+            should raise_error Puppet::Error, /Tries must be an integer/
+        end
 
-    rels = @execer.autorequire
-    rels[0].should be_instance_of(Puppet::Relationship)
-    rels[0].target.should equal(@execer)
+        if "REVISIT: too much test log spam" == "a good thing" then
+          it "should reject '#{invalid}' in an array as invalid" do
+            pending "inconsistent, but this is not supporting arrays, unlike 
timeout"
+            expect { @exec[:tries] = [invalid] }.
+              should raise_error Puppet::Error, /Tries must be an integer/
+          end
+        end
+      end
+    end
+
+    describe "when setting try_sleep" do
+      [0, 0.2, 1, 10, 4294967295].each do |valid|
+        it "should accept '#{valid}' as valid" do
+          @exec[:try_sleep] = valid
+          @exec[:try_sleep].should == valid
+        end
+
+        if "REVISIT: too much test log spam" == "a good thing" then
+          it "should accept '#{valid}' in an array as valid" do
+            pending "inconsistent, but this is not supporting arrays, unlike 
timeout"
+            @exec[:try_sleep] = [valid]
+            @exec[:try_sleep].should == valid
+          end
+        end
+      end
+
+      { -3.5        => "cannot be a negative number",
+        -1          => "cannot be a negative number",
+        '1/2'       => 'must be a number',
+        '1_000_000' => 'must be a number',
+        '+12'       => 'must be a number',
+        ''          => 'must be a number',
+        'foo'       => 'must be a number',
+      }.each do |invalid, error|
+        it "should reject '#{invalid}' as invalid" do
+          expect { @exec[:try_sleep] = invalid }.
+            should raise_error Puppet::Error, /try_sleep #{error}/
+        end
+
+        if "REVISIT: too much test log spam" == "a good thing" then
+          it "should reject '#{invalid}' in an array as invalid" do
+            pending "inconsistent, but this is not supporting arrays, unlike 
timeout"
+            expect { @exec[:try_sleep] = [invalid] }.
+              should raise_error Puppet::Error, /try_sleep #{error}/
+          end
+        end
+      end
+    end
+
+    describe "when setting refreshonly" do
+      [:true, :false].each do |value|
+        it "should accept '#{value}'" do
+          @exec[:refreshonly] = value
+          @exec[:refreshonly].should == value
+        end
+      end
+
+      [1, 0, "1", "0", "yes", "y", "no", "n"].each do |value|
+        it "should reject '#{value}'" do
+          expect { @exec[:refreshonly] = value }.
+            should raise_error(Puppet::Error,
+              /Invalid value #{value.inspect}\. Valid values are true, false/
+            )
+        end
+      end
+    end
+
+    describe "when setting creates" do
+      it_should_behave_like "all path parameters", :creates, :array => true do
+        def instance(path)
+          Puppet::Type.type(:exec).new(:name => '/bin/true', :creates => path)
+        end
+      end
+    end
+  end
+
+  describe "when setting unless" do
+    it_should_behave_like "all exec command parameters", :unless
+    it_should_behave_like "all exec command parameters that take arrays", 
:unless
+  end
+
+  describe "when setting onlyif" do
+    it_should_behave_like "all exec command parameters", :onlyif
+    it_should_behave_like "all exec command parameters that take arrays", 
:onlyif
+  end
+
+  describe "#check" do
+    before :each do
+      @test = Puppet::Type.type(:exec).new(:name => "/bin/true")
+    end
+
+    describe ":refreshonly" do
+      { :true => false, :false => true }.each do |input, result|
+        it "should return '#{result}' when given '#{input}'" do
+          @test[:refreshonly] = input
+          @test.check_all_attributes.should == result
+        end
+      end
+    end
+
+    describe ":creates" do
+      before :all do
+        @exist   = "/"
+        @unexist = "/this/path/should/never/exist"
+        while FileTest.exist?(@unexist) do @unexist += "/foo" end
+      end
+
+      context "with a single item" do
+        it "should run when the item does not exist" do
+          @test[:creates] = @unexist
+          @test.check_all_attributes.should == true
+        end
+
+        it "should not run when the item exists" do
+          @test[:creates] = @exist
+          @test.check_all_attributes.should == false
+        end
+      end
+
+      context "with an array with one item" do
+        it "should run when the item does not exist" do
+          @test[:creates] = [@unexist]
+          @test.check_all_attributes.should == true
+        end
+
+        it "should not run when the item exists" do
+          @test[:creates] = [@exist]
+          @test.check_all_attributes.should == false
+        end
+      end
+
+      context "with an array with multiple items" do
+        it "should run when all items do not exist" do
+          @test[:creates] = [@unexist] * 3
+          @test.check_all_attributes.should == true
+        end
+
+        it "should not run when one item exists" do
+          @test[:creates] = [@unexist, @exist, @unexist]
+          @test.check_all_attributes.should == false
+        end
+
+        it "should not run when all items exist" do
+          @test[:creates] = [@exist] * 3
+        end
+      end
+    end
+
+    { :onlyif => { :pass => false, :fail => true  },
+      :unless => { :pass => true,  :fail => false },
+    }.each do |param, sense|
+      describe ":#{param}" do
+        before :each do
+          @pass = "/magic/pass"
+          @fail = "/magic/fail"
+
+          @pass_status = stub('status', :exitstatus => sense[:pass] ? 0 : 1)
+          @fail_status = stub('status', :exitstatus => sense[:fail] ? 0 : 1)
+
+          @test.provider.stubs(:checkexe).returns(true)
+          [true, false].each do |check|
+            @test.provider.stubs(:run).with(@pass, check).
+              returns(['test output', @pass_status])
+            @test.provider.stubs(:run).with(@fail, check).
+              returns(['test output', @fail_status])
+          end
+        end
+
+        context "with a single item" do
+          it "should run if the command exits non-zero" do
+            @test[param] = @fail
+            @test.check_all_attributes.should == true
+          end
+
+          it "should not run if the command exits zero" do
+            @test[param] = @pass
+            @test.check_all_attributes.should == false
+          end
+        end
+
+        context "with an array with a single item" do
+          it "should run if the command exits non-zero" do
+            @test[param] = [@fail]
+            @test.check_all_attributes.should == true
+          end
+
+          it "should not run if the command exits zero" do
+            @test[param] = [@pass]
+            @test.check_all_attributes.should == false
+          end
+        end
+
+        context "with an array with multiple items" do
+          it "should run if all the commands exits non-zero" do
+            @test[param] = [@fail] * 3
+            @test.check_all_attributes.should == true
+          end
+
+          it "should not run if one command exits zero" do
+            @test[param] = [@pass, @fail, @pass]
+            @test.check_all_attributes.should == false
+          end
+
+          it "should not run if all command exits zero" do
+            @test[param] = [@pass] * 3
+            @test.check_all_attributes.should == false
+          end
+        end
+      end
+    end
+  end
+
+  describe "#retrieve" do
+    before :each do
+      @exec_resource = Puppet::Type.type(:exec).new(:name => "/bogus/cmd")
+    end
+
+    it "should return :notrun when check_all_attributes returns true" do
+      @exec_resource.stubs(:check_all_attributes).returns true
+      @exec_resource.retrieve[:returns].should == :notrun
+    end
+
+    it "should return default exit code 0 when check_all_attributes returns 
false" do
+      @exec_resource.stubs(:check_all_attributes).returns false
+      @exec_resource.retrieve[:returns].should == ['0']
+    end
+
+    it "should return the specified exit code when check_all_attributes 
returns false" do
+      @exec_resource.stubs(:check_all_attributes).returns false
+      @exec_resource[:returns] = 42
+      @exec_resource.retrieve[:returns].should == ["42"]
+    end
+  end
+
+  describe "#output" do
+    before :each do
+      @exec_resource = Puppet::Type.type(:exec).new(:name => "/bogus/cmd")
+    end
+
+    it "should return the provider's run output" do
+      provider = stub 'provider'
+      status = stubs "process_status"
+      status.stubs(:exitstatus).returns("0")
+      provider.expects(:run).returns(["silly output", status])
+      @exec_resource.stubs(:provider).returns(provider)
+
+      @exec_resource.refresh
+      @exec_resource.output.should == 'silly output'
+    end
+  end
+
+  describe "#refresh" do
+    before :each do
+      @exec_resource = Puppet::Type.type(:exec).new(:name => "/bogus/cmd")
+    end
+
+    it "should call provider run with the refresh parameter if it is set" do
+      provider = stub 'provider'
+      @exec_resource.stubs(:provider).returns(provider)
+      @exec_resource.stubs(:[]).with(:refresh).returns('/myother/bogus/cmd')
+      provider.expects(:run).with('/myother/bogus/cmd')
+
+      @exec_resource.refresh
+    end
+
+    it "should call provider run with the specified command if the refresh 
parameter is not set" do
+      provider = stub 'provider'
+      status = stubs "process_status"
+      status.stubs(:exitstatus).returns("0")
+      provider.expects(:run).with('/bogus/cmd').returns(["silly output", 
status])
+      @exec_resource.stubs(:provider).returns(provider)
+
+      @exec_resource.refresh
+    end
+
+    it "should not run the provider if check_all_attributes is false" do
+      @exec_resource.stubs(:check_all_attributes).returns false
+      provider = stub 'provider'
+      provider.expects(:run).never
+      @exec_resource.stubs(:provider).returns(provider)
+
+      @exec_resource.refresh
+    end
   end
 end
-- 
1.7.4

-- 
You received this message because you are subscribed to the Google Groups 
"Puppet Developers" group.
To post to this group, send email to puppet-dev@googlegroups.com.
To unsubscribe from this group, send email to 
puppet-dev+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/puppet-dev?hl=en.

Reply via email to