diff --git a/spec/shared_behaviours/path_parameters.rb b/spec/shared_behaviours/path_parameters.rb index bdcd4cf25..2d195a18e 100755 --- a/spec/shared_behaviours/path_parameters.rb +++ b/spec/shared_behaviours/path_parameters.rb @@ -1,187 +1,160 @@ # In order to use this correctly you must define a method to get an instance # of the type being tested, so that this code can remain generic: # # it_should_behave_like "all path parameters", :path do # def instance(path) # Puppet::Type.type(:example).new( # :name => 'foo', :require => 'bar', :path_param => path # ) # end # # That method will be invoked for each test to create the instance that we # subsequently test through the system; you should ensure that the minimum of # possible attributes are set to keep the tests clean. # # You must also pass the symbolic name of the parameter being tested to the # block, and optionally can pass a hash of additional options to the block. # # The known options are: # :array :: boolean, does this support arrays of paths, default true. shared_examples_for "all pathname parameters with arrays" do |win32| path_types = { "unix absolute" => %q{/foo/bar}, "unix relative" => %q{foo/bar}, "win32 non-drive absolute" => %q{\foo\bar}, "win32 non-drive relative" => %q{foo\bar}, "win32 drive absolute" => %q{c:\foo\bar}, "win32 drive relative" => %q{c:foo\bar} } describe "when given an array of paths" do (1..path_types.length).each do |n| path_types.keys.combination(n) do |set| data = path_types.collect { |k, v| set.member?(k) ? v : nil } .compact has_relative = set.find { |k| k =~ /relative/ or k =~ /non-drive/ } has_windows = set.find { |k| k =~ /win32/ } has_unix = set.find { |k| k =~ /unix/ } if has_relative or (has_windows and !win32) or (has_unix and win32) reject = true else reject = false end it "should #{reject ? 'reject' : 'accept'} #{set.join(", ")}" do if reject then expect { instance(data) }. should raise_error Puppet::Error, /fully qualified/ else instance = instance(data) instance[@param].should == data end end it "should #{reject ? 'reject' : 'accept'} #{set.join(", ")} doubled" do if reject then expect { instance(data + data) }. should raise_error Puppet::Error, /fully qualified/ else instance = instance(data + data) instance[@param].should == (data + data) end end end end end end shared_examples_for "all path parameters" do |param, options| # Extract and process options to the block. options ||= {} array = options[:array].nil? ? true : options.delete(:array) if options.keys.length > 0 then fail "unknown options for 'all path parameters': " + options.keys.sort.join(', ') end def instance(path) fail "we didn't implement the 'instance(path)' method in the it_should_behave_like block" end ######################################################################## # The actual testing code... before :all do @param = param end - before :each do - @file_separator = File::SEPARATOR - end - after :each do - with_verbose_disabled do - verbose, $VERBOSE = $VERBOSE, nil - File::SEPARATOR = @file_separator - $VERBOSE = verbose - end - end - - describe "on a Unix-like platform it" do - before :each do - with_verbose_disabled do - File::SEPARATOR = '/' - end - Puppet.features.stubs(:microsoft_windows?).returns(false) - Puppet.features.stubs(:posix?).returns(true) - end - + describe "on a Unix-like platform it", :as_platform => :posix do if array then it_should_behave_like "all pathname parameters with arrays", false end it "should accept a fully qualified path" do path = File.join('', 'foo') instance = instance(path) instance[@param].should == path end it "should give a useful error when the path is not absolute" do path = 'foo' expect { instance(path) }. should raise_error Puppet::Error, /fully qualified/ end { "Unix" => '/', "Win32" => '\\' }.each do |style, slash| %w{q Q a A z Z c C}.sort.each do |drive| it "should reject drive letter '#{drive}' with #{style} path separators" do path = "#{drive}:#{slash}Program Files" expect { instance(path) }. should raise_error Puppet::Error, /fully qualified/ end end end end - describe "on a Windows-like platform it" do - before :each do - with_verbose_disabled do - File::SEPARATOR = '\\' - end - Puppet.features.stubs(:microsoft_windows?).returns(true) - Puppet.features.stubs(:posix?).returns(false) - end - + describe "on a Windows-like platform it", :as_platform => :windows do if array then it_should_behave_like "all pathname parameters with arrays", true end it "should reject a fully qualified unix path" do path = '/foo' expect { instance(path) }.to raise_error(Puppet::Error, /fully qualified/) end it "should give a useful error when the path is not absolute" do path = 'foo' expect { instance(path) }. should raise_error Puppet::Error, /fully qualified/ end it "also accepts Unix style path separators" do path = 'C:/Program Files' instance = instance(path) instance[@param].should == path end { "Unix" => '/', "Win32" => '\\' }.each do |style, slash| %w{q Q a A z Z c C}.sort.each do |drive| it "should accept drive letter '#{drive}' with #{style} path separators " do path = "#{drive}:#{slash}Program Files" instance = instance(path) instance[@param].should == path end end end { "UNC paths" => %q{\\\\foo\bar}, "unparsed local paths" => %q{\\\\?\c:\foo}, "unparsed UNC paths" => %q{\\\\?\foo\bar} }.each do |name, path| it "should accept #{name} as absolute" do instance = instance(path) instance[@param].should == path end end end end diff --git a/spec/shared_contexts/platform.rb b/spec/shared_contexts/platform.rb new file mode 100644 index 000000000..7548a6d78 --- /dev/null +++ b/spec/shared_contexts/platform.rb @@ -0,0 +1,43 @@ +# Contexts for stubbing platforms +# In a describe or context block, adding :as_platform => :windows or +# :as_platform => :posix will stub the relevant Puppet features, as well as +# the behavior of Ruby's filesystem methods by changing File::ALT_SEPARATOR. + +shared_context "windows", :as_platform => :windows do + before :each do + Facter.stubs(:value).with(:operatingsystem).returns 'Windows' + Puppet.features.stubs(:microsoft_windows?).returns(true) + Puppet.features.stubs(:posix?).returns(false) + end + + around do |example| + file_alt_separator = File::ALT_SEPARATOR + # prevent Ruby from warning about changing a constant + with_verbose_disabled do + File::ALT_SEPARATOR = '\\' + end + example.run + with_verbose_disabled do + File::ALT_SEPARATOR = file_alt_separator + end + end +end + +shared_context "posix", :as_platform => :posix do + before :each do + Puppet.features.stubs(:microsoft_windows?).returns(false) + Puppet.features.stubs(:posix?).returns(true) + end + + around do |example| + file_alt_separator = File::ALT_SEPARATOR + # prevent Ruby from warning about changing a constant + with_verbose_disabled do + File::ALT_SEPARATOR = nil + end + example.run + with_verbose_disabled do + File::ALT_SEPARATOR = file_alt_separator + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a21bb3a99..f19ee9ea8 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,125 +1,129 @@ dir = File.expand_path(File.dirname(__FILE__)) $LOAD_PATH.unshift File.join(dir, 'lib') # Don't want puppet getting the command line arguments for rake or autotest ARGV.clear require 'puppet' require 'mocha' gem 'rspec', '>=2.0.0' require 'rspec/expectations' # So everyone else doesn't have to include this base constant. module PuppetSpec FIXTURE_DIR = File.join(dir = File.expand_path(File.dirname(__FILE__)), "fixtures") unless defined?(FIXTURE_DIR) end require 'pathname' require 'tmpdir' require 'puppet_spec/verbose' require 'puppet_spec/files' require 'puppet_spec/fixtures' require 'puppet_spec/matchers' require 'puppet_spec/database' require 'monkey_patches/alias_should_to_must' require 'monkey_patches/publicize_methods' +Pathname.glob("#{dir}/shared_contexts/*.rb") do |file| + require file.relative_path_from(Pathname.new(dir)) +end + Pathname.glob("#{dir}/shared_behaviours/**/*.rb") do |behaviour| require behaviour.relative_path_from(Pathname.new(dir)) end RSpec.configure do |config| include PuppetSpec::Fixtures config.mock_with :mocha config.before :each do # Disabling garbage collection inside each test, and only running it at # the end of each block, gives us an ~ 15 percent speedup, and more on # some platforms *cough* windows *cough* that are a little slower. GC.disable # We need to preserve the current state of all our indirection cache and # terminus classes. This is pretty important, because changes to these # are global and lead to order dependencies in our testing. # # We go direct to the implementation because there is no safe, sane public # API to manage restoration of these to their default values. This # should, once the value is proved, be moved to a standard API on the # indirector. # # To make things worse, a number of the tests stub parts of the # indirector. These stubs have very specific expectations that what # little of the public API we could use is, well, likely to explode # randomly in some tests. So, direct access. --daniel 2011-08-30 $saved_indirection_state = {} indirections = Puppet::Indirector::Indirection.send(:class_variable_get, :@@indirections) indirections.each do |indirector| $saved_indirection_state[indirector.name] = { :@terminus_class => indirector.instance_variable_get(:@terminus_class), :@cache_class => indirector.instance_variable_get(:@cache_class) } end # REVISIT: I think this conceals other bad tests, but I don't have time to # fully diagnose those right now. When you read this, please come tell me # I suck for letting this float. --daniel 2011-04-21 Signal.stubs(:trap) Puppet.settings.send(:initialize_everything_for_tests) # Longer keys are secure, but they sure make for some slow testing - both # in terms of generating keys, and in terms of anything the next step down # the line doing validation or whatever. Most tests don't care how long # or secure it is, just that it exists, so these are better and faster # defaults, in testing only. # # I would make these even shorter, but OpenSSL doesn't support anything # below 512 bits. Sad, really, because a 0 bit key would be just fine. Puppet[:req_bits] = 512 Puppet[:keylength] = 512 @logs = [] Puppet::Util::Log.newdestination(Puppet::Test::LogCollector.new(@logs)) @log_level = Puppet::Util::Log.level end config.after :each do Puppet.settings.send(:clear_everything_for_tests) Puppet::Node::Environment.clear Puppet::Util::Storage.clear Puppet::Util::ExecutionStub.reset PuppetSpec::Files.cleanup @logs.clear Puppet::Util::Log.close_all Puppet::Util::Log.level = @log_level # Restore the indirector configuration. See before hook. indirections = Puppet::Indirector::Indirection.send(:class_variable_get, :@@indirections) indirections.each do |indirector| $saved_indirection_state.fetch(indirector.name, {}).each do |variable, value| indirector.instance_variable_set(variable, value) end end $saved_indirection_state = nil # Some tests can cause us to connect, in which case the lingering # connection is a resource that can cause unexpected failure in later # tests, as well as sharing state accidentally. # We're testing if ActiveRecord::Base is defined because some test cases # may stub Puppet.features.rails? which is how we should normally # introspect for this functionality. ActiveRecord::Base.remove_connection if defined?(ActiveRecord::Base) # This will perform a GC between tests, but only if actually required. We # experimented with forcing a GC run, and that was less efficient than # just letting it run all the time. GC.enable end end diff --git a/spec/unit/parser/functions/generate_spec.rb b/spec/unit/parser/functions/generate_spec.rb index e50805393..8b9d85136 100755 --- a/spec/unit/parser/functions/generate_spec.rb +++ b/spec/unit/parser/functions/generate_spec.rb @@ -1,85 +1,77 @@ #!/usr/bin/env rspec require 'spec_helper' describe "the generate function" do before :all do Puppet::Parser::Functions.autoloader.loadall end let(:scope) { Puppet::Parser::Scope.new } it "should exist" do Puppet::Parser::Functions.function("generate").should == "function_generate" end it " accept a fully-qualified path as a command" do command = File.expand_path('/command/foo') Dir.expects(:chdir).with(File.dirname(command)).returns("yay") scope.function_generate([command]).should == "yay" end it "should not accept a relative path as a command" do lambda { scope.function_generate(["command"]) }.should raise_error(Puppet::ParseError) end it "should not accept a command containing illegal characters" do lambda { scope.function_generate([File.expand_path('/##/command')]) }.should raise_error(Puppet::ParseError) end it "should not accept a command containing spaces" do lambda { scope.function_generate([File.expand_path('/com mand')]) }.should raise_error(Puppet::ParseError) end it "should not accept a command containing '..'" do command = File.expand_path("/command/../") lambda { scope.function_generate([command]) }.should raise_error(Puppet::ParseError) end it "should execute the generate script with the correct working directory" do command = File.expand_path("/command") Dir.expects(:chdir).with(File.dirname(command)).returns("yay") scope.function_generate([command]).should == 'yay' end - describe "on Windows" do - before :each do - Puppet.features.stubs(:microsoft_windows?).returns(true) - end - + describe "on Windows", :as_platform => :windows do it "should accept lower-case drive letters" do command = 'd:/command/foo' Dir.expects(:chdir).with(File.dirname(command)).returns("yay") scope.function_generate([command]).should == 'yay' end it "should accept upper-case drive letters" do command = 'D:/command/foo' Dir.expects(:chdir).with(File.dirname(command)).returns("yay") scope.function_generate([command]).should == 'yay' end it "should accept forward and backslashes in the path" do command = 'D:\command/foo\bar' Dir.expects(:chdir).with(File.dirname(command)).returns("yay") scope.function_generate([command]).should == 'yay' end it "should reject colons when not part of the drive letter" do lambda { scope.function_generate(['C:/com:mand']) }.should raise_error(Puppet::ParseError) end it "should reject root drives" do lambda { scope.function_generate(['C:/']) }.should raise_error(Puppet::ParseError) end end - describe "on non-Windows" do - before :each do - Puppet.features.stubs(:microsoft_windows?).returns(false) - end - + describe "on non-Windows", :as_platform => :posix do it "should reject backslashes" do lambda { scope.function_generate(['/com\\mand']) }.should raise_error(Puppet::ParseError) end end end diff --git a/spec/unit/provider/exec/windows_spec.rb b/spec/unit/provider/exec/windows_spec.rb index 748646ac3..5e1b5c912 100755 --- a/spec/unit/provider/exec/windows_spec.rb +++ b/spec/unit/provider/exec/windows_spec.rb @@ -1,113 +1,107 @@ #!/usr/bin/env rspec require 'spec_helper' -describe Puppet::Type.type(:exec).provider(:windows) do +describe Puppet::Type.type(:exec).provider(:windows), :as_platform => :windows do include PuppetSpec::Files let(:resource) { Puppet::Type.type(:exec).new(:title => 'C:\foo', :provider => :windows) } let(:provider) { described_class.new(resource) } - before :each do - Facter.stubs(:value).with(:operatingsystem).returns 'Windows' - Puppet.features.stubs(:microsoft_windows?).returns(true) - Puppet.features.stubs(:posix?).returns(false) - end - after :all do # This provider may not be suitable on some machines, so we want to reset # the default so it isn't used by mistake in future specs. Puppet::Type.type(:exec).defaultprovider = nil end describe "#extractexe" do describe "when the command has no arguments" do it "should return the command if it's quoted" do provider.extractexe('"foo"').should == 'foo' end it "should return the command if it's quoted and contains spaces" do provider.extractexe('"foo bar"').should == 'foo bar' end it "should return the command if it's not quoted" do provider.extractexe('foo').should == 'foo' end end describe "when the command has arguments" do it "should return the command if it's quoted" do provider.extractexe('"foo" bar baz').should == 'foo' end it "should return the command if it's quoted and contains spaces" do provider.extractexe('"foo bar" baz "quux quiz"').should == 'foo bar' end it "should return the command if it's not quoted" do provider.extractexe('foo bar baz').should == 'foo' end end end describe "#checkexe" do describe "when the command is absolute", :if => Puppet.features.microsoft_windows? do it "should return if the command exists and is a file" do command = tmpfile('command') FileUtils.touch(command) provider.checkexe(command).should == nil end it "should fail if the command doesn't exist" do command = tmpfile('command') expect { provider.checkexe(command) }.to raise_error(ArgumentError, "Could not find command '#{command}'") end it "should fail if the command isn't a file" do command = tmpfile('command') FileUtils.mkdir(command) expect { provider.checkexe(command) }.to raise_error(ArgumentError, "'#{command}' is a directory, not a file") end end describe "when the command is relative" do describe "and a path is specified" do before :each do provider.stubs(:which) end it "should search for executables with no extension" do provider.resource[:path] = [File.expand_path('/bogus/bin')] provider.expects(:which).with('foo').returns('foo') provider.checkexe('foo') end it "should fail if the command isn't in the path" do expect { provider.checkexe('foo') }.to raise_error(ArgumentError, "Could not find command 'foo'") end end it "should fail if no path is specified" do expect { provider.checkexe('foo') }.to raise_error(ArgumentError, "Could not find command 'foo'") end end end describe "#validatecmd" do it "should fail if the command isn't absolute and there is no path" do expect { provider.validatecmd('foo') }.to raise_error(Puppet::Error, /'foo' is not qualified and no path was specified/) end it "should not fail if the command is absolute and there is no path" do provider.validatecmd('C:\foo').should == nil end it "should not fail if the command is not absolute and there is a path" do resource[:path] = 'C:\path;C:\another_path' provider.validatecmd('foo').should == nil end end end diff --git a/spec/unit/type/exec_spec.rb b/spec/unit/type/exec_spec.rb index 5a4c392de..3bb607b4b 100755 --- a/spec/unit/type/exec_spec.rb +++ b/spec/unit/type/exec_spec.rb @@ -1,719 +1,712 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Type.type(:exec) do include PuppetSpec::Files def exec_tester(command, exitstatus = 0, rest = {}) Puppet.features.stubs(:root?).returns(true) output = rest.delete(:output) || '' tries = rest[:tries] || 1 args = { :name => command, :path => @example_path, :logoutput => false, :loglevel => :err, :returns => 0 }.merge(rest) exec = Puppet::Type.type(:exec).new(args) status = stub "process", :exitstatus => exitstatus Puppet::Util::SUIDManager.expects(:run_and_capture).times(tries). with(command, nil, nil).returns([output, status]) return exec end before do @command = make_absolute('/bin/true whatever') @executable = make_absolute('/bin/true') @bogus_cmd = make_absolute('/bogus/cmd') end describe "when not stubbing the provider" do before do path = tmpdir('path') true_cmd = File.join(path, 'true') false_cmd = File.join(path, 'false') FileUtils.touch(true_cmd) FileUtils.touch(false_cmd) File.chmod(0755, true_cmd) File.chmod(0755, false_cmd) @example_path = [path] end it "should return :executed_command as its event" do resource = Puppet::Type.type(:exec).new :command => @command resource.parameter(:returns).event.name.should == :executed_command end describe "when execing" do it "should use the 'run_and_capture' method to exec" do exec_tester("true").refresh.should == :executed_command end it "should report a failure" do 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 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 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 output = "output1\noutput2\n" 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 output = "output1\noutput2\n" proc { exec_tester('false', 1, :output => output, :logoutput => true).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 output = "output1\noutput2\n" proc { exec_tester('false', 1, :output => output, :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 "should log the output on failure when returns is specified as an array" do output = "output1\noutput2\n" 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 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 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 tries = 5 resource = exec_tester("false", 1, :tries => tries, :try_sleep => 0) proc { resource.refresh }.should raise_error(Puppet::Error) end end end it "should be able to autorequire files mentioned in the command" do foo = make_absolute('/bin/foo') catalog = Puppet::Resource::Catalog.new tmp = Puppet::Type.type(:file).new(:name => foo) catalog.add_resource tmp execer = Puppet::Type.type(:exec).new(:name => 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 path-separator delimited list" => expect.join(File::PATH_SEPARATOR), "both array and path-separator delimited lists" => ["one", "two#{File::PATH_SEPARATOR}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 describe "on platforms where path separator is not :" do before :each do @old_verbosity = $VERBOSE $VERBOSE = nil @old_separator = File::PATH_SEPARATOR File::PATH_SEPARATOR = 'q' end after :each do File::PATH_SEPARATOR = @old_separator $VERBOSE = @old_verbosity end it "should use the path separator of the current platform" do type = Puppet::Type.type(:exec).new(:name => @command, :path => "fooqbarqbaz") type[:path].should == %w[foo bar baz] end end end describe "when setting user" do - describe "on POSIX systems" do - before :each do - Puppet.features.stubs(:posix?).returns(true) - Puppet.features.stubs(:microsoft_windows?).returns(false) - end - + describe "on POSIX systems", :as_platform => :posix do it "should fail if we are not root" do Puppet.features.stubs(:root?).returns(false) expect { Puppet::Type.type(:exec).new(:name => '/bin/true whatever', :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 => '/bin/true whatever', :user => value) type[:user].should == value end end end - describe "on Windows systems" do + describe "on Windows systems", :as_platform => :windows do before :each do - Puppet.features.stubs(:posix?).returns(false) - Puppet.features.stubs(:microsoft_windows?).returns(true) Puppet.features.stubs(:root?).returns(true) end it "should reject user parameter" do expect { Puppet::Type.type(:exec).new(:name => 'c:\windows\notepad.exe', :user => 'input') }. should raise_error Puppet::Error, /Unable to execute commands as other users on Windows/ 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) # Specify shell provider so we don't have to care about command validation Puppet::Type.type(:exec).new(:name => @executable, :cwd => path, :provider => :shell) 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 => @executable) 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 => @executable) 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 => @executable) 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 [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', '', 'foo', '5foo'].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 Puppet::Util.stubs(:execute).with do |cmd,args| sleep 1 true end FileTest.stubs(:file?).returns(false) FileTest.stubs(:file?).with(File.expand_path('/bin/sleep')).returns(true) FileTest.stubs(:executable?).returns(false) FileTest.stubs(:executable?).with(File.expand_path('/bin/sleep')).returns(true) sleep_exec = Puppet::Type.type(:exec).new(:name => 'sleep 1', :path => [File.expand_path('/bin')], :timeout => '0.2') lambda { sleep_exec.refresh }.should raise_error Puppet::Error, "Command exceeded timeout" end it "should convert timeout to a float" do command = make_absolute('/bin/false') resource = Puppet::Type.type(:exec).new :command => command, :timeout => "12" resource[:timeout].should be_a(Float) resource[:timeout].should == 12.0 end it "should munge negative timeouts to 0.0" do command = make_absolute('/bin/false') resource = Puppet::Type.type(:exec).new :command => command, :timeout => "-12.0" resource.parameter(:timeout).value.should be_a(Float) resource.parameter(:timeout).value.should == 0.0 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 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 + end - describe "when setting creates" do - it_should_behave_like "all path parameters", :creates, :array => true do - def instance(path) - # Specify shell provider so we don't have to care about command validation - Puppet::Type.type(:exec).new(:name => @executable, :creates => path, :provider => :shell) - end + describe "when setting creates" do + it_should_behave_like "all path parameters", :creates, :array => true do + def instance(path) + # Specify shell provider so we don't have to care about command validation + Puppet::Type.type(:exec).new(:name => @executable, :creates => path, :provider => :shell) 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 => @executable) 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 :each do @exist = tmpfile('exist') FileUtils.touch(@exist) @unexist = tmpfile('unexist') 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 = make_absolute("/magic/pass") @fail = make_absolute("/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 it "should emit output to debug" do Puppet::Util::Log.level = :debug @test[param] = @fail @test.check_all_attributes.should == true @logs.shift.message.should == "test output" 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 myother_bogus_cmd = make_absolute('/myother/bogus/cmd') 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 diff --git a/spec/unit/util/log/destinations_spec.rb b/spec/unit/util/log/destinations_spec.rb index 07052b361..1d3ab79bc 100755 --- a/spec/unit/util/log/destinations_spec.rb +++ b/spec/unit/util/log/destinations_spec.rb @@ -1,162 +1,158 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/util/log' describe Puppet::Util::Log.desttypes[:report] do before do @dest = Puppet::Util::Log.desttypes[:report] end it "should require a report at initialization" do @dest.new("foo").report.should == "foo" end it "should send new messages to the report" do report = mock 'report' dest = @dest.new(report) report.expects(:<<).with("my log") dest.handle "my log" end end describe Puppet::Util::Log.desttypes[:file] do before do File.stubs(:open) # prevent actually creating the file @class = Puppet::Util::Log.desttypes[:file] end it "should default to autoflush false" do @class.new('/tmp/log').autoflush.should == false end describe "when matching" do shared_examples_for "file destination" do it "should match an absolute path" do @class.match?(abspath).should be_true end it "should not match a relative path" do @class.match?(relpath).should be_false end end - describe "on POSIX systems" do - before :each do Puppet.features.stubs(:microsoft_windows?).returns false end - + describe "on POSIX systems", :as_platform => :posix do let (:abspath) { '/tmp/log' } let (:relpath) { 'log' } it_behaves_like "file destination" end - describe "on Windows systems" do - before :each do Puppet.features.stubs(:microsoft_windows?).returns true end - + describe "on Windows systems", :as_platform => :windows do let (:abspath) { 'C:\\temp\\log.txt' } let (:relpath) { 'log.txt' } it_behaves_like "file destination" end end end describe Puppet::Util::Log.desttypes[:syslog] do let (:klass) { Puppet::Util::Log.desttypes[:syslog] } # these tests can only be run when syslog is present, because # we can't stub the top-level Syslog module describe "when syslog is available", :if => Puppet.features.syslog? do before :each do Syslog.stubs(:opened?).returns(false) Syslog.stubs(:const_get).returns("LOG_KERN").returns(0) Syslog.stubs(:open) end it "should open syslog" do Syslog.expects(:open) klass.new end it "should close syslog" do Syslog.expects(:close) dest = klass.new dest.close end it "should send messages to syslog" do syslog = mock 'syslog' syslog.expects(:info).with("don't panic") Syslog.stubs(:open).returns(syslog) msg = Puppet::Util::Log.new(:level => :info, :message => "don't panic") dest = klass.new dest.handle(msg) end end describe "when syslog is unavailable" do it "should not be a suitable log destination" do Puppet.features.stubs(:syslog?).returns(false) klass.suitable?(:syslog).should be_false end end end describe Puppet::Util::Log.desttypes[:console] do describe "when color is available" do it "should support color output" do Puppet.stubs(:[]).with(:color).returns(true) subject.colorize(:red, 'version').should == "\e[0;31mversion\e[0m" end it "should withhold color output when not appropriate" do Puppet.stubs(:[]).with(:color).returns(false) subject.colorize(:red, 'version').should == "version" end it "should handle multiple overlapping colors in a stack-like way" do Puppet.stubs(:[]).with(:color).returns(true) vstring = subject.colorize(:red, 'version') subject.colorize(:green, "(#{vstring})").should == "\e[0;32m(\e[0;31mversion\e[0;32m)\e[0m" end it "should handle resets in a stack-like way" do Puppet.stubs(:[]).with(:color).returns(true) vstring = subject.colorize(:reset, 'version') subject.colorize(:green, "(#{vstring})").should == "\e[0;32m(\e[mversion\e[0;32m)\e[0m" end end end describe Puppet::Util::Log.desttypes[:telly_prototype_console] do describe "when color is available" do it "should support color output" do Puppet.stubs(:[]).with(:color).returns(true) subject.colorize(:red, 'version').should == "\e[0;31mversion\e[0m" end it "should withhold color output when not appropriate" do Puppet.stubs(:[]).with(:color).returns(false) subject.colorize(:red, 'version').should == "version" end it "should handle multiple overlapping colors in a stack-like way" do Puppet.stubs(:[]).with(:color).returns(true) vstring = subject.colorize(:red, 'version') subject.colorize(:green, "(#{vstring})").should == "\e[0;32m(\e[0;31mversion\e[0;32m)\e[0m" end it "should handle resets in a stack-like way" do Puppet.stubs(:[]).with(:color).returns(true) vstring = subject.colorize(:reset, 'version') subject.colorize(:green, "(#{vstring})").should == "\e[0;32m(\e[mversion\e[0;32m)\e[0m" end end end diff --git a/spec/unit/util_spec.rb b/spec/unit/util_spec.rb index 37542fe9e..3b7af4e7e 100755 --- a/spec/unit/util_spec.rb +++ b/spec/unit/util_spec.rb @@ -1,747 +1,747 @@ #!/usr/bin/env ruby require 'spec_helper' describe Puppet::Util do include PuppetSpec::Files if Puppet.features.microsoft_windows? def set_mode(mode, file) Puppet::Util::Windows::Security.set_mode(mode, file) end def get_mode(file) Puppet::Util::Windows::Security.get_mode(file) & 07777 end else def set_mode(mode, file) File.chmod(mode, file) end def get_mode(file) File.lstat(file).mode & 07777 end end 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) + describe "on posix systems", :as_platform => :posix do + it "should default to the platform of the local system" do + Puppet::Util.should be_absolute_path('/foo') + Puppet::Util.should_not be_absolute_path('C:/foo') + end + end - Puppet::Util.should be_absolute_path('C:/foo') - Puppet::Util.should_not be_absolute_path('/foo') + describe "on windows", :as_platform => :windows do + it "should default to the platform of the local system" do + Puppet::Util.should be_absolute_path('C:/foo') + Puppet::Util.should_not be_absolute_path('/foo') + end 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(:process_handle) { 0xDEADBEEF } let(:thread_handle) { 0xCAFEBEEF } let(:proc_info_stub) { stub 'processinfo', :process_handle => process_handle, :thread_handle => thread_handle, :process_id => pid} let(:null_file) { Puppet.features.microsoft_windows? ? 'NUL' : '/dev/null' } def stub_process_wait(exitstatus) if Puppet.features.microsoft_windows? Puppet::Util::Windows::Process.stubs(:wait_process).with(process_handle).returns(exitstatus) Process.stubs(:CloseHandle).with(process_handle) Process.stubs(:CloseHandle).with(thread_handle) else Process.stubs(:waitpid2).with(pid).returns([pid, stub('child_status', :exitstatus => exitstatus)]) end end describe "#execute_posix" 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", :if => Puppet.features.microsoft_windows? do before :each do Process.stubs(:create).returns(proc_info_stub) stub_process_wait(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}, :close_handles => false ).returns(proc_info_stub) Puppet::Util.execute_windows('test command', {}, @stdin, @stdout, @stderr) end it "should return the process info of the child process" do Puppet::Util.execute_windows('test command', {}, @stdin, @stdout, @stderr).should == proc_info_stub 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" do before :each do stub_process_wait(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' } let(:rval) { Puppet.features.microsoft_windows? ? proc_info_stub : pid } 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(rval) 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(rval) 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(rval) 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(rval) 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(rval) 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(rval) Puppet::Util.execute('test command', :squelch => false, :combine => false) end end end describe "on Windows", :if => Puppet.features.microsoft_windows? do it "should always close the process and thread handles" do Puppet::Util.stubs(:execute_windows).returns(proc_info_stub) Puppet::Util::Windows::Process.expects(:wait_process).with(process_handle).raises('whatever') Process.expects(:CloseHandle).with(thread_handle) Process.expects(:CloseHandle).with(process_handle) expect { Puppet::Util.execute('test command') }.should raise_error(RuntimeError) end end end describe "after execution" do before :each do stub_process_wait(0) if Puppet.features.microsoft_windows? Puppet::Util.stubs(:execute_windows).returns(proc_info_stub) else Puppet::Util.stubs(:execute_posix).returns(pid) end end it "should wait for the child process to exit" do Puppet::Util.stubs(:wait_for_output) 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) 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 stub_process_wait(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 stub_process_wait(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 expect { Puppet::Util.execute('fail command', :failonfail => true) }.not_to raise_error end end end describe "#execpipe" do let :instance do instance = Class.new.new instance.send(:extend, Puppet::Util) instance end it "should execute a string as a string" do instance.expects(:open).with('| echo hello 2>&1').returns('hello') $CHILD_STATUS.expects(:==).with(0).returns(true) instance.execpipe('echo hello').should == 'hello' end it "should execute an array by pasting together with spaces" do instance.expects(:open).with('| echo hello 2>&1').returns('hello') $CHILD_STATUS.expects(:==).with(0).returns(true) instance.execpipe(['echo', 'hello']).should == 'hello' end it "should fail if asked to fail, and the child does" do instance.stubs(:open).returns('error message') $CHILD_STATUS.expects(:==).with(0).returns(false) expect { instance.execpipe('echo hello') }. to raise_error Puppet::ExecutionFailure, /error message/ end it "should not fail if asked not to fail, and the child does" do instance.stubs(:open).returns('error message') $CHILD_STATUS.stubs(:==).with(0).returns(false) expect do instance.execpipe('echo hello', false).should == 'error message' end.not_to raise_error 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 describe "#binread" do let(:contents) { "foo\r\nbar" } it "should preserve line endings" do path = tmpfile('util_binread') File.open(path, 'wb') { |f| f.print contents } Puppet::Util.binread(path).should == contents end it "should raise an error if the file doesn't exist" do expect { Puppet::Util.binread('/path/does/not/exist') }.to raise_error(Errno::ENOENT) end end context "#replace_file" do describe "on POSIX platforms", :if => Puppet.features.posix? do subject { Puppet::Util } it { should respond_to :replace_file } let :target do target = Tempfile.new("puppet-util-replace-file") target.puts("hello, world") target.flush # make sure content is on disk. target.fsync rescue nil target.close target end it "should fail if no block is given" do expect { subject.replace_file(target.path, 0600) }.to raise_error /block/ end it "should replace a file when invoked" do # Check that our file has the expected content. File.read(target.path).should == "hello, world\n" # Replace the file. subject.replace_file(target.path, 0600) do |fh| fh.puts "I am the passenger..." end # ...and check the replacement was complete. File.read(target.path).should == "I am the passenger...\n" end [0555, 0600, 0660, 0700, 0770].each do |mode| it "should copy 0#{mode.to_s(8)} permissions from the target file by default" do set_mode(mode, target.path) get_mode(target.path).should == mode subject.replace_file(target.path, 0000) {|fh| fh.puts "bazam" } get_mode(target.path).should == mode File.read(target.path).should == "bazam\n" end end it "should copy the permissions of the source file before yielding" do set_mode(0555, target.path) inode = File.stat(target.path).ino unless Puppet.features.microsoft_windows? yielded = false subject.replace_file(target.path, 0600) do |fh| get_mode(fh.path).should == 0555 yielded = true end yielded.should be_true # We can't check inode on Windows File.stat(target.path).ino.should_not == inode unless Puppet.features.microsoft_windows? get_mode(target.path).should == 0555 end it "should use the default permissions if the source file doesn't exist" do new_target = target.path + '.foo' File.should_not be_exist(new_target) begin subject.replace_file(new_target, 0555) {|fh| fh.puts "foo" } get_mode(new_target).should == 0555 ensure File.unlink(new_target) if File.exists?(new_target) end end it "should not replace the file if an exception is thrown in the block" do yielded = false threw = false begin subject.replace_file(target.path, 0600) do |fh| yielded = true fh.puts "different content written, then..." raise "...throw some random failure" end rescue Exception => e if e.to_s =~ /some random failure/ threw = true else raise end end yielded.should be_true threw.should be_true # ...and check the replacement was complete. File.read(target.path).should == "hello, world\n" end end describe "on Windows platforms" do it "should fail and complain" do Puppet.features.stubs(:microsoft_windows?).returns true expect { Puppet::Util.replace_file("C:/foo", 0644) {} }.to raise_error(Puppet::DevError, "replace_file is non-functional on Windows") end end end end