diff options
Diffstat (limited to 'spec/support')
-rw-r--r-- | spec/support/chef_helpers.rb | 52 | ||||
-rw-r--r-- | spec/support/lib/chef/provider/easy.rb | 35 | ||||
-rw-r--r-- | spec/support/lib/chef/provider/snakeoil.rb | 40 | ||||
-rw-r--r-- | spec/support/lib/chef/resource/cat.rb | 41 | ||||
-rw-r--r-- | spec/support/lib/chef/resource/one_two_three_four.rb | 43 | ||||
-rw-r--r-- | spec/support/lib/chef/resource/zen_master.rb | 46 | ||||
-rw-r--r-- | spec/support/matchers/leak.rb | 96 | ||||
-rw-r--r-- | spec/support/mock/constant.rb | 52 | ||||
-rw-r--r-- | spec/support/mock/platform.rb | 18 | ||||
-rw-r--r-- | spec/support/platform_helpers.rb | 31 | ||||
-rw-r--r-- | spec/support/platforms/prof/gc.rb | 54 | ||||
-rw-r--r-- | spec/support/platforms/prof/win32.rb | 46 | ||||
-rw-r--r-- | spec/support/shared/functional/directory_resource.rb | 85 | ||||
-rw-r--r-- | spec/support/shared/functional/file_resource.rb | 173 | ||||
-rw-r--r-- | spec/support/shared/functional/knife.rb | 37 | ||||
-rw-r--r-- | spec/support/shared/functional/securable_resource.rb | 394 | ||||
-rw-r--r-- | spec/support/shared/unit/api_error_inspector.rb | 192 | ||||
-rw-r--r-- | spec/support/shared/unit/file_system_support.rb | 110 |
18 files changed, 1545 insertions, 0 deletions
diff --git a/spec/support/chef_helpers.rb b/spec/support/chef_helpers.rb new file mode 100644 index 0000000000..77f5fc7669 --- /dev/null +++ b/spec/support/chef_helpers.rb @@ -0,0 +1,52 @@ +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +CHEF_SPEC_DATA = File.expand_path(File.dirname(__FILE__) + "/../data/") +CHEF_SPEC_BACKUP_PATH = File.join(Dir.tmpdir, 'test-backup-path') + +Chef::Config[:log_level] = :fatal +Chef::Config[:cache_type] = "Memory" +Chef::Config[:cache_options] = { } +Chef::Config[:persistent_queue] = false +Chef::Config[:file_backup_path] = CHEF_SPEC_BACKUP_PATH + +Chef::Log.level(Chef::Config.log_level) +Chef::Config.solo(false) + +Chef::Log.logger = Logger.new(StringIO.new) + +def sha256_checksum(path) + Digest::SHA256.hexdigest(File.read(path)) +end + +# From Ruby 1.9.2+ +# Here for backwards compatibility with Ruby 1.8.7 +# http://rubydoc.info/stdlib/tmpdir/1.9.2/Dir/Tmpname +def make_tmpname(prefix_suffix, n) + case prefix_suffix + when String + prefix = prefix_suffix + suffix = "" + when Array + prefix = prefix_suffix[0] + suffix = prefix_suffix[1] + else + raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}" + end + t = Time.now.strftime("%Y%m%d") + path = "#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}" + path << "-#{n}" if n + path << suffix +end diff --git a/spec/support/lib/chef/provider/easy.rb b/spec/support/lib/chef/provider/easy.rb new file mode 100644 index 0000000000..054b45256c --- /dev/null +++ b/spec/support/lib/chef/provider/easy.rb @@ -0,0 +1,35 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Provider + class Easy < Chef::Provider + def load_current_resource + true + end + + def action_sell + true + end + + def action_buy + true + end + end + end +end diff --git a/spec/support/lib/chef/provider/snakeoil.rb b/spec/support/lib/chef/provider/snakeoil.rb new file mode 100644 index 0000000000..c5d8d52a82 --- /dev/null +++ b/spec/support/lib/chef/provider/snakeoil.rb @@ -0,0 +1,40 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Provider + class SnakeOil < Chef::Provider + def load_current_resource + true + end + + def action_purr + @new_resource.updated_by_last_action(true) + true + end + + def action_sell + true + end + + def action_buy + true + end + end + end +end diff --git a/spec/support/lib/chef/resource/cat.rb b/spec/support/lib/chef/resource/cat.rb new file mode 100644 index 0000000000..5809ad3275 --- /dev/null +++ b/spec/support/lib/chef/resource/cat.rb @@ -0,0 +1,41 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Resource + class Cat < Chef::Resource + + attr_accessor :action + + def initialize(name, run_context=nil) + @resource_name = :cat + super + @action = "sell" + end + + def pretty_kitty(arg=nil) + set_if_args(@pretty_kitty, arg) do + case arg + when true, false + @pretty_kitty = arg + end + end + end + end + end +end diff --git a/spec/support/lib/chef/resource/one_two_three_four.rb b/spec/support/lib/chef/resource/one_two_three_four.rb new file mode 100644 index 0000000000..4f4b063eb6 --- /dev/null +++ b/spec/support/lib/chef/resource/one_two_three_four.rb @@ -0,0 +1,43 @@ +# +# Author:: John Hampton (<john@cleanoffer.com>) +# Copyright:: Copyright (c) 2009 CleanOffer, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +class Chef + class Resource + class OneTwoThreeFour < Chef::Resource + attr_reader :i_can_count + + def initialize(name, run_context) + @resource_name = :one_two_three_four + super + end + + def i_can_count(tf) + @i_can_count = tf + end + + def something(arg=nil) + set_if_args(@something, arg) do + case arg + when true, false + @something = arg + end + end + end + end + end +end diff --git a/spec/support/lib/chef/resource/zen_master.rb b/spec/support/lib/chef/resource/zen_master.rb new file mode 100644 index 0000000000..71842b74c0 --- /dev/null +++ b/spec/support/lib/chef/resource/zen_master.rb @@ -0,0 +1,46 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008, 2010 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/knife' +require 'chef/json_compat' + +class Chef + class Resource + class ZenMaster < Chef::Resource + attr_reader :peace + + def initialize(name, run_context=nil) + @resource_name = :zen_master + super + end + + def peace(tf) + @peace = tf + end + + def something(arg=nil) + set_if_args(@something, arg) do + case arg + when true, false + @something = arg + end + end + end + end + end +end diff --git a/spec/support/matchers/leak.rb b/spec/support/matchers/leak.rb new file mode 100644 index 0000000000..eb80fcd492 --- /dev/null +++ b/spec/support/matchers/leak.rb @@ -0,0 +1,96 @@ +# +# Author:: Seth Chisamore (<schisamo@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +module Matchers + module LeakBase + include RSpec::Matchers + + def initialize(opts={}, &block) + @warmup = opts[:warmup] || 5 + @iterations = opts[:iterations] || 100 + @variance = opts[:variance] || 1000 + end + + def failure_message_for_should + "expected final measure [#{@final_measure}] to be greater than or within +/- #{@variance} delta of initial measure [#{@initial_measure}]" + end + + def failure_message_for_should_not + "expected final measure [#{@final_measure}] to be less than or within +/- #{@variance} delta of initial measure [#{@initial_measure}]" + end + + private + def match(measure, given_proc) + profiler.start + + @initial_measure = 0 + @final_measure = 0 + + @warmup.times do + given_proc.call + end + + @initial_measure = profiler.send(measure) + + @iterations.times do + given_proc.call + end + + profiler.stop + + @final_measure = profiler.send(measure) + @final_measure > (@initial_measure + @variance) + end + + def profiler + @profiler ||= begin + if Chef::Platform.windows? + require File.join(File.dirname(__FILE__), '..', 'prof', 'win32') + RSpec::Prof::Win32::Profiler.new + else + require File.join(File.dirname(__FILE__), '..', 'prof', 'gc') + RSpec::Prof::GC::Profiler.new + end + end + end + + end + + class LeakMemory + include LeakBase + + def matches?(given_proc) + match(:working_set_size, given_proc) + end + end + + class LeakHandles + include LeakBase + + def matches?(given_proc) + match(:handle_count, given_proc) + end + end + + def leak_memory(opts, &block) + Matchers::LeakMemory.new(opts, &block) + end + def leak_handles(opts, &block) + Matchers::LeakHandles.new(opts, &block) + end +end diff --git a/spec/support/mock/constant.rb b/spec/support/mock/constant.rb new file mode 100644 index 0000000000..c706ad29dd --- /dev/null +++ b/spec/support/mock/constant.rb @@ -0,0 +1,52 @@ +# Allows easy mocking of global and class constants + +# Inspired by: +# http://missingbit.blogspot.com/2011/07/stubbing-constants-in-rspec_20.html +# http://digitaldumptruck.jotabout.com/?p=551 + +def mock_constants(constants, &block) + saved_constants = {} + constants.each do |constant, val| + source_object, const_name = parse_constant(constant) + saved_constants[constant] = source_object.const_get(const_name) + with_warnings(nil) {source_object.const_set(const_name, val) } + end + + begin + block.call + ensure + constants.each do |constant, val| + source_object, const_name = parse_constant(constant) + with_warnings(nil) { source_object.const_set(const_name, saved_constants[constant]) } + end + end +end + +def parse_constant(constant) + source, _, constant_name = constant.to_s.rpartition('::') + [constantize(source), constant_name] +end + +# Taken from ActiveSupport + +# File activesupport/lib/active_support/core_ext/kernel/reporting.rb, line 3 +# +# Sets $VERBOSE for the duration of the block and back to its original value afterwards. +def with_warnings(flag) + old_verbose, $VERBOSE = $VERBOSE, flag + yield +ensure + $VERBOSE = old_verbose +end + +# File activesupport/lib/active_support/inflector/methods.rb, line 209 +def constantize(camel_cased_word) + names = camel_cased_word.split('::') + names.shift if names.empty? || names.first.empty? + + constant = Object + names.each do |name| + constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name) + end + constant +end diff --git a/spec/support/mock/platform.rb b/spec/support/mock/platform.rb new file mode 100644 index 0000000000..78b704ea9b --- /dev/null +++ b/spec/support/mock/platform.rb @@ -0,0 +1,18 @@ +# makes Chef think it's running on a certain platform..useful for unit testing +# platform-specific functionality. +# +# If a block is given yields to the block with +RUBY_PLATFORM+ set to +# 'i386-mingw32' (windows) or 'x86_64-darwin11.2.0' (unix). Usueful for +# testing code that mixes in platform specific modules like +Chef::Mixin::Securable+ +# or +Chef::FileAccessControl+ +def platform_mock(platform = :unix, &block) + Chef::Platform.stub!(:windows?).and_return(platform == :windows ? true : false) + ENV['SYSTEMDRIVE'] = (platform == :windows ? 'C:' : nil) + if block_given? + mock_constants({"RUBY_PLATFORM" => (platform == :windows ? 'i386-mingw32' : 'x86_64-darwin11.2.0'), + "File::PATH_SEPARATOR" => (platform == :windows ? ";" : ":"), + "File::ALT_SEPARATOR" => (platform == :windows ? "\\" : nil) }) do +yield + end + end +end diff --git a/spec/support/platform_helpers.rb b/spec/support/platform_helpers.rb new file mode 100644 index 0000000000..558817d72a --- /dev/null +++ b/spec/support/platform_helpers.rb @@ -0,0 +1,31 @@ +def ruby_19? + !!(RUBY_VERSION =~ /^1.9/) +end + +def ruby_18? + !!(RUBY_VERSION =~ /^1.8/) +end + +def windows? + !!(RUBY_PLATFORM =~ /mswin|mingw|windows/) +end + +# def jruby? + +def unix? + !windows? +end + +def os_x? + !!(RUBY_PLATFORM =~ /darwin/) +end + +def solaris? + !!(RUBY_PLATFORM =~ /solaris/) +end + +def freebsd? + !!(RUBY_PLATFORM =~ /freebsd/) +end + +DEV_NULL = windows? ? 'NUL' : '/dev/null' diff --git a/spec/support/platforms/prof/gc.rb b/spec/support/platforms/prof/gc.rb new file mode 100644 index 0000000000..6ca50df648 --- /dev/null +++ b/spec/support/platforms/prof/gc.rb @@ -0,0 +1,54 @@ +# +# Author:: Seth Chisamore (<schisamo@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +module RSpec + module Prof + module GC + class Profiler + + # GC 1 invokes. + # Index Invoke Time(sec) Use Size(byte) Total Size(byte) Total Object GC time(ms) + # 1 0.012 159240 212940 10647 0.00000000000001530000 + LINE_PATTERN = /^\s+([\d\.]*)\s+([\d\.]*)\s+([\d\.]*)\s+([\d\.]*)\s+([\d\.]*)\s+([\d\.]*)$/ + + def start + ::GC::Profiler.enable unless ::GC::Profiler.enabled? + end + + def stop + ::GC::Profiler.disable + end + + def working_set_size + begin + ::GC.start + ::GC::Profiler.result.scan(LINE_PATTERN)[-1][2].to_i if ::GC::Profiler.enabled? + ensure + ::GC::Profiler.clear + end + end + + def handle_count + 0 + end + + end + end + end +end + diff --git a/spec/support/platforms/prof/win32.rb b/spec/support/platforms/prof/win32.rb new file mode 100644 index 0000000000..ab256ff0fc --- /dev/null +++ b/spec/support/platforms/prof/win32.rb @@ -0,0 +1,46 @@ +# +# Author:: Seth Chisamore (<schisamo@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/win32/process' + +module RSpec + module Prof + module Win32 + class Profiler + + def start + GC.start + end + + def stop + GC.start + end + + def working_set_size + Chef::ReservedNames::Win32::Process.get_current_process.memory_info[:WorkingSetSize] + end + + def handle_count + Chef::ReservedNames::Win32::Process.get_current_process.handle_count + end + end + + end + end +end + diff --git a/spec/support/shared/functional/directory_resource.rb b/spec/support/shared/functional/directory_resource.rb new file mode 100644 index 0000000000..14ab7a10e8 --- /dev/null +++ b/spec/support/shared/functional/directory_resource.rb @@ -0,0 +1,85 @@ +# +# Author:: Seth Chisamore (<schisamo@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +shared_examples_for "a directory resource" do + context "when the target directory does not exist" do + it "creates the directory when the :create action is run" do + resource.run_action(:create) + File.should exist(path) + end + + it "recursively creates required directories if requested" do + resource.recursive(true) + recursive_path = File.join(path, 'red-headed-stepchild') + resource.path(recursive_path) + resource.run_action(:create) + File.should exist(path) + File.should exist(recursive_path) + end + end + + context "when the target directory exists" do + before(:each) do + FileUtils.mkdir(path) + end + + it "does not re-create the directory" do + resource.run_action(:create) + File.should exist(path) + end + + it "deletes the directory when the :delete action is run" do + resource.run_action(:delete) + File.should_not exist(path) + end + + it "recursively deletes directories if requested" do + FileUtils.mkdir(File.join(path, 'red-headed-stepchild')) + resource.recursive(true) + resource.run_action(:delete) + File.should_not exist(path) + end + end + + # Set up the context for security tests + def allowed_acl(sid, expected_perms) + [ + ACE.access_allowed(sid, expected_perms[:specific]), + ACE.access_allowed(sid, expected_perms[:generic], (Chef::ReservedNames::Win32::API::Security::INHERIT_ONLY_ACE | Chef::ReservedNames::Win32::API::Security::CONTAINER_INHERIT_ACE | Chef::ReservedNames::Win32::API::Security::OBJECT_INHERIT_ACE)) + ] + end + + def denied_acl(sid, expected_perms) + [ + ACE.access_denied(sid, expected_perms[:specific]), + ACE.access_denied(sid, expected_perms[:generic], (Chef::ReservedNames::Win32::API::Security::INHERIT_ONLY_ACE | Chef::ReservedNames::Win32::API::Security::CONTAINER_INHERIT_ACE | Chef::ReservedNames::Win32::API::Security::OBJECT_INHERIT_ACE)) + ] + end + + it_behaves_like "a securable resource" +end + +shared_context Chef::Resource::Directory do + let(:path) do + File.join(Dir.tmpdir, make_tmpname(directory_base, nil)) + end + + after(:each) do + FileUtils.rm_r(path) if File.exists?(path) + end +end diff --git a/spec/support/shared/functional/file_resource.rb b/spec/support/shared/functional/file_resource.rb new file mode 100644 index 0000000000..631a5ed742 --- /dev/null +++ b/spec/support/shared/functional/file_resource.rb @@ -0,0 +1,173 @@ +# +# Author:: Seth Chisamore (<schisamo@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +shared_examples_for "a file with the wrong content" do + it "overwrites the file with the updated content when the :create action is run" do + Chef::Config[:file_backup_path] = CHEF_SPEC_BACKUP_PATH + sleep 1 + resource.run_action(:create) + File.stat(path).mtime.should > @expected_mtime + sha256_checksum(path).should_not == @expected_checksum + end + + it "doesn't overwrite the file when the :create_if_missing action is run" do + sleep 1 + resource.run_action(:create_if_missing) + File.stat(path).mtime.should == @expected_mtime + sha256_checksum(path).should == @expected_checksum + end + + it "should backup the existing file" do + Chef::Config[:file_backup_path] = CHEF_SPEC_BACKUP_PATH + resource.run_action(:create) + Dir.glob(backup_glob).size.should equal(1) + end + + it "should not attempt to backup the existing file if :backup == 0" do + Chef::Config[:file_backup_path] = CHEF_SPEC_BACKUP_PATH + resource.backup(0) + resource.run_action(:create) + Dir.glob(backup_glob).size.should equal(0) + end + + it "deletes the file when the :delete action is run" do + resource.run_action(:delete) + File.should_not exist(path) + end +end + +shared_examples_for "a file with the correct content" do + it "does not overwrite the original when the :create action is run" do + resource.run_action(:create) + sha256_checksum(path).should == @expected_checksum + end + + it "does not update the mtime/atime of the file when the :create action is run" do + sleep 1 + File.stat(path).mtime.should == @expected_mtime + File.stat(path).atime.should be_within(2).of(@expected_atime) + end + + it "doesn't overwrite the file when the :create_if_missing action is run" do + resource.run_action(:create_if_missing) + sha256_checksum(path).should == @expected_checksum + end + + it "deletes the file when the :delete action is run" do + resource.run_action(:delete) + File.should_not exist(path) + end +end + +shared_examples_for "a file resource" do + # note the stripping of the drive letter from the tmpdir on windows + let(:backup_glob) { File.join(CHEF_SPEC_BACKUP_PATH, Dir.tmpdir.sub(/^([A-Za-z]:)/, ""), "#{file_base}*") } + + context "when the target file does not exist" do + it "creates the file when the :create action is run" do + resource.run_action(:create) + File.should exist(path) + end + + it "creates the file with the correct content when the :create action is run" do + resource.run_action(:create) + IO.read(path).should == expected_content + end + + it "creates the file with the correct content when the :create_if_missing action is run" do + resource.run_action(:create_if_missing) + IO.read(path).should == expected_content + end + + it "deletes the file when the :delete action is run" do + resource.run_action(:delete) + File.should_not exist(path) + end + end + + # Set up the context for security tests + def allowed_acl(sid, expected_perms) + [ ACE.access_allowed(sid, expected_perms[:specific]) ] + end + + def denied_acl(sid, expected_perms) + [ ACE.access_denied(sid, expected_perms[:specific]) ] + end + + + context "when the target file has the wrong content" do + before(:each) do + File.open(path, "w") { |f| f.print "This is so wrong!!!" } + @expected_mtime = File.stat(path).mtime + @expected_checksum = sha256_checksum(path) + end + + describe "and the target file has the correct permissions" do + include_context "setup correct permissions" + + it_behaves_like "a file with the wrong content" + + it_behaves_like "a securable resource" + end + + context "and the target file has incorrect permissions" do + include_context "setup broken permissions" + + it_behaves_like "a file with the wrong content" + + it_behaves_like "a securable resource" + end + end + + context "when the target file has the correct content" do + before(:each) do + File.open(path, "w") { |f| f.print expected_content } + @expected_mtime = File.stat(path).mtime + @expected_atime = File.stat(path).atime + @expected_checksum = sha256_checksum(path) + end + + describe "and the target file has the correct permissions" do + include_context "setup correct permissions" + + it_behaves_like "a file with the correct content" + + it_behaves_like "a securable resource" + end + + context "and the target file has incorrect permissions" do + include_context "setup broken permissions" + + it_behaves_like "a file with the correct content" + + it_behaves_like "a securable resource" + end + end + +end + +shared_context Chef::Resource::File do + let(:path) do + File.join(Dir.tmpdir, make_tmpname(file_base, nil)) + end + + after(:each) do + FileUtils.rm_r(path) if File.exists?(path) + FileUtils.rm_r(CHEF_SPEC_BACKUP_PATH) if File.exists?(CHEF_SPEC_BACKUP_PATH) + end +end diff --git a/spec/support/shared/functional/knife.rb b/spec/support/shared/functional/knife.rb new file mode 100644 index 0000000000..e96de7c27a --- /dev/null +++ b/spec/support/shared/functional/knife.rb @@ -0,0 +1,37 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: AJ Christensen (<aj@junglist.gen.nz>) +# Author:: Ho-Sheng Hsiao (<hosh@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +module SpecHelpers + module Knife + def redefine_argv(value) + Object.send(:remove_const, :ARGV) + Object.send(:const_set, :ARGV, value) + end + + def with_argv(*argv) + original_argv = ARGV + redefine_argv(argv.flatten) + begin + yield + ensure + redefine_argv(original_argv) + end + end + end +end diff --git a/spec/support/shared/functional/securable_resource.rb b/spec/support/shared/functional/securable_resource.rb new file mode 100644 index 0000000000..2eeb16c784 --- /dev/null +++ b/spec/support/shared/functional/securable_resource.rb @@ -0,0 +1,394 @@ +# +# Author:: Seth Chisamore (<schisamo@opscode.com>) +# Author:: Mark Mzyk (<mmzyk@opscode.com>) +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2011 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# TODO test that these work when you are logged on as a user joined to a domain (rather than local computer) +# TODO test that you can set users from other domains + +require 'etc' + +shared_context "setup correct permissions" do + context "on unix", :unix_only do + context "with root", :requires_root do + before :each do + File.chown(Etc.getpwnam('nobody').uid, 1337, path) + File.chmod(0776, path) + end + end + + context "without root", :requires_unprivileged_user do + before :each do + File.chmod(0776, path) + end + end + end + + # FIXME: windows +end + +shared_context "setup broken permissions" do + context "on unix", :unix_only do + context "with root", :requires_root do + before :each do + File.chown(0, 0, path) + File.chmod(0644, path) + end + end + + context "without root", :requires_unprivileged_user do + before :each do + File.chmod(0644, path) + end + end + end + + # FIXME: windows +end + +shared_examples_for "a securable resource" do + context "on Unix", :unix_only do + let(:expected_user_name) { 'nobody' } + let(:expected_uid) { Etc.getpwnam(expected_user_name).uid } + let(:desired_gid) { 1337 } + let(:expected_gid) { 1337 } + + pending "should set an owner (Rerun specs under root)", :requires_unprivileged_user => true + pending "should set a group (Rerun specs under root)", :requires_unprivileged_user => true + + it "should set an owner", :requires_root do + resource.owner expected_user_name + resource.run_action(:create) + File.lstat(path).uid.should == expected_uid + end + + it "should set a group", :requires_root do + resource.group desired_gid + resource.run_action(:create) + File.lstat(path).gid.should == expected_gid + end + + it "should set permissions in string form as an octal number" do + mode_string = '776' + resource.mode mode_string + resource.run_action(:create) + pending('Linux does not support lchmod', :if => resource.instance_of?(Chef::Resource::Link) && !os_x? && !freebsd?) do + (File.lstat(path).mode & 007777).should == (mode_string.oct & 007777) + end + end + + it "should set permissions in numeric form as a ruby-interpreted octal" do + mode_integer = 0776 + resource.mode mode_integer + resource.run_action(:create) + pending('Linux does not support lchmod', :if => resource.instance_of?(Chef::Resource::Link) && !os_x? && !freebsd?) do + (File.lstat(path).mode & 007777).should == (mode_integer & 007777) + end + end + end + + context "on Windows", :windows_only do + + if windows? + SID = Chef::ReservedNames::Win32::Security::SID + ACE = Chef::ReservedNames::Win32::Security::ACE + end + + def get_security_descriptor(path) + Chef::ReservedNames::Win32::Security.get_named_security_info(path) + end + + def explicit_aces + descriptor.dacl.select { |ace| ace.explicit? } + end + + def extract_ace_properties(aces) + hashes = [] + aces.each do |ace| + hashes << { :mask => ace.mask, :type => ace.type, :flags => ace.flags } + end + hashes + end + + # Standard expected rights + let(:expected_read_perms) do + { + :generic => Chef::ReservedNames::Win32::API::Security::GENERIC_READ, + :specific => Chef::ReservedNames::Win32::API::Security::FILE_GENERIC_READ, + } + end + + let(:expected_read_execute_perms) do + { + :generic => Chef::ReservedNames::Win32::API::Security::GENERIC_READ | Chef::ReservedNames::Win32::API::Security::GENERIC_EXECUTE, + :specific => Chef::ReservedNames::Win32::API::Security::FILE_GENERIC_READ | Chef::ReservedNames::Win32::API::Security::FILE_GENERIC_EXECUTE + } + end + + let(:expected_write_perms) do + { + :generic => Chef::ReservedNames::Win32::API::Security::GENERIC_WRITE, + :specific => Chef::ReservedNames::Win32::API::Security::FILE_GENERIC_WRITE + } + end + + let(:expected_modify_perms) do + { + :generic => Chef::ReservedNames::Win32::API::Security::GENERIC_READ | Chef::ReservedNames::Win32::API::Security::GENERIC_WRITE | Chef::ReservedNames::Win32::API::Security::GENERIC_EXECUTE | Chef::ReservedNames::Win32::API::Security::DELETE, + :specific => Chef::ReservedNames::Win32::API::Security::FILE_GENERIC_READ | Chef::ReservedNames::Win32::API::Security::FILE_GENERIC_WRITE | Chef::ReservedNames::Win32::API::Security::FILE_GENERIC_EXECUTE | Chef::ReservedNames::Win32::API::Security::DELETE + } + end + + let(:expected_full_control_perms) do + { + :generic => Chef::ReservedNames::Win32::API::Security::GENERIC_ALL, + :specific => Chef::ReservedNames::Win32::API::Security::FILE_ALL_ACCESS + } + end + + RSpec::Matchers.define :have_expected_properties do |mask, type, flags| + match do |ace| + ace.mask == mask + ace.type == type + ace.flags == flags + end + end + + def descriptor + get_security_descriptor(path) + end + + before(:each) do + resource.run_action(:delete) + end + + it "sets owner to Administrators on create if owner is not specified" do + File.exist?(path).should == false + resource.run_action(:create) + descriptor.owner.should == SID.Administrators + end + + it "sets owner when owner is specified" do + resource.owner 'Guest' + resource.run_action(:create) + descriptor.owner.should == SID.Guest + end + + it "fails to set owner when owner has invalid characters" do + lambda { resource.owner 'Lance "The Nose" Glindenberry III' }.should raise_error#(Chef::Exceptions::ValidationFailed) + end + + it "sets owner when owner is specified with a \\" do + resource.owner "#{ENV['USERDOMAIN']}\\Guest" + resource.run_action(:create) + descriptor.owner.should == SID.Guest + end + + it "leaves owner alone if owner is not specified and resource already exists" do + # Set owner to Guest so it's not the same as the current user (which is the default on create) + resource.owner 'Guest' + resource.run_action(:create) + descriptor.owner.should == SID.Guest + + new_resource = create_resource + new_resource.owner.should == nil + new_resource.run_action(:create) + descriptor.owner.should == SID.Guest + end + + it "sets group to None on create if group is not specified" do + resource.group.should == nil + File.exist?(path).should == false + resource.run_action(:create) + descriptor.group.should == SID.None + end + + it "sets group when group is specified" do + resource.group 'Everyone' + resource.run_action(:create) + descriptor.group.should == SID.Everyone + end + + it "fails to set group when group has invalid characters" do + lambda { resource.group 'Lance "The Nose" Glindenberry III' }.should raise_error(Chef::Exceptions::ValidationFailed) + end + + it "sets group when group is specified with a \\" do + pending "Need to find a group containing a backslash that is on most peoples' machines" do + resource.group "#{ENV['COMPUTERNAME']}\\Administrators" + resource.run_action(:create) + descriptor.group.should == SID.Everyone + end + end + + it "leaves group alone if group is not specified and resource already exists" do + # Set group to Everyone so it's not the default (None) + resource.group 'Everyone' + resource.run_action(:create) + descriptor.group.should == SID.Everyone + + new_resource = create_resource + new_resource.group.should == nil + new_resource.run_action(:create) + descriptor.group.should == SID.Everyone + end + + describe "with rights and deny_rights attributes" do + + it "correctly sets :read rights" do + resource.rights(:read, 'Guest') + resource.run_action(:create) + explicit_aces.should == allowed_acl(SID.Guest, expected_read_perms) + end + + it "correctly sets :read_execute rights" do + resource.rights(:read_execute, 'Guest') + resource.run_action(:create) + explicit_aces.should == allowed_acl(SID.Guest, expected_read_execute_perms) + end + + it "correctly sets :write rights" do + resource.rights(:write, 'Guest') + resource.run_action(:create) + explicit_aces.should == allowed_acl(SID.Guest, expected_write_perms) + end + + it "correctly sets :modify rights" do + resource.rights(:modify, 'Guest') + resource.run_action(:create) + explicit_aces.should == allowed_acl(SID.Guest, expected_modify_perms) + end + + it "correctly sets :full_control rights" do + resource.rights(:full_control, 'Guest') + resource.run_action(:create) + explicit_aces.should == allowed_acl(SID.Guest, expected_full_control_perms) + end + + it "correctly sets deny_rights" do + # deny is an ACE with full rights, but is a deny type ace, not an allow type + resource.deny_rights(:full_control, 'Guest') + resource.run_action(:create) + explicit_aces.should == denied_acl(SID.Guest, expected_full_control_perms) + end + + it "Sets multiple rights" do + resource.rights(:read, 'Everyone') + resource.rights(:modify, 'Guest') + resource.run_action(:create) + + explicit_aces.should == + allowed_acl(SID.Everyone, expected_read_perms) + + allowed_acl(SID.Guest, expected_modify_perms) + end + + it "Sets deny_rights ahead of rights" do + resource.rights(:read, 'Everyone') + resource.deny_rights(:modify, 'Guest') + resource.run_action(:create) + + explicit_aces.should == + denied_acl(SID.Guest, expected_modify_perms) + + allowed_acl(SID.Everyone, expected_read_perms) + end + + it "Sets deny_rights ahead of rights when specified in reverse order" do + resource.deny_rights(:modify, 'Guest') + resource.rights(:read, 'Everyone') + resource.run_action(:create) + + explicit_aces.should == + denied_acl(SID.Guest, expected_modify_perms) + + allowed_acl(SID.Everyone, expected_read_perms) + end + + end + + context "with a mode attribute" do + if windows? + Security = Chef::ReservedNames::Win32::API::Security + end + + it "respects mode in string form as an octal number" do + #on windows, mode cannot modify owner and/or group permissons + #unless the owner and/or group as appropriate is specified + resource.mode '400' + resource.owner 'Guest' + resource.group 'Everyone' + resource.run_action(:create) + + explicit_aces.should == [ ACE.access_allowed(SID.Guest, Security::FILE_GENERIC_READ) ] + end + + it "respects mode in numeric form as a ruby-interpreted octal" do + resource.mode 0700 + resource.owner 'Guest' + resource.run_action(:create) + + explicit_aces.should == [ ACE.access_allowed(SID.Guest, Security::FILE_GENERIC_READ | Security::FILE_GENERIC_WRITE | Security::FILE_GENERIC_EXECUTE | Security::DELETE) ] + end + + it "respects the owner, group and everyone bits of mode" do + resource.mode 0754 + resource.owner 'Guest' + resource.group 'Administrators' + resource.run_action(:create) + + explicit_aces.should == [ + ACE.access_allowed(SID.Guest, Security::FILE_GENERIC_READ | Security::FILE_GENERIC_WRITE | Security::FILE_GENERIC_EXECUTE | Security::DELETE), + ACE.access_allowed(SID.Administrators, Security::FILE_GENERIC_READ | Security::FILE_GENERIC_EXECUTE), + ACE.access_allowed(SID.Everyone, Security::FILE_GENERIC_READ) + ] + end + + it "respects the individual read, write and execute bits of mode" do + resource.mode 0421 + resource.owner 'Guest' + resource.group 'Administrators' + resource.run_action(:create) + + explicit_aces.should == [ + ACE.access_allowed(SID.Guest, Security::FILE_GENERIC_READ), + ACE.access_allowed(SID.Administrators, Security::FILE_GENERIC_WRITE | Security::DELETE), + ACE.access_allowed(SID.Everyone, Security::FILE_GENERIC_EXECUTE) + ] + end + + it 'warns when mode tries to set owner bits but owner is not specified' do + @warn = [] + Chef::Log.stub!(:warn) { |msg| @warn << msg } + + resource.mode 0400 + resource.run_action(:create) + + @warn.include?("Mode 400 includes bits for the owner, but owner is not specified").should be_true + end + + it 'warns when mode tries to set group bits but group is not specified' do + @warn = [] + Chef::Log.stub!(:warn) { |msg| @warn << msg } + + resource.mode 0040 + resource.run_action(:create) + + @warn.include?("Mode 040 includes bits for the group, but group is not specified").should be_true + end + end + + end +end diff --git a/spec/support/shared/unit/api_error_inspector.rb b/spec/support/shared/unit/api_error_inspector.rb new file mode 100644 index 0000000000..8231ceb195 --- /dev/null +++ b/spec/support/shared/unit/api_error_inspector.rb @@ -0,0 +1,192 @@ +# +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + + + +# == API Error Inspector Examples +# These tests are work in progress. They exercise the code enough to ensure it +# runs without error, but don't make assertions about the output. This is +# because aspects such as how information gets formatted, what's included, etc. +# are still in flux. When testing an inspector, change the outputter to use +# STDOUT and manually check the ouput. + +shared_examples_for "an api error inspector" do + + before do + @node_name = "test-node.example.com" + @config = { + :validation_client_name => "testorg-validator", + :validation_key => "/etc/chef/testorg-validator.pem", + :chef_server_url => "https://chef-api.example.com", + :node_name => "testnode-name", + :client_key => "/etc/chef/client.pem" + } + @description = Chef::Formatters::ErrorDescription.new("Error registering the node:") + @outputter = Chef::Formatters::Outputter.new(StringIO.new, STDERR) + #@outputter = Chef::Formatters::Outputter.new(STDOUT, STDERR) + + end + + describe "when explaining a network error" do + before do + @exception = Errno::ECONNREFUSED.new("connection refused") + @inspector = described_class.new(@node_name, @exception, @config) + @inspector.add_explanation(@description) + end + + it "prints a nice message" do + @description.display(@outputter) + end + + end + + describe "when explaining a 'private key missing' error" do + before do + @exception = Chef::Exceptions::PrivateKeyMissing.new("no private key yo") + @inspector = described_class.new(@node_name, @exception, @config) + @inspector.add_explanation(@description) + end + + it "prints a nice message" do + @description.display(@outputter) + end + + end + + describe "when explaining a 401 caused by clock skew" do + before do + @response_body = "synchronize the clock on your host" + @response = Net::HTTPUnauthorized.new("1.1", "401", "(response) unauthorized") + @response.stub!(:body).and_return(@response_body) + @exception = Net::HTTPServerException.new("(exception) unauthorized", @response) + @inspector = described_class.new(@node_name, @exception, @config) + @inspector.add_explanation(@description) + end + + it "prints a nice message" do + @description.display(@outputter) + end + + end + + describe "when explaining a 401 (no clock skew)" do + before do + @response_body = "check your key and node name" + @response = Net::HTTPUnauthorized.new("1.1", "401", "(response) unauthorized") + @response.stub!(:body).and_return(@response_body) + @exception = Net::HTTPServerException.new("(exception) unauthorized", @response) + @inspector = described_class.new(@node_name, @exception, @config) + @inspector.add_explanation(@description) + end + + it "prints a nice message" do + @description.display(@outputter) + end + + end + + describe "when explaining a 403" do + before do + @response_body = "forbidden" + @response = Net::HTTPForbidden.new("1.1", "403", "(response) forbidden") + @response.stub!(:body).and_return(@response_body) + @exception = Net::HTTPServerException.new("(exception) forbidden", @response) + @inspector = described_class.new(@node_name, @exception, @config) + @inspector.add_explanation(@description) + end + + it "prints a nice message" do + @description.display(@outputter) + end + + end + + describe "when explaining a 400" do + before do + @response_body = "didn't like your data" + @response = Net::HTTPBadRequest.new("1.1", "400", "(response) bad request") + @response.stub!(:body).and_return(@response_body) + @exception = Net::HTTPServerException.new("(exception) bad request", @response) + @inspector = described_class.new(@node_name, @exception, @config) + @inspector.add_explanation(@description) + end + + it "prints a nice message" do + @description.display(@outputter) + end + + end + + describe "when explaining a 404" do + before do + @response_body = "probably caused by a redirect to a get" + @response = Net::HTTPNotFound.new("1.1", "404", "(response) not found") + @response.stub!(:body).and_return(@response_body) + @exception = Net::HTTPServerException.new("(exception) not found", @response) + @inspector = described_class.new(@node_name, @exception, @config) + @inspector.add_explanation(@description) + end + + it "prints a nice message" do + @description.display(@outputter) + end + end + + describe "when explaining a 500" do + before do + @response_body = "sad trombone" + @response = Net::HTTPInternalServerError.new("1.1", "500", "(response) internal server error") + @response.stub!(:body).and_return(@response_body) + @exception = Net::HTTPFatalError.new("(exception) internal server error", @response) + @inspector = described_class.new(@node_name, @exception, @config) + @inspector.add_explanation(@description) + end + + it "prints a nice message" do + @description.display(@outputter) + end + end + + describe "when explaining a 503" do + before do + @response_body = "sad trombone orchestra" + @response = Net::HTTPBadGateway.new("1.1", "502", "(response) bad gateway") + @response.stub!(:body).and_return(@response_body) + @exception = Net::HTTPFatalError.new("(exception) bad gateway", @response) + @inspector = described_class.new(@node_name, @exception, @config) + @inspector.add_explanation(@description) + end + + it "prints a nice message" do + @description.display(@outputter) + end + end + + describe "when explaining an unknown error" do + before do + @exception = RuntimeError.new("(exception) something went wrong") + @inspector = described_class.new(@node_name, @exception, @config) + @inspector.add_explanation(@description) + end + + it "prints a nice message" do + @description.display(@outputter) + end + end + +end diff --git a/spec/support/shared/unit/file_system_support.rb b/spec/support/shared/unit/file_system_support.rb new file mode 100644 index 0000000000..8516292c67 --- /dev/null +++ b/spec/support/shared/unit/file_system_support.rb @@ -0,0 +1,110 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system' +require 'chef/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/base_fs_object' + +module FileSystemSupport + class MemoryFile < Chef::ChefFS::FileSystem::BaseFSObject + def initialize(name, parent, value) + super(name, parent) + @value = value + end + def read + return @value + end + end + + class MemoryDir < Chef::ChefFS::FileSystem::BaseFSDir + def initialize(name, parent) + super(name, parent) + @children = [] + end + attr_reader :children + def child(name) + @children.select { |child| child.name == name }.first || Chef::ChefFS::FileSystem::NonexistentFSObject.new(name, self) + end + def add_child(child) + @children.push(child) + end + def can_have_child?(name, is_dir) + root.cannot_be_in_regex ? (name !~ root.cannot_be_in_regex) : true + end + end + + class MemoryRoot < MemoryDir + def initialize(pretty_name, cannot_be_in_regex = nil) + super('', nil) + @pretty_name = pretty_name + @cannot_be_in_regex = cannot_be_in_regex + end + + attr_reader :cannot_be_in_regex + + def path_for_printing + @pretty_name + end + end + + def memory_fs(pretty_name, value, cannot_be_in_regex = nil) + if !value.is_a?(Hash) + raise "memory_fs() must take a Hash" + end + dir = MemoryRoot.new(pretty_name, cannot_be_in_regex) + value.each do |key, child| + dir.add_child(memory_fs_value(child, key.to_s, dir)) + end + dir + end + + def memory_fs_value(value, name = '', parent = nil) + if value.is_a?(Hash) + dir = MemoryDir.new(name, parent) + value.each do |key, child| + dir.add_child(memory_fs_value(child, key.to_s, dir)) + end + dir + else + MemoryFile.new(name, parent, value || "#{name}\n") + end + end + + def pattern(p) + Chef::ChefFS::FilePattern.new(p) + end + + def return_paths(*expected) + ReturnPaths.new(expected) + end + + def no_blocking_calls_allowed + [ MemoryFile, MemoryDir ].each do |c| + [ :children, :exists?, :read ].each do |m| + c.any_instance.stub(m).and_raise("#{m.to_s} should not be called") + end + end + end + + def list_should_yield_paths(fs, pattern_str, *expected_paths) + result_paths = [] + Chef::ChefFS::FileSystem.list(fs, pattern(pattern_str)) { |result| result_paths << result.path } + result_paths.should =~ expected_paths + end +end + |