#! /usr/bin/env ruby require 'spec_helper' require 'facter/util/resolution' describe Facter::Util::Resolution do include FacterSpec::ConfigHelper it "should require a name" do lambda { Facter::Util::Resolution.new }.should raise_error(ArgumentError) end it "should have a name" do Facter::Util::Resolution.new("yay").name.should == "yay" end it "should be able to set the value" do resolve = Facter::Util::Resolution.new("yay") resolve.value = "foo" resolve.value.should == "foo" end it "should have a method for setting the weight" do Facter::Util::Resolution.new("yay").should respond_to(:has_weight) end it "should have a method for setting the code" do Facter::Util::Resolution.new("yay").should respond_to(:setcode) end it "should support a timeout value" do Facter::Util::Resolution.new("yay").should respond_to(:timeout=) end it "should default to a timeout of 0 seconds" do Facter::Util::Resolution.new("yay").limit.should == 0 end it "should default to nil for code" do Facter::Util::Resolution.new("yay").code.should be_nil end it "should default to nil for interpreter" do Facter.expects(:warnonce).with("The 'Facter::Util::Resolution.interpreter' method is deprecated and will be removed in a future version.") Facter::Util::Resolution.new("yay").interpreter.should be_nil end it "should provide a 'limit' method that returns the timeout" do res = Facter::Util::Resolution.new("yay") res.timeout = "testing" res.limit.should == "testing" end describe "when overriding environment variables" do it "should execute the caller's block with the specified env vars" do test_env = { "LANG" => "C", "FOO" => "BAR" } Facter::Util::Resolution.with_env test_env do test_env.keys.each do |key| ENV[key].should == test_env[key] end end end it "should restore pre-existing environment variables to their previous values" do orig_env = {} new_env = {} # an arbitrary sentinel value to use to temporarily set the environment vars to sentinel_value = "Abracadabra" # grab some values from the existing ENV (arbitrarily choosing 3 here) ENV.keys.first(3).each do |key| # save the original values so that we can test against them later orig_env[key] = ENV[key] # create bogus temp values for the chosen keys new_env[key] = sentinel_value end # verify that, during the 'with_env', the new values are used Facter::Util::Resolution.with_env new_env do orig_env.keys.each do |key| ENV[key].should == new_env[key] end end # verify that, after the 'with_env', the old values are restored orig_env.keys.each do |key| ENV[key].should == orig_env[key] end end it "should not be affected by a 'return' statement in the yield block" do @sentinel_var = :resolution_test_foo.to_s # the intent of this test case is to test a yield block that contains a return statement. However, it's illegal # to use a return statement outside of a method, so we need to create one here to give scope to the 'return' def handy_method() ENV[@sentinel_var] = "foo" new_env = { @sentinel_var => "bar" } Facter::Util::Resolution.with_env new_env do ENV[@sentinel_var].should == "bar" return end end handy_method() ENV[@sentinel_var].should == "foo" end end describe "when setting the code" do before do Facter.stubs(:warnonce) @resolve = Facter::Util::Resolution.new("yay") end it "should deprecate the interpreter argument to 'setcode'" do Facter.expects(:warnonce).with("The interpreter parameter to 'setcode' is deprecated and will be removed in a future version.") @resolve.setcode "foo", "bar" @resolve.interpreter.should == "bar" end it "should deprecate the interpreter= method" do Facter.expects(:warnonce).with("The 'Facter::Util::Resolution.interpreter=' method is deprecated and will be removed in a future version.") @resolve.interpreter = "baz" @resolve.interpreter.should == "baz" end it "should deprecate the interpreter method" do Facter.expects(:warnonce).with("The 'Facter::Util::Resolution.interpreter' method is deprecated and will be removed in a future version.") @resolve.interpreter end it "should set the code to any provided string" do @resolve.setcode "foo" @resolve.code.should == "foo" end it "should set the code to any provided block" do block = lambda { } @resolve.setcode(&block) @resolve.code.should equal(block) end it "should prefer the string over a block" do @resolve.setcode("foo") { } @resolve.code.should == "foo" end it "should fail if neither a string nor block has been provided" do lambda { @resolve.setcode }.should raise_error(ArgumentError) end end describe 'callbacks when flushing facts' do class FlushFakeError < StandardError; end subject do Facter::Util::Resolution.new("jeff") end context '#on_flush' do it 'accepts a block with on_flush' do subject.on_flush() { raise NotImplementedError } end end context '#flush' do it 'calls the block passed to on_flush' do subject.on_flush() { raise FlushFakeError } expect { subject.flush }.to raise_error FlushFakeError end end end it "should be able to return a value" do Facter::Util::Resolution.new("yay").should respond_to(:value) end describe "when returning the value" do before do @resolve = Facter::Util::Resolution.new("yay") end it "should return any value that has been provided" do @resolve.value = "foo" @resolve.value.should == "foo" end describe "and setcode has not been called" do it "should return nil" do Facter::Util::Resolution.expects(:exec).with(nil, nil).never @resolve.value.should be_nil end end describe "and the code is a string" do describe "on windows" do before do given_a_configuration_of(:is_windows => true) end it "should return the result of executing the code" do @resolve.setcode "/bin/foo" Facter::Util::Resolution.expects(:exec).once.with("/bin/foo").returns "yup" @resolve.value.should == "yup" end it "should return nil if the value is an empty string" do @resolve.setcode "/bin/foo" Facter::Util::Resolution.expects(:exec).once.returns "" @resolve.value.should be_nil end end describe "on non-windows systems" do before do given_a_configuration_of(:is_windows => false) end it "should return the result of executing the code" do @resolve.setcode "/bin/foo" Facter::Util::Resolution.expects(:exec).once.with("/bin/foo").returns "yup" @resolve.value.should == "yup" end it "should return nil if the value is an empty string" do @resolve.setcode "/bin/foo" Facter::Util::Resolution.expects(:exec).once.returns "" @resolve.value.should be_nil end end end describe "and the code is a block" do it "should warn but not fail if the code fails" do @resolve.setcode { raise "feh" } Facter.expects(:warn) @resolve.value.should be_nil end it "should return the value returned by the block" do @resolve.setcode { "yayness" } @resolve.value.should == "yayness" end it "should return nil if the value is an empty string" do @resolve.setcode { "" } @resolve.value.should be_nil end it "should return nil if the value is an empty block" do @resolve.setcode { "" } @resolve.value.should be_nil end it "should use its limit method to determine the timeout, to avoid conflict when a 'timeout' method exists for some other reason" do @resolve.expects(:timeout).never @resolve.expects(:limit).returns "foo" Timeout.expects(:timeout).with("foo") @resolve.setcode { sleep 2; "raise This is a test"} @resolve.value end it "should timeout after the provided timeout" do Facter.expects(:warn) @resolve.timeout = 0.1 @resolve.setcode { sleep 2; raise "This is a test" } Thread.expects(:new).yields @resolve.value.should be_nil end it "should waitall to avoid zombies if the timeout is exceeded" do Facter.stubs(:warn) @resolve.timeout = 0.1 @resolve.setcode { sleep 2; raise "This is a test" } Thread.expects(:new).yields Process.expects(:waitall) @resolve.value end end end it "should return its value when converted to a string" do @resolve = Facter::Util::Resolution.new("yay") @resolve.expects(:value).returns "myval" @resolve.to_s.should == "myval" end it "should allow the adding of confines" do Facter::Util::Resolution.new("yay").should respond_to(:confine) end it "should provide a method for returning the number of confines" do @resolve = Facter::Util::Resolution.new("yay") @resolve.confine "one" => "foo", "two" => "fee" @resolve.weight.should == 2 end it "should return 0 confines when no confines have been added" do Facter::Util::Resolution.new("yay").weight.should == 0 end it "should provide a way to set the weight" do @resolve = Facter::Util::Resolution.new("yay") @resolve.has_weight(45) @resolve.weight.should == 45 end it "should allow the weight to override the number of confines" do @resolve = Facter::Util::Resolution.new("yay") @resolve.confine "one" => "foo", "two" => "fee" @resolve.weight.should == 2 @resolve.has_weight(45) @resolve.weight.should == 45 end it "should have a method for determining if it is suitable" do Facter::Util::Resolution.new("yay").should respond_to(:suitable?) end describe "when adding confines" do before do @resolve = Facter::Util::Resolution.new("yay") end it "should accept a hash of fact names and values" do lambda { @resolve.confine :one => "two" }.should_not raise_error end it "should create a Util::Confine instance for every argument in the provided hash" do Facter::Util::Confine.expects(:new).with("one", "foo") Facter::Util::Confine.expects(:new).with("two", "fee") @resolve.confine "one" => "foo", "two" => "fee" end end describe "when determining suitability" do before do @resolve = Facter::Util::Resolution.new("yay") end it "should always be suitable if no confines have been added" do @resolve.should be_suitable end it "should be unsuitable if any provided confines return false" do confine1 = mock 'confine1', :true? => true confine2 = mock 'confine2', :true? => false Facter::Util::Confine.expects(:new).times(2).returns(confine1).then.returns(confine2) @resolve.confine :one => :two, :three => :four @resolve.should_not be_suitable end it "should be suitable if all provided confines return true" do confine1 = mock 'confine1', :true? => true confine2 = mock 'confine2', :true? => true Facter::Util::Confine.expects(:new).times(2).returns(confine1).then.returns(confine2) @resolve.confine :one => :two, :three => :four @resolve.should be_suitable end end it "should have a class method for executing code" do Facter::Util::Resolution.should respond_to(:exec) end # taken from puppet: spec/unit/util_spec.rb describe "#absolute_path?" do context "when run on unix", :as_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 Facter::Util::Resolution.should be_absolute_path(path) 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 Facter::Util::Resolution.should_not be_absolute_path(path) end end end context "when run on windows", :as_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 Facter::Util::Resolution.should be_absolute_path(path) 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 Facter::Util::Resolution.should_not be_absolute_path(path) end end end end describe "#search_paths" do context "on windows", :as_platform => :windows do it "should use the PATH environment variable to determine locations" do ENV.expects(:[]).with('PATH').returns 'C:\Windows;C:\Windows\System32' Facter::Util::Resolution.search_paths.should == %w{C:\Windows C:\Windows\System32} end end context "on posix", :as_platform => :posix do it "should use the PATH environment variable plus /sbin and /usr/sbin on unix" do ENV.expects(:[]).with('PATH').returns "/bin:/usr/bin" Facter::Util::Resolution.search_paths.should == %w{/bin /usr/bin /sbin /usr/sbin} end end end describe "#which" do context "when run on posix", :as_platform => :posix do before :each do Facter::Util::Resolution.stubs(:search_paths).returns [ '/bin', '/sbin', '/usr/sbin'] end context "and provided with an absolute path" do it "should return the binary if executable" do File.expects(:executable?).with('/opt/foo').returns true Facter::Util::Resolution.which('/opt/foo').should == '/opt/foo' end it "should return nil if the binary is not executable" do File.expects(:executable?).with('/opt/foo').returns false Facter::Util::Resolution.which('/opt/foo').should be_nil end end context "and not provided with an absolute path" do it "should return the absolute path if found" do File.expects(:executable?).with('/bin/foo').returns false File.expects(:executable?).with('/sbin/foo').returns true File.expects(:executable?).with('/usr/sbin/foo').never Facter::Util::Resolution.which('foo').should == '/sbin/foo' end it "should return nil if not found" do File.expects(:executable?).with('/bin/foo').returns false File.expects(:executable?).with('/sbin/foo').returns false File.expects(:executable?).with('/usr/sbin/foo').returns false Facter::Util::Resolution.which('foo').should be_nil end end end context "when run on windows", :as_platform => :windows do before :each do Facter::Util::Resolution.stubs(:search_paths).returns ['C:\Windows\system32', 'C:\Windows', 'C:\Windows\System32\Wbem' ] ENV.stubs(:[]).with('PATHEXT').returns nil end context "and provided with an absolute path" do it "should return the binary if executable" do File.expects(:executable?).with('C:\Tools\foo.exe').returns true File.expects(:executable?).with('\\\\remote\dir\foo.exe').returns true Facter::Util::Resolution.which('C:\Tools\foo.exe').should == 'C:\Tools\foo.exe' Facter::Util::Resolution.which('\\\\remote\dir\foo.exe').should == '\\\\remote\dir\foo.exe' end it "should return the binary with added extension if executable" do ['.COM', '.BAT', '.CMD', '' ].each do |ext| File.stubs(:executable?).with('C:\Windows\system32\netsh'+ext).returns false end File.expects(:executable?).with('C:\Windows\system32\netsh.EXE').returns true Facter.expects(:warnonce).with('Using Facter::Util::Resolution.which with an absolute path like C:\\Windows\\system32\\netsh but no fileextension is deprecated. Please add the correct extension (.EXE)') Facter::Util::Resolution.which('C:\Windows\system32\netsh').should == 'C:\Windows\system32\netsh.EXE' end it "should return nil if the binary is not executable" do File.expects(:executable?).with('C:\Tools\foo.exe').returns false File.expects(:executable?).with('\\\\remote\dir\foo.exe').returns false Facter::Util::Resolution.which('C:\Tools\foo.exe').should be_nil Facter::Util::Resolution.which('\\\\remote\dir\foo.exe').should be_nil end end context "and not provided with an absolute path" do it "should return the absolute path if found" do File.expects(:executable?).with('C:\Windows\system32\foo.exe').returns false File.expects(:executable?).with('C:\Windows\foo.exe').returns true File.expects(:executable?).with('C:\Windows\System32\Wbem\foo.exe').never Facter::Util::Resolution.which('foo.exe').should == 'C:\Windows\foo.exe' end it "should return the absolute path with file extension if found" do ['.COM', '.EXE', '.BAT', '.CMD', '' ].each do |ext| File.stubs(:executable?).with('C:\Windows\system32\foo'+ext).returns false File.stubs(:executable?).with('C:\Windows\System32\Wbem\foo'+ext).returns false end ['.COM', '.BAT', '.CMD', '' ].each do |ext| File.stubs(:executable?).with('C:\Windows\foo'+ext).returns false end File.stubs(:executable?).with('C:\Windows\foo.EXE').returns true Facter::Util::Resolution.which('foo').should == 'C:\Windows\foo.EXE' end it "should return nil if not found" do File.expects(:executable?).with('C:\Windows\system32\foo.exe').returns false File.expects(:executable?).with('C:\Windows\foo.exe').returns false File.expects(:executable?).with('C:\Windows\System32\Wbem\foo.exe').returns false Facter::Util::Resolution.which('foo.exe').should be_nil end end end describe "#expand_command" do context "on windows", :as_platform => :windows do it "should expand binary" do Facter::Util::Resolution.expects(:which).with('cmd').returns 'C:\Windows\System32\cmd' Facter::Util::Resolution.expand_command( 'cmd /c echo foo > C:\bar' ).should == 'C:\Windows\System32\cmd /c echo foo > C:\bar' end it "should expand double quoted binary" do Facter::Util::Resolution.expects(:which).with('my foo').returns 'C:\My Tools\my foo.exe' Facter::Util::Resolution.expand_command('"my foo" /a /b').should == '"C:\My Tools\my foo.exe" /a /b' end it "should not expand single quoted binary" do Facter::Util::Resolution.expects(:which).with('\'C:\My').returns nil Facter::Util::Resolution.expand_command('\'C:\My Tools\foo.exe\' /a /b').should be_nil end it "should quote expanded binary if found in path with spaces" do Facter::Util::Resolution.expects(:which).with('foo').returns 'C:\My Tools\foo.exe' Facter::Util::Resolution.expand_command('foo /a /b').should == '"C:\My Tools\foo.exe" /a /b' end it "should return nil if not found" do Facter::Util::Resolution.expects(:which).with('foo').returns nil Facter::Util::Resolution.expand_command('foo /a | stuff >> /dev/null').should be_nil end end context "on unix", :as_platform => :posix do it "should expand binary" do Facter::Util::Resolution.expects(:which).with('foo').returns '/bin/foo' Facter::Util::Resolution.expand_command('foo -a | stuff >> /dev/null').should == '/bin/foo -a | stuff >> /dev/null' end it "should expand double quoted binary" do Facter::Util::Resolution.expects(:which).with('/tmp/my foo').returns '/tmp/my foo' Facter::Util::Resolution.expand_command(%q{"/tmp/my foo" bar}).should == %q{"/tmp/my foo" bar} end it "should expand single quoted binary" do Facter::Util::Resolution.expects(:which).with('my foo').returns '/home/bob/my path/my foo' Facter::Util::Resolution.expand_command(%q{'my foo' -a}).should == %q{'/home/bob/my path/my foo' -a} end it "should quote expanded binary if found in path with spaces" do Facter::Util::Resolution.expects(:which).with('foo.sh').returns '/home/bob/my tools/foo.sh' Facter::Util::Resolution.expand_command('foo.sh /a /b').should == %q{'/home/bob/my tools/foo.sh' /a /b} end it "should return nil if not found" do Facter::Util::Resolution.expects(:which).with('foo').returns nil Facter::Util::Resolution.expand_command('foo -a | stuff >> /dev/null').should be_nil end end end end # It's not possible, AFAICT, to mock %x{}, so I can't really test this bit. describe "when executing code" do # set up some command strings, making sure we get the right version for both unix and windows echo_command = Facter::Util::Config.is_windows? ? 'cmd.exe /c "echo foo"' : 'echo foo' echo_env_var_command = Facter::Util::Config.is_windows? ? 'cmd.exe /c "echo %%%s%%"' : 'echo $%s' it "should deprecate the interpreter parameter" do Facter.expects(:warnonce).with("The interpreter parameter to 'exec' is deprecated and will be removed in a future version.") Facter::Util::Resolution.exec("/something", "/bin/perl") end # execute a simple echo command it "should execute the binary" do Facter::Util::Resolution.exec(echo_command).should == "foo" end it "should override the LANG environment variable" do Facter::Util::Resolution.exec(echo_env_var_command % 'LANG').should == "C" end it "should respect other overridden environment variables" do Facter::Util::Resolution.with_env( {"FOO" => "foo"} ) do Facter::Util::Resolution.exec(echo_env_var_command % 'FOO').should == "foo" end end it "should restore overridden LANG environment variable after execution" do # we're going to call with_env in a nested fashion, to make sure that the environment gets restored properly # at each level Facter::Util::Resolution.with_env( {"LANG" => "foo"} ) do # Resolution.exec always overrides 'LANG' for its own execution scope Facter::Util::Resolution.exec(echo_env_var_command % 'LANG').should == "C" # But after 'exec' completes, we should see our value restored ENV['LANG'].should == "foo" # Now we'll do a nested call to with_env Facter::Util::Resolution.with_env( {"LANG" => "bar"} ) do # During 'exec' it should still be 'C' Facter::Util::Resolution.exec(echo_env_var_command % 'LANG').should == "C" # After exec it should be restored to our current value for this level of the nesting... ENV['LANG'].should == "bar" end # Now we've dropped out of one level of nesting, ENV['LANG'].should == "foo" # Call exec one more time just for kicks Facter::Util::Resolution.exec(echo_env_var_command % 'LANG').should == "C" # One last check at our current nesting level. ENV['LANG'].should == "foo" end end context "when run on unix", :as_platform => :posix do context "binary is present" do it "should run the command if path to binary is absolute" do Facter::Util::Resolution.expects(:expand_command).with('/usr/bin/uname -m').returns('/usr/bin/uname -m') Facter::Util::Resolution.expects(:`).with('/usr/bin/uname -m').returns 'x86_64' Facter::Util::Resolution.exec('/usr/bin/uname -m').should == 'x86_64' end it "should run the expanded command if path to binary not absolute" do Facter::Util::Resolution.expects(:expand_command).with('uname -m').returns('/usr/bin/uname -m') Facter::Util::Resolution.expects(:`).with('/usr/bin/uname -m').returns 'x86_64' Facter::Util::Resolution.exec('uname -m').should == 'x86_64' end end context "binary is not present" do it "should not run the command if path to binary is absolute" do Facter::Util::Resolution.expects(:expand_command).with('/usr/bin/uname -m').returns nil Facter::Util::Resolution.expects(:`).with('/usr/bin/uname -m').never Facter::Util::Resolution.exec('/usr/bin/uname -m').should be_nil end it "should not run the command if path to binary is not absolute" do Facter::Util::Resolution.expects(:expand_command).with('uname -m').returns nil Facter::Util::Resolution.expects(:`).with('uname -m').never Facter::Util::Resolution.exec('uname -m').should be_nil end end end context "when run on windows", :as_platform => :windows do context "binary is present" do it "should run the command if path to binary is absolute" do Facter::Util::Resolution.expects(:expand_command).with(%q{C:\Windows\foo.exe /a /b}).returns(%q{C:\Windows\foo.exe /a /b}) Facter::Util::Resolution.expects(:`).with(%q{C:\Windows\foo.exe /a /b}).returns 'bar' Facter::Util::Resolution.exec(%q{C:\Windows\foo.exe /a /b}).should == 'bar' end it "should run the expanded command if path to binary not absolute" do Facter::Util::Resolution.expects(:expand_command).with(%q{foo.exe /a /b}).returns(%q{C:\Windows\foo.exe /a /b}) Facter::Util::Resolution.expects(:`).with(%q{C:\Windows\foo.exe /a /b}).returns 'bar' Facter::Util::Resolution.exec(%q{foo.exe /a /b}).should == 'bar' end end context "binary is not present" do it "should not run the command if path to binary is absolute" do Facter::Util::Resolution.expects(:expand_command).with(%q{C:\Windows\foo.exe /a /b}).returns nil Facter::Util::Resolution.expects(:`).with(%q{C:\Windows\foo.exe /a /b}).never Facter::Util::Resolution.exec(%q{C:\Windows\foo.exe /a /b}).should be_nil end it "should try to run the command and return output of a shell-builtin" do Facter::Util::Resolution.expects(:expand_command).with(%q{echo foo}).returns nil Facter::Util::Resolution.expects(:`).with(%q{echo foo}).returns 'foo' Facter.expects(:warnonce).with 'Using Facter::Util::Resolution.exec with a shell built-in is deprecated. Most built-ins can be replaced with native ruby commands. If you really have to run a built-in, pass "cmd /c your_builtin" as a command (command responsible for this message was "echo foo")' Facter::Util::Resolution.exec(%q{echo foo}).should == 'foo' end it "should try to run the command and return nil if not shell-builtin" do Facter::Util::Resolution.expects(:expand_command).with(%q{echo foo}).returns nil Facter::Util::Resolution.stubs(:`).with(%q{echo foo}).raises Errno::ENOENT, 'some_error_message' Facter.expects(:warnonce).never Facter::Util::Resolution.exec(%q{echo foo}).should be_nil end end end end end