diff options
Diffstat (limited to 'spec/unit/runner_spec.rb')
-rw-r--r-- | spec/unit/runner_spec.rb | 402 |
1 files changed, 402 insertions, 0 deletions
diff --git a/spec/unit/runner_spec.rb b/spec/unit/runner_spec.rb new file mode 100644 index 0000000000..388596e350 --- /dev/null +++ b/spec/unit/runner_spec.rb @@ -0,0 +1,402 @@ + +# 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. +# + +require 'spec_helper' + +class SnitchyProvider < Chef::Provider + def self.all_actions_called + @all_actions_called ||= [] + end + + def self.action_called(action) + all_actions_called << action + end + + def self.clear_action_record + @all_actions_called = nil + end + + def load_current_resource + true + end + + def action_first_action + @new_resource.updated_by_last_action(true) + self.class.action_called(:first) + end + + def action_second_action + @new_resource.updated_by_last_action(true) + self.class.action_called(:second) + end + + def action_third_action + @new_resource.updated_by_last_action(true) + self.class.action_called(:third) + end + +end + +class FailureResource < Chef::Resource + + attr_accessor :action + + def initialize(*args) + super + @action = :fail + end + + def provider + FailureProvider + end +end + +class FailureProvider < Chef::Provider + + class ChefClientFail < StandardError; end + + def load_current_resource + true + end + + def action_fail + raise ChefClientFail, "chef had an error of some sort" + end +end + +describe Chef::Runner do + + before(:each) do + @node = Chef::Node.new + @node.name "latte" + @node.automatic[:platform] = "mac_os_x" + @node.automatic[:platform_version] = "10.5.1" + @events = Chef::EventDispatch::Dispatcher.new + @run_context = Chef::RunContext.new(@node, Chef::CookbookCollection.new({}), @events) + @first_resource = Chef::Resource::Cat.new("loulou1", @run_context) + @run_context.resource_collection << @first_resource + Chef::Platform.set( + :resource => :cat, + :provider => Chef::Provider::SnakeOil + ) + @runner = Chef::Runner.new(@run_context) + end + + it "should pass each resource in the collection to a provider" do + @run_context.resource_collection.should_receive(:execute_each_resource).once + @runner.converge + end + + it "should use the provider specified by the resource (if it has one)" do + provider = Chef::Provider::Easy.new(@run_context.resource_collection[0], @run_context) + # Expect provider to be called twice, because will fall back to old provider lookup + @run_context.resource_collection[0].should_receive(:provider).twice.and_return(Chef::Provider::Easy) + Chef::Provider::Easy.should_receive(:new).once.and_return(provider) + @runner.converge + end + + it "should use the platform provider if it has one" do + Chef::Platform.should_receive(:find_provider_for_node).once.and_return(Chef::Provider::SnakeOil) + @runner.converge + end + + it "should run the action for each resource" do + Chef::Platform.should_receive(:find_provider_for_node).once.and_return(Chef::Provider::SnakeOil) + provider = Chef::Provider::SnakeOil.new(@run_context.resource_collection[0], @run_context) + provider.should_receive(:action_sell).once.and_return(true) + Chef::Provider::SnakeOil.should_receive(:new).once.and_return(provider) + @runner.converge + end + + it "should raise exceptions as thrown by a provider" do + provider = Chef::Provider::SnakeOil.new(@run_context.resource_collection[0], @run_context) + Chef::Provider::SnakeOil.stub!(:new).once.and_return(provider) + provider.stub!(:action_sell).once.and_raise(ArgumentError) + lambda { @runner.converge }.should raise_error(ArgumentError) + end + + it "should not raise exceptions thrown by providers if the resource has ignore_failure set to true" do + @run_context.resource_collection[0].stub!(:ignore_failure).and_return(true) + provider = Chef::Provider::SnakeOil.new(@run_context.resource_collection[0], @run_context) + Chef::Provider::SnakeOil.stub!(:new).once.and_return(provider) + provider.stub!(:action_sell).once.and_raise(ArgumentError) + lambda { @runner.converge }.should_not raise_error(ArgumentError) + end + + it "should retry with the specified delay if retries are specified" do + @first_resource.retries 3 + provider = Chef::Provider::SnakeOil.new(@run_context.resource_collection[0], @run_context) + Chef::Provider::SnakeOil.stub!(:new).once.and_return(provider) + provider.stub!(:action_sell).and_raise(ArgumentError) + @first_resource.should_receive(:sleep).with(2).exactly(3).times + lambda { @runner.converge }.should raise_error(ArgumentError) + end + + it "should execute immediate actions on changed resources" do + notifying_resource = Chef::Resource::Cat.new("peanut", @run_context) + notifying_resource.action = :purr # only action that will set updated on the resource + + @run_context.resource_collection << notifying_resource + @first_resource.action = :nothing # won't be updated unless notified by other resource + + notifying_resource.notifies(:purr, @first_resource, :immediately) + + @runner.converge + + @first_resource.should be_updated + end + + it "should follow a chain of actions" do + @first_resource.action = :nothing + + middle_resource = Chef::Resource::Cat.new("peanut", @run_context) + middle_resource.action = :nothing + @run_context.resource_collection << middle_resource + middle_resource.notifies(:purr, @first_resource, :immediately) + + last_resource = Chef::Resource::Cat.new("snuffles", @run_context) + last_resource.action = :purr + @run_context.resource_collection << last_resource + last_resource.notifies(:purr, middle_resource, :immediately) + + @runner.converge + + last_resource.should be_updated # by action(:purr) + middle_resource.should be_updated # by notification from last_resource + @first_resource.should be_updated # by notification from middle_resource + end + + it "should execute delayed actions on changed resources" do + @first_resource.action = :nothing + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :purr + + @run_context.resource_collection << second_resource + second_resource.notifies(:purr, @first_resource, :delayed) + + @runner.converge + + @first_resource.should be_updated + end + + it "should execute delayed notifications when a failure occurs in the chef client run" do + @first_resource.action = :nothing + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :purr + + @run_context.resource_collection << second_resource + second_resource.notifies(:purr, @first_resource, :delayed) + + third_resource = FailureResource.new("explode", @run_context) + @run_context.resource_collection << third_resource + + lambda {@runner.converge}.should raise_error(FailureProvider::ChefClientFail) + + @first_resource.should be_updated + end + + it "should execute delayed notifications when a failure occurs in a notification" do + @first_resource.action = :nothing + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :purr + + @run_context.resource_collection << second_resource + + third_resource = FailureResource.new("explode", @run_context) + third_resource.action = :nothing + @run_context.resource_collection << third_resource + + second_resource.notifies(:fail, third_resource, :delayed) + second_resource.notifies(:purr, @first_resource, :delayed) + + lambda {@runner.converge}.should raise_error(FailureProvider::ChefClientFail) + + @first_resource.should be_updated + end + + it "should execute delayed notifications when a failure occurs in multiple notifications" do + @first_resource.action = :nothing + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :purr + + @run_context.resource_collection << second_resource + + third_resource = FailureResource.new("explode", @run_context) + third_resource.action = :nothing + @run_context.resource_collection << third_resource + + fourth_resource = FailureResource.new("explode again", @run_context) + fourth_resource.action = :nothing + @run_context.resource_collection << fourth_resource + + second_resource.notifies(:fail, third_resource, :delayed) + second_resource.notifies(:fail, fourth_resource, :delayed) + second_resource.notifies(:purr, @first_resource, :delayed) + + exception = nil + begin + @runner.converge + rescue => e + exception = e + end + exception.should be_a(Chef::Exceptions::MultipleFailures) + + expected_message =<<-E +Multiple failures occurred: +* FailureProvider::ChefClientFail occurred in delayed notification: [explode] (dynamically defined) had an error: FailureProvider::ChefClientFail: chef had an error of some sort +* FailureProvider::ChefClientFail occurred in delayed notification: [explode again] (dynamically defined) had an error: FailureProvider::ChefClientFail: chef had an error of some sort +E + exception.message.should == expected_message + + @first_resource.should be_updated + end + + it "does not duplicate delayed notifications" do + SnitchyProvider.clear_action_record + + Chef::Platform.set( + :resource => :cat, + :provider => SnitchyProvider + ) + + @first_resource.action = :nothing + + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :first_action + @run_context.resource_collection << second_resource + + third_resource = Chef::Resource::Cat.new("snickers", @run_context) + third_resource.action = :first_action + @run_context.resource_collection << third_resource + + second_resource.notifies(:second_action, @first_resource, :delayed) + second_resource.notifies(:third_action, @first_resource, :delayed) + + third_resource.notifies(:second_action, @first_resource, :delayed) + third_resource.notifies(:third_action, @first_resource, :delayed) + + @runner.converge + # resources 2 and 3 call :first_action in the course of normal resource + # execution, and schedule delayed actions :second and :third on the first + # resource. The duplicate actions should "collapse" to a single notification + # and order should be preserved. + SnitchyProvider.all_actions_called.should == [:first, :first, :second, :third] + end + + it "executes delayed notifications in the order they were declared" do + SnitchyProvider.clear_action_record + + Chef::Platform.set( + :resource => :cat, + :provider => SnitchyProvider + ) + + @first_resource.action = :nothing + + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :first_action + @run_context.resource_collection << second_resource + + third_resource = Chef::Resource::Cat.new("snickers", @run_context) + third_resource.action = :first_action + @run_context.resource_collection << third_resource + + second_resource.notifies(:second_action, @first_resource, :delayed) + second_resource.notifies(:second_action, @first_resource, :delayed) + + third_resource.notifies(:third_action, @first_resource, :delayed) + third_resource.notifies(:third_action, @first_resource, :delayed) + + @runner.converge + SnitchyProvider.all_actions_called.should == [:first, :first, :second, :third] + end + + it "does not fire notifications if the resource was not updated by the last action executed" do + # REGRESSION TEST FOR CHEF-1452 + SnitchyProvider.clear_action_record + + Chef::Platform.set( + :resource => :cat, + :provider => SnitchyProvider + ) + + @first_resource.action = :first_action + + second_resource = Chef::Resource::Cat.new("peanut", @run_context) + second_resource.action = :nothing + @run_context.resource_collection << second_resource + + third_resource = Chef::Resource::Cat.new("snickers", @run_context) + third_resource.action = :nothing + @run_context.resource_collection << third_resource + + @first_resource.notifies(:second_action, second_resource, :immediately) + second_resource.notifies(:third_action, third_resource, :immediately) + + @runner.converge + + # All of the resources should only fire once: + SnitchyProvider.all_actions_called.should == [:first, :second, :third] + + # all of the resources should be marked as updated for reporting purposes + @first_resource.should be_updated + second_resource.should be_updated + third_resource.should be_updated + end + + it "should check a resource's only_if and not_if if notified by another resource" do + @first_resource.action = :nothing + + only_if_called_times = 0 + @first_resource.only_if {only_if_called_times += 1; true} + + not_if_called_times = 0 + @first_resource.not_if {not_if_called_times += 1; false} + + second_resource = Chef::Resource::Cat.new("carmel", @run_context) + @run_context.resource_collection << second_resource + second_resource.notifies(:purr, @first_resource, :delayed) + second_resource.action = :purr + + # hits only_if first time when the resource is run in order, second on notify + @runner.converge + + only_if_called_times.should == 2 + not_if_called_times.should == 2 + end + + it "should resolve resource references in notifications when resources are defined lazily" do + @first_resource.action = :nothing + + lazy_resources = lambda { + last_resource = Chef::Resource::Cat.new("peanut", @run_context) + @run_context.resource_collection << last_resource + last_resource.notifies(:purr, @first_resource.to_s, :delayed) + last_resource.action = :purr + } + second_resource = Chef::Resource::RubyBlock.new("myblock", @run_context) + @run_context.resource_collection << second_resource + second_resource.block { lazy_resources.call } + + @runner.converge + + @first_resource.should be_updated + end + +end + |