1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
|
require 'spec_helper'
require 'chef/recipe'
# The goal of these tests is to make sure that loading resources from a file creates the necessary notifications.
# Then once converge has started, both immediate and delayed notifications are called as the resources are converged.
# We want to do this WITHOUT actually converging any resources - we don't want to take time changing the system,
# we just want to make sure the run_context, the notification DSL and the converge hooks are working together
# to perform notifications.
# This test is extremely fragile since it mocks MANY different systems at once - any of them changes, this test
# breaks
describe "Notifications" do
# We always pretend we are on OSx because that has a specific provider (HomebrewProvider) so it
# tests the translation from Provider => HomebrewProvider
let(:node) {
n = Chef::Node.new
n.override[:os] = "darwin"
n
}
let(:cookbook_collection) { double("Chef::CookbookCollection").as_null_object }
let(:events) { double("Chef::EventDispatch::Dispatcher").as_null_object }
let(:run_context) { Chef::RunContext.new(node, cookbook_collection, events) }
let(:recipe) { Chef::Recipe.new("notif", "test", run_context) }
let(:runner) { Chef::Runner.new(run_context) }
before do
# By default, every provider will do nothing
p = Chef::Provider.new(nil, run_context)
allow_any_instance_of(Chef::Resource).to receive(:provider_for_action).and_return(p)
allow(p).to receive(:run_action)
end
it "should subscribe from one resource to another" do
log_resource = recipe.declare_resource(:log, "subscribed-log") do
message "This is a log message"
action :nothing
subscribes :write, "package[vim]", :immediately
end
package_resource = recipe.declare_resource(:package, "vim") do
action :install
end
expect(log_resource).to receive(:run_action).with(:nothing, nil, nil).and_call_original
expect(package_resource).to receive(:run_action).with(:install, nil, nil).and_call_original
update_action(package_resource)
expect(log_resource).to receive(:run_action).with(:write, :immediate, package_resource).and_call_original
runner.converge
end
it "should notify from one resource to another immediately" do
log_resource = recipe.declare_resource(:log, "log") do
message "This is a log message"
action :write
notifies :install, "package[vim]", :immediately
end
package_resource = recipe.declare_resource(:package, "vim") do
action :nothing
end
expect(log_resource).to receive(:run_action).with(:write, nil, nil).and_call_original
update_action(log_resource)
expect(package_resource).to receive(:run_action).with(:install, :immediate, log_resource).ordered.and_call_original
expect(package_resource).to receive(:run_action).with(:nothing, nil, nil).ordered.and_call_original
runner.converge
end
it "should notify from one resource to another before" do
log_resource = recipe.declare_resource(:log, "log") do
message "This is a log message"
action :write
notifies :install, "package[vim]", :before
end
update_action(log_resource, 2)
package_resource = recipe.declare_resource(:package, "vim") do
action :nothing
end
actions = []
[ log_resource, package_resource ].each do |resource|
allow(resource).to receive(:run_action).and_wrap_original do |m, action, notification_type, notifying_resource|
actions << { resource: resource.to_s, action: action }
actions[-1][:why_run] = Chef::Config[:why_run] if Chef::Config[:why_run]
actions[-1][:notification_type] = notification_type if notification_type
actions[-1][:notifying_resource] = notifying_resource.to_s if notifying_resource
m.call(action, notification_type, notifying_resource)
end
end
runner.converge
expect(actions).to eq [
# First it runs why-run to check if the resource would update
{ resource: log_resource.to_s, action: :write, why_run: true },
# Then it runs the before action
{ resource: package_resource.to_s, action: :install, notification_type: :before, notifying_resource: log_resource.to_s },
# Then it runs the actual action
{ resource: log_resource.to_s, action: :write },
{ resource: package_resource.to_s, action: :nothing }
]
end
it "should not notify from one resource to another before if the resource is not updated" do
log_resource = recipe.declare_resource(:log, "log") do
message "This is a log message"
action :write
notifies :install, "package[vim]", :before
end
package_resource = recipe.declare_resource(:package, "vim") do
action :nothing
end
actions = []
[ log_resource, package_resource ].each do |resource|
allow(resource).to receive(:run_action).and_wrap_original do |m, action, notification_type, notifying_resource|
actions << { resource: resource.to_s, action: action }
actions[-1][:why_run] = Chef::Config[:why_run] if Chef::Config[:why_run]
actions[-1][:notification_type] = notification_type if notification_type
actions[-1][:notifying_resource] = notifying_resource.to_s if notifying_resource
m.call(action, notification_type, notifying_resource)
end
end
runner.converge
expect(actions).to eq [
# First it runs why-run to check if the resource would update
{ resource: log_resource.to_s, action: :write, why_run: true },
# Then it does NOT run the before action
# Then it runs the actual action
{ resource: log_resource.to_s, action: :write },
{ resource: package_resource.to_s, action: :nothing }
]
end
it "should notify from one resource to another delayed" do
log_resource = recipe.declare_resource(:log, "log") do
message "This is a log message"
action :write
notifies :install, "package[vim]", :delayed
end
package_resource = recipe.declare_resource(:package, "vim") do
action :nothing
end
expect(log_resource).to receive(:run_action).with(:write, nil, nil).and_call_original
update_action(log_resource)
expect(package_resource).to receive(:run_action).with(:nothing, nil, nil).ordered.and_call_original
expect(package_resource).to receive(:run_action).with(:install, :delayed, nil).ordered.and_call_original
runner.converge
end
describe "when one resource is defined lazily" do
it "subscribes to a resource defined in a ruby block" do
r = recipe
t = self
ruby_block = recipe.declare_resource(:ruby_block, "rblock") do
block do
log_resource = r.declare_resource(:log, "log") do
message "This is a log message"
action :write
end
t.expect(log_resource).to t.receive(:run_action).with(:write, nil, nil).and_call_original
t.update_action(log_resource)
end
end
package_resource = recipe.declare_resource(:package, "vim") do
action :nothing
subscribes :install, "log[log]", :delayed
end
# RubyBlock needs to be able to run for our lazy examples to work - and it alone cannot affect the system
expect(ruby_block).to receive(:provider_for_action).and_call_original
expect(package_resource).to receive(:run_action).with(:nothing, nil, nil).ordered.and_call_original
expect(package_resource).to receive(:run_action).with(:install, :delayed, nil).ordered.and_call_original
runner.converge
end
it "notifies from inside a ruby_block to a resource defined outside" do
r = recipe
t = self
ruby_block = recipe.declare_resource(:ruby_block, "rblock") do
block do
log_resource = r.declare_resource(:log, "log") do
message "This is a log message"
action :write
notifies :install, "package[vim]", :immediately
end
t.expect(log_resource).to t.receive(:run_action).with(:write, nil, nil).and_call_original
t.update_action(log_resource)
end
end
package_resource = recipe.declare_resource(:package, "vim") do
action :nothing
end
# RubyBlock needs to be able to run for our lazy examples to work - and it alone cannot affect the system
expect(ruby_block).to receive(:provider_for_action).and_call_original
expect(package_resource).to receive(:run_action).with(:install, :immediate, instance_of(Chef::Resource::Log)).ordered.and_call_original
expect(package_resource).to receive(:run_action).with(:nothing, nil, nil).ordered.and_call_original
runner.converge
end
end
# Mocks having the provider run successfully and update the resource
def update_action(resource, times=1)
p = Chef::Provider.new(resource, run_context)
expect(resource).to receive(:provider_for_action).exactly(times).times.and_return(p)
expect(p).to receive(:run_action).exactly(times).times {
resource.updated_by_last_action(true)
}
end
end
|