kitchen-scribe 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +1 -0
- data/Gemfile.lock +3 -0
- data/README.md +89 -19
- data/lib/chef/knife/scribe_adjust.rb +325 -0
- data/lib/chef/knife/scribe_copy.rb +4 -4
- data/lib/chef/knife/scribe_hire.rb +2 -2
- data/lib/kitchen-scribe/version.rb +1 -1
- data/spec/chef/knife/scribe_adjust_spec.rb +916 -0
- data/spec/spec_helper.rb +1 -0
- metadata +5 -2
@@ -24,10 +24,10 @@ class Chef
|
|
24
24
|
|
25
25
|
include Chef::Mixin::ShellOut
|
26
26
|
|
27
|
-
DEFAULT_CHRONICLE_PATH = ".chronicle"
|
28
|
-
DEFAULT_REMOTE_NAME = "origin"
|
29
|
-
DEFAULT_BRANCH = "master"
|
30
|
-
DEFAULT_COMMIT_MESSAGE = 'Commiting chef state as of %TIME%'
|
27
|
+
DEFAULT_CHRONICLE_PATH = ".chronicle" unless const_defined?(:DEFAULT_CHRONICLE_PATH)
|
28
|
+
DEFAULT_REMOTE_NAME = "origin" unless const_defined?(:DEFAULT_REMOTE_NAME)
|
29
|
+
DEFAULT_BRANCH = "master" unless const_defined?(:DEFAULT_BRANCH)
|
30
|
+
DEFAULT_COMMIT_MESSAGE = 'Commiting chef state as of %TIME%' unless const_defined?(:DEFAULT_COMMIT_MESSAGE)
|
31
31
|
|
32
32
|
banner "knife scribe copy"
|
33
33
|
|
@@ -25,8 +25,8 @@ class Chef
|
|
25
25
|
|
26
26
|
include Chef::Mixin::ShellOut
|
27
27
|
|
28
|
-
DEFAULT_CHRONICLE_PATH = ".chronicle"
|
29
|
-
DEFAULT_REMOTE_NAME = "origin"
|
28
|
+
DEFAULT_CHRONICLE_PATH = ".chronicle" unless const_defined?(:DEFAULT_CHRONICLE_PATH)
|
29
|
+
DEFAULT_REMOTE_NAME = "origin" unless const_defined?(:DEFAULT_REMOTE_NAME)
|
30
30
|
|
31
31
|
banner "knife scribe hire"
|
32
32
|
|
@@ -0,0 +1,916 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Pawel Kozlowski (<pawel.kozlowski@u2i.com>)
|
3
|
+
# Copyright:: Copyright (c) 2013 Pawel Kozlowski
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require File.expand_path('../../../spec_helper', __FILE__)
|
20
|
+
|
21
|
+
describe Chef::Knife::ScribeAdjust do
|
22
|
+
before(:each) do
|
23
|
+
@scribe = Chef::Knife::ScribeAdjust.new
|
24
|
+
@scribe.stub(:ui).and_return(double("ui", :fatal => nil, :error => nil))
|
25
|
+
end
|
26
|
+
|
27
|
+
it "responds to #action_merge" do
|
28
|
+
@scribe.should respond_to(:action_merge)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "responds to #action_hash_only_merge" do
|
32
|
+
@scribe.should respond_to(:action_hash_only_merge)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "responds to #action_overwrite" do
|
36
|
+
@scribe.should respond_to(:action_overwrite)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "responds to #action_delete" do
|
40
|
+
@scribe.should respond_to(:action_delete)
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#run" do
|
44
|
+
|
45
|
+
describe "when no files were given as parameters" do
|
46
|
+
before(:each) do
|
47
|
+
@scribe.name_args = [ ]
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should show usage and exit if not filename is provided" do
|
51
|
+
@scribe.name_args = []
|
52
|
+
@scribe.ui.should_receive(:fatal).with("At least one adjustment file needs to be specified!")
|
53
|
+
@scribe.should_receive(:show_usage)
|
54
|
+
lambda { @scribe.run }.should raise_error(SystemExit)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "when files were given in parameters" do
|
59
|
+
before(:each) do
|
60
|
+
@scribe.name_args = [ "spec1.json", "spec2.json" ]
|
61
|
+
@scribe.stub(:generate_template)
|
62
|
+
@scribe.stub(:parse_adjustments)
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "when generate option has been provided" do
|
66
|
+
before(:each) do
|
67
|
+
@scribe.config[:generate] = true
|
68
|
+
end
|
69
|
+
|
70
|
+
it "generates adjustment templates for each filename specified" do
|
71
|
+
@scribe.name_args.each { |filename| @scribe.should_receive(:generate_template).with(filename) }
|
72
|
+
@scribe.run
|
73
|
+
end
|
74
|
+
|
75
|
+
it "doesn't call #parse_adjustments" do
|
76
|
+
@scribe.should_not_receive(:parse_adjustments)
|
77
|
+
@scribe.run
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "when generate option hasn't been provided" do
|
82
|
+
before(:each) do
|
83
|
+
@scribe.config[:generate] = false
|
84
|
+
end
|
85
|
+
|
86
|
+
it "doesn't generates adjustment templates for each filename specified" do
|
87
|
+
@scribe.should_not_receive(:generate_template)
|
88
|
+
@scribe.run
|
89
|
+
end
|
90
|
+
|
91
|
+
it "calls #parse_adjustments method" do
|
92
|
+
@scribe.should_receive(:parse_adjustments)
|
93
|
+
@scribe.run
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "#parse_adjustments" do
|
100
|
+
before(:each) do
|
101
|
+
@scribe.stub(:write_adjustments)
|
102
|
+
@scribe.stub(:parse_adjustment_file)
|
103
|
+
@scribe.stub(:hire)
|
104
|
+
@scribe.stub(:record_state)
|
105
|
+
@scribe.stub(:diff)
|
106
|
+
end
|
107
|
+
|
108
|
+
it "initializes error structure for each file" do
|
109
|
+
@scribe.name_args.each { |filename| @scribe.errors.should_receive(:push).with({"name" => filename, "general" => nil, "adjustments" => {}}) }
|
110
|
+
@scribe.parse_adjustments
|
111
|
+
end
|
112
|
+
|
113
|
+
it "parses all adjustments specified" do
|
114
|
+
@scribe.name_args.each { |filename| @scribe.should_receive(:parse_adjustment_file).with(filename) }
|
115
|
+
@scribe.parse_adjustments
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "when dryrun option has been provided" do
|
119
|
+
before(:each) do
|
120
|
+
@scribe.config[:dryrun] = true
|
121
|
+
end
|
122
|
+
|
123
|
+
it "prints a diff of all adjustments" do
|
124
|
+
@scribe.should_receive(:diff)
|
125
|
+
@scribe.parse_adjustments
|
126
|
+
end
|
127
|
+
|
128
|
+
it "doesn't atttempt to writes out adjustments" do
|
129
|
+
@scribe.should_not_receive(:write_adjustments)
|
130
|
+
@scribe.parse_adjustments
|
131
|
+
end
|
132
|
+
|
133
|
+
describe "when errors occured" do
|
134
|
+
before(:each) do
|
135
|
+
@scribe.stub(:errors?).and_return(true)
|
136
|
+
end
|
137
|
+
|
138
|
+
it "prints errors but does not exit" do
|
139
|
+
@scribe.should_receive(:print_errors)
|
140
|
+
lambda { @scribe.parse_adjustments }.should_not raise_error(SystemExit)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
describe "when dryrun option hasn't been provided" do
|
146
|
+
before(:each) do
|
147
|
+
@scribe.config[:dryrun] = false
|
148
|
+
end
|
149
|
+
|
150
|
+
describe "when no errors occured" do
|
151
|
+
before(:each) do
|
152
|
+
@scribe.stub(:errors?).and_return(false)
|
153
|
+
end
|
154
|
+
|
155
|
+
it "doesn't print errors" do
|
156
|
+
@scribe.should_not_receive(:print_errors)
|
157
|
+
@scribe.parse_adjustments
|
158
|
+
end
|
159
|
+
|
160
|
+
it "writes out all adjustments" do
|
161
|
+
@scribe.should_receive(:write_adjustments)
|
162
|
+
@scribe.parse_adjustments
|
163
|
+
end
|
164
|
+
|
165
|
+
describe "when document option has been enabled" do
|
166
|
+
before(:each) do
|
167
|
+
@scribe.config[:document] = true
|
168
|
+
@scribe.descriptions.push("Foo").push("Bar\t\n")
|
169
|
+
end
|
170
|
+
|
171
|
+
it "hires a scribe" do
|
172
|
+
@scribe.should_receive(:hire)
|
173
|
+
@scribe.parse_adjustments
|
174
|
+
end
|
175
|
+
|
176
|
+
it "records the initial and final state of the system with a striped description" do
|
177
|
+
@scribe.should_receive(:record_state).with(no_args()).ordered
|
178
|
+
@scribe.should_receive(:record_state).with("Foo\nBar").ordered
|
179
|
+
@scribe.parse_adjustments
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe "when document option hasn't been enabled" do
|
184
|
+
before(:each) do
|
185
|
+
@scribe.config[:document] = false
|
186
|
+
end
|
187
|
+
|
188
|
+
it "doesn't hire a scribe" do
|
189
|
+
@scribe.should_not_receive(:hire)
|
190
|
+
@scribe.parse_adjustments
|
191
|
+
end
|
192
|
+
|
193
|
+
it "doesn't record the initial and final state of the system" do
|
194
|
+
@scribe.should_not_receive(:record_state)
|
195
|
+
@scribe.parse_adjustments
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
describe "when errors occured" do
|
201
|
+
before(:each) do
|
202
|
+
@scribe.stub(:errors?).and_return(true)
|
203
|
+
end
|
204
|
+
|
205
|
+
it "doesn't write out any adjustments" do
|
206
|
+
@scribe.should_not_receive(:write_adjustments)
|
207
|
+
lambda { @scribe.parse_adjustments }.should raise_error(SystemExit)
|
208
|
+
end
|
209
|
+
|
210
|
+
it "prints errors" do
|
211
|
+
@scribe.should_receive(:print_errors)
|
212
|
+
lambda { @scribe.parse_adjustments }.should raise_error(SystemExit)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
describe "#generate_template" do
|
220
|
+
before(:each) do
|
221
|
+
@f1 = double()
|
222
|
+
@f1.stub(:write)
|
223
|
+
@filename = "spec1.json"
|
224
|
+
end
|
225
|
+
|
226
|
+
describe "when type param is 'enviroment'" do
|
227
|
+
before(:each) do
|
228
|
+
@scribe.config[:type] = "environment"
|
229
|
+
end
|
230
|
+
|
231
|
+
it "saves the environment template JSON into the specified file" do
|
232
|
+
File.should_receive(:open).with(@filename, "w").and_yield(@f1)
|
233
|
+
Chef::Knife::ScribeAdjust::TEMPLATE_HASH["adjustments"] = [Chef::Knife::ScribeAdjust::ENVIRONMENT_ADJUSTMENT_TEMPLATE]
|
234
|
+
@f1.should_receive(:write).with(JSON.pretty_generate(Chef::Knife::ScribeAdjust::TEMPLATE_HASH))
|
235
|
+
@scribe.generate_template @filename
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
describe "when type param is 'node'" do
|
240
|
+
before(:each) do
|
241
|
+
@scribe.config[:type] = "node"
|
242
|
+
end
|
243
|
+
|
244
|
+
it "saves the environment template JSON into the specified file" do
|
245
|
+
File.should_receive(:open).with(@filename, "w").and_yield(@f1)
|
246
|
+
Chef::Knife::ScribeAdjust::TEMPLATE_HASH["adjustments"] = [Chef::Knife::ScribeAdjust::NODE_ADJUSTMENT_TEMPLATE]
|
247
|
+
@f1.should_receive(:write).with(JSON.pretty_generate(Chef::Knife::ScribeAdjust::TEMPLATE_HASH))
|
248
|
+
@scribe.generate_template @filename
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe "when type param is 'role'" do
|
253
|
+
before(:each) do
|
254
|
+
@scribe.config[:type] = "role"
|
255
|
+
end
|
256
|
+
|
257
|
+
it "saves the environment template JSON into the specified file" do
|
258
|
+
File.should_receive(:open).with(@filename, "w").and_yield(@f1)
|
259
|
+
Chef::Knife::ScribeAdjust::TEMPLATE_HASH["adjustments"] = [Chef::Knife::ScribeAdjust::ROLE_ADJUSTMENT_TEMPLATE]
|
260
|
+
@f1.should_receive(:write).with(JSON.pretty_generate(Chef::Knife::ScribeAdjust::TEMPLATE_HASH))
|
261
|
+
@scribe.generate_template @filename
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
describe "when type param is not recognized" do
|
266
|
+
before(:each) do
|
267
|
+
@scribe.config[:type] = "xxxx"
|
268
|
+
end
|
269
|
+
|
270
|
+
it "throws an error through the ui and returns" do
|
271
|
+
@scribe.ui.should_receive(:fatal).with("Incorrect adjustment type! Only 'node', 'environment' or 'role' allowed.")
|
272
|
+
lambda { @scribe.generate_template @filename }.should raise_error(SystemExit)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
describe "record_state" do
|
278
|
+
before(:each) do
|
279
|
+
@copyist_config = double("copyist config", :[]= => nil)
|
280
|
+
@copyist = double("copyist", :run => nil, :config => @copyist_config)
|
281
|
+
@scribe.config[:chronicle_path] = "test_path"
|
282
|
+
@scribe.config[:remote_url] = "test_remote_url"
|
283
|
+
@scribe.config[:remote_name] = "test_remote_name"
|
284
|
+
Chef::Knife::ScribeCopy.stub(:new).and_return(@copyist)
|
285
|
+
end
|
286
|
+
|
287
|
+
describe "when called for the first time" do
|
288
|
+
it "creates and runs a new instance of ScribeCopy" do
|
289
|
+
Chef::Knife::ScribeCopy.should_receive(:new).and_return(@copyist)
|
290
|
+
@copyist.should_receive(:run)
|
291
|
+
@scribe.record_state
|
292
|
+
end
|
293
|
+
|
294
|
+
it "passes all relevant config variables to the hired scribe" do
|
295
|
+
[:chronicle_path, :remote_name, :branch].each { |key| @copyist_config.should_receive(:[]=).with(key, @scribe.config[key]) }
|
296
|
+
@scribe.record_state
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
describe "when not called for the first time" do
|
301
|
+
before(:each) do
|
302
|
+
@scribe.record_state
|
303
|
+
end
|
304
|
+
|
305
|
+
it "creates and runs a new instance of ScribeCopy" do
|
306
|
+
Chef::Knife::ScribeCopy.should_not_receive(:new)
|
307
|
+
@copyist.should_receive(:run)
|
308
|
+
@scribe.record_state
|
309
|
+
end
|
310
|
+
|
311
|
+
it "doesn't reconfigure the copyist" do
|
312
|
+
[:chronicle_path, :remote_name, :branch].each { |key| @copyist_config.should_not_receive(:[]=).with(key, @scribe.config[key]) }
|
313
|
+
@scribe.record_state
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
it "passes its argument as the message for the scribe" do
|
318
|
+
arg = double("argument")
|
319
|
+
@copyist_config.should_receive(:[]=).with(:message, arg)
|
320
|
+
@scribe.record_state(arg)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
describe "hire" do
|
325
|
+
before(:each) do
|
326
|
+
@hired_scribe_config = double("hired scribe config", :[]= => nil)
|
327
|
+
@hired_scribe = double("hired scribe", :run => nil, :config => @hired_scribe_config)
|
328
|
+
@scribe.config[:chronicle_path] = "test_path"
|
329
|
+
@scribe.config[:remote_url] = "test_remote_url"
|
330
|
+
@scribe.config[:remote_name] = "test_remote_name"
|
331
|
+
Chef::Knife::ScribeHire.stub(:new).and_return(@hired_scribe)
|
332
|
+
end
|
333
|
+
|
334
|
+
it "creates and runs a new instance of ScribeHire" do
|
335
|
+
Chef::Knife::ScribeHire.should_receive(:new).and_return(@hired_scribe)
|
336
|
+
@hired_scribe.should_receive(:run)
|
337
|
+
@scribe.hire
|
338
|
+
end
|
339
|
+
|
340
|
+
it "passes all relevant config variables to the hired scribe" do
|
341
|
+
[:chronicle_path, :remote_url, :remote_name].each { |key| @hired_scribe_config.should_receive(:[]=).with(key, @scribe.config[key]) }
|
342
|
+
@scribe.hire
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
describe "#adjustment_file_valid?" do
|
347
|
+
before(:each) do
|
348
|
+
@scribe.errors.push({"name" => "filename", "general" => nil, "adjustments" => {}})
|
349
|
+
end
|
350
|
+
|
351
|
+
describe "when the contants of the file is not a Hash" do
|
352
|
+
it "saves an appropriate general error to the error hash and returns false" do
|
353
|
+
[1,[],nil,"test"].each do |not_a_hash|
|
354
|
+
@scribe.errors.last.should_receive(:[]=).with("general", "Adjustment file must contain a JSON hash!")
|
355
|
+
@scribe.adjustment_file_valid?(not_a_hash).should be_false
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
describe "when the adjustment hash is missing 'adjustments'" do
|
361
|
+
it "saves an appropriate general error to the error hash and returns false" do
|
362
|
+
parsed_file = { "author_email" => "test@mail.com",
|
363
|
+
"author_name" => "test",
|
364
|
+
"description" => "test description"
|
365
|
+
}
|
366
|
+
@scribe.errors.last.should_receive(:[]=).with("general", "Adjustment file must contain an array of adjustments!")
|
367
|
+
@scribe.adjustment_file_valid?(parsed_file).should be_false
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
describe "when the adjustment hash is missing 'adjustments' key or it doesn't point to an array" do
|
372
|
+
it "saves an appropriate general error to the error hash and returns false" do
|
373
|
+
parsed_file = { "author_email" => "test@mail.com",
|
374
|
+
"author_name" => "test",
|
375
|
+
"description" => "test description",
|
376
|
+
"adjustments" => 1
|
377
|
+
}
|
378
|
+
[1,{},nil,"test"].each do |not_an_array|
|
379
|
+
parsed_file["adjustments"] = not_an_array
|
380
|
+
@scribe.errors.last.should_receive(:[]=).with("general", "Adjustment file must contain an array of adjustments!")
|
381
|
+
@scribe.adjustment_file_valid?(parsed_file).should be_false
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
describe "#adjustment_valid?" do
|
388
|
+
before(:each) do
|
389
|
+
@scribe.errors.push({"name" => "filename", "general" => nil, "adjustments" => {}})
|
390
|
+
end
|
391
|
+
|
392
|
+
describe "when the adjustment hash is not a Hash" do
|
393
|
+
it "writes an appropriate adjustment related message into the errors hash and returns false" do
|
394
|
+
[1,[],nil,"test"].each do |not_a_hash|
|
395
|
+
@scribe.errors.last["adjustments"].should_receive(:store).with(0, "Adjustment must be a JSON hash!")
|
396
|
+
@scribe.adjustment_valid?(not_a_hash, 0).should be_false
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
describe "when the adjustment hash is missing a value" do
|
402
|
+
it "writes an appropriate adjustment related message into the errors hash and returns false" do
|
403
|
+
complete_params_hash = { "action" => "merge",
|
404
|
+
"type" => "node",
|
405
|
+
"search" => "name:test",
|
406
|
+
"adjustment" => { }
|
407
|
+
}
|
408
|
+
complete_params_hash.keys.each do |missing_param|
|
409
|
+
incomplete_params_hash = complete_params_hash.clone
|
410
|
+
incomplete_params_hash.delete(missing_param)
|
411
|
+
@scribe.errors.last["adjustments"].should_receive(:store).with(0, "Adjustment hash must contain " + missing_param + "!")
|
412
|
+
@scribe.adjustment_valid?(incomplete_params_hash, 0).should be_false
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
describe "when action is incorrect" do
|
418
|
+
before(:each) do
|
419
|
+
@adjustment_hash = { "action" => "xxxxxx",
|
420
|
+
"type" => "environment",
|
421
|
+
"search" => "test",
|
422
|
+
"adjustment" => { "a" => 1, "b" => 2 }
|
423
|
+
}
|
424
|
+
end
|
425
|
+
|
426
|
+
it "returns false with ui fatal message" do
|
427
|
+
@scribe.should_receive(:respond_to?).with("action_" + @adjustment_hash["action"]).and_return(false)
|
428
|
+
@scribe.errors.last["adjustments"].should_receive(:store).with(0, "Incorrect action!")
|
429
|
+
@scribe.adjustment_valid?(@adjustment_hash, 0).should be_false
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
describe "when action is correct" do
|
434
|
+
before(:each) do
|
435
|
+
@adjustment_hash = { "action" => "merge",
|
436
|
+
"type" => "environment",
|
437
|
+
"search" => "test",
|
438
|
+
"adjustment" => { "a" => 1, "b" => 2 }
|
439
|
+
}
|
440
|
+
end
|
441
|
+
|
442
|
+
it "returns true without any ui message" do
|
443
|
+
@scribe.should_receive(:respond_to?).with("action_" + @adjustment_hash["action"]).and_return(true)
|
444
|
+
@scribe.errors.last["adjustments"].should_not_receive(:store).with(0, "Incorrect action!")
|
445
|
+
@scribe.adjustment_valid?(@adjustment_hash, 0).should be_true
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
describe "#write_adjustments" do
|
451
|
+
before(:each) do
|
452
|
+
@adjusted_env = double("adjusted environment")
|
453
|
+
@adjusted_env.stub(:[]).with("chef_type").and_return("environment")
|
454
|
+
@adjusted_node = double("adjusted node")
|
455
|
+
@adjusted_node.stub(:[]).with("chef_type").and_return("node")
|
456
|
+
@scribe.changes["environment:test_env"] = {
|
457
|
+
"original" => { "chef_type" => "environment", "name" => "test_env" },
|
458
|
+
"adjusted" => @adjusted_env
|
459
|
+
}
|
460
|
+
@scribe.changes["node:test_node"] = {
|
461
|
+
"original" => { "chef_type" => "node", "name" => "test_node" },
|
462
|
+
"adjusted" => @adjusted_node
|
463
|
+
}
|
464
|
+
@env_class = double("env class")
|
465
|
+
@node_class = double("node class")
|
466
|
+
@env_object = double("env object")
|
467
|
+
@node_object = double("node_object")
|
468
|
+
end
|
469
|
+
|
470
|
+
it "saves each adjusted version on the chef server" do
|
471
|
+
Chef.should_receive(:const_get).with("Environment").and_return(@env_class)
|
472
|
+
@env_class.should_receive(:json_create).with(@adjusted_env).and_return(@env_object)
|
473
|
+
@env_object.should_receive(:save)
|
474
|
+
Chef.should_receive(:const_get).with("Node").and_return(@node_class)
|
475
|
+
@node_class.should_receive(:json_create).with(@adjusted_node).and_return(@node_object)
|
476
|
+
@node_object.should_receive(:save)
|
477
|
+
@scribe.write_adjustments
|
478
|
+
end
|
479
|
+
end
|
480
|
+
|
481
|
+
describe "#parse_adjustment_file" do
|
482
|
+
before(:each) do
|
483
|
+
@filename = "spec1.json"
|
484
|
+
@file = double("adjustment file")
|
485
|
+
File.stub(:open).and_yield(@file)
|
486
|
+
File.stub(:open).with(@filename, "r").and_yield(@file)
|
487
|
+
@adjustment_hash = { "author_name" => "test",
|
488
|
+
"author_email" => "test@test.com",
|
489
|
+
"description" => "test description",
|
490
|
+
"adjustments" => [{ "action" => "BAD_ACTION",
|
491
|
+
"type" => "environment",
|
492
|
+
"search" => "test",
|
493
|
+
"adjustment" => { "a" => 1, "b" => 2 }
|
494
|
+
},
|
495
|
+
{ "action" => "delete",
|
496
|
+
"type" => "node",
|
497
|
+
"search" => "foo:bar",
|
498
|
+
"adjustment" => [ "c" ]
|
499
|
+
}
|
500
|
+
]
|
501
|
+
}
|
502
|
+
File.stub(:exists?).and_return(true)
|
503
|
+
@scribe.stub(:apply_adjustment)
|
504
|
+
@scribe.errors.push({"name" => @filename, "general" => nil, "adjustments" => {}})
|
505
|
+
end
|
506
|
+
|
507
|
+
it "checks if the file exists" do
|
508
|
+
File.should_receive(:exists?).with(@filename).and_return(false)
|
509
|
+
@scribe.parse_adjustment_file(@filename)
|
510
|
+
end
|
511
|
+
|
512
|
+
describe "when the file does not exist" do
|
513
|
+
before(:each) do
|
514
|
+
File.stub(:exists?).and_return(false)
|
515
|
+
end
|
516
|
+
|
517
|
+
it "writes a general error into the errors hash" do
|
518
|
+
@scribe.errors.last.should_receive(:[]=).with("general", "File does not exist!")
|
519
|
+
@scribe.parse_adjustment_file(@filename)
|
520
|
+
end
|
521
|
+
|
522
|
+
it "doesn't attempt to open the file" do
|
523
|
+
File.should_not_receive(:open).with(@filename)
|
524
|
+
@scribe.parse_adjustment_file(@filename)
|
525
|
+
end
|
526
|
+
end
|
527
|
+
|
528
|
+
it "parses the adjustment file" do
|
529
|
+
File.should_receive(:open).with(@filename, "r").and_yield(@file)
|
530
|
+
JSON.should_receive(:load).with(@file).and_return(@adjustment_hash)
|
531
|
+
@scribe.parse_adjustment_file(@filename)
|
532
|
+
end
|
533
|
+
|
534
|
+
describe "when the JSON file is malformed" do
|
535
|
+
it "returns writes a fatal error through the ui" do
|
536
|
+
@scribe.errors.last.should_receive(:[]=).with("general", "Malformed JSON!")
|
537
|
+
@scribe.parse_adjustment_file(@filename)
|
538
|
+
end
|
539
|
+
|
540
|
+
it "doesn't throw an exception" do
|
541
|
+
File.should_receive(:open).with(@filename, "r").and_yield('{"a" : 3, b => ]')
|
542
|
+
lambda { @scribe.parse_adjustment_file(@filename) }.should_not raise_error(JSON::ParserError)
|
543
|
+
end
|
544
|
+
end
|
545
|
+
|
546
|
+
describe "when the file exists and is well formed" do
|
547
|
+
before(:each) do
|
548
|
+
JSON.stub(:load).and_return(@adjustment_hash)
|
549
|
+
end
|
550
|
+
|
551
|
+
it "checks if the adjustment file is valid" do
|
552
|
+
@scribe.should_receive(:adjustment_file_valid?).with(@adjustment_hash).and_return(false)
|
553
|
+
@scribe.parse_adjustment_file(@filename)
|
554
|
+
end
|
555
|
+
|
556
|
+
describe "if the adjustment file is correct" do
|
557
|
+
it "applies each adjustment if it's correct'" do
|
558
|
+
@scribe.should_receive(:adjustment_valid?).with(@adjustment_hash["adjustments"][0], 0).and_return(false)
|
559
|
+
@scribe.should_receive(:adjustment_valid?).with(@adjustment_hash["adjustments"][1], 1).and_return(true)
|
560
|
+
@scribe.should_receive(:apply_adjustment).with(@adjustment_hash["adjustments"][1])
|
561
|
+
@scribe.parse_adjustment_file(@filename)
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
describe "if all adjustments are correct" do
|
566
|
+
before(:each) do
|
567
|
+
@scribe.stub(:adjustment_valid?).and_return(true)
|
568
|
+
end
|
569
|
+
|
570
|
+
it "adds the description to the descriptions array" do
|
571
|
+
@scribe.descriptions.should_receive(:push).with(@adjustment_hash["description"])
|
572
|
+
@scribe.parse_adjustment_file(@filename)
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
|
577
|
+
describe "if at least one adjustment was correct" do
|
578
|
+
before(:each) do
|
579
|
+
@scribe.stub(:adjustment_valid?).and_return(true,false)
|
580
|
+
@scribe.errors.last["adjustments"].store(1, "Foo")
|
581
|
+
end
|
582
|
+
|
583
|
+
it "adds the description to the descriptions array" do
|
584
|
+
@scribe.descriptions.should_receive(:push).with(@adjustment_hash["description"] + "[with errors]")
|
585
|
+
@scribe.parse_adjustment_file(@filename)
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
describe "if no adjustment was correct" do
|
590
|
+
before(:each) do
|
591
|
+
@scribe.stub(:adjustment_valid?).and_return(false,false)
|
592
|
+
@scribe.errors.last["adjustments"].store(0, "Foo")
|
593
|
+
@scribe.errors.last["adjustments"].store(1, "Bar")
|
594
|
+
end
|
595
|
+
|
596
|
+
it "doesn't add the description to the descriptions array" do
|
597
|
+
@scribe.descriptions.should_not_receive(:push).with(@adjustment_hash["description"])
|
598
|
+
@scribe.parse_adjustment_file(@filename)
|
599
|
+
end
|
600
|
+
end
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
describe "#apply_adjustment" do
|
605
|
+
before(:each) do
|
606
|
+
@adjustment = { "action" => "merge",
|
607
|
+
"type" => "environment",
|
608
|
+
"search" => "test",
|
609
|
+
"adjustment" => { "a" => 1, "b" => 2 }
|
610
|
+
}
|
611
|
+
|
612
|
+
@scribe.stub(:adjustment_valid?).and_return(true)
|
613
|
+
@query = double("Chef query")
|
614
|
+
Chef::Search::Query.stub(:new).and_return(@query)
|
615
|
+
@chef_obj = double("chef_object")
|
616
|
+
@chef_obj.stub(:to_hash).and_return( { "name" => "test_name", "chef_type" => "test_type", "a" => 3, "c" => 3 } )
|
617
|
+
chef_obj_class = double("chef_object_class")
|
618
|
+
json_create_return_obj = double("final_chef_object")
|
619
|
+
json_create_return_obj.stub(:save)
|
620
|
+
chef_obj_class.stub(:json_create).and_return(json_create_return_obj)
|
621
|
+
@chef_obj.stub(:class).and_return(chef_obj_class)
|
622
|
+
@query.stub(:search).and_yield(@chef_obj)
|
623
|
+
end
|
624
|
+
|
625
|
+
describe "when search parameter doesn't contain a ':' character" do
|
626
|
+
before(:each) do
|
627
|
+
@adjustment["search"] = "test_name"
|
628
|
+
end
|
629
|
+
|
630
|
+
it "performs a search using the 'search' parameter as a name" do
|
631
|
+
@query.should_receive(:search).with(@adjustment["type"], "name:" + @adjustment["search"])
|
632
|
+
@scribe.apply_adjustment(@adjustment)
|
633
|
+
end
|
634
|
+
end
|
635
|
+
|
636
|
+
describe "when search parameter contains a ':' character" do
|
637
|
+
before(:each) do
|
638
|
+
@adjustment["search"] = "foo:test_name"
|
639
|
+
end
|
640
|
+
|
641
|
+
it "performs a search using the 'search' parameter as a complete query" do
|
642
|
+
@query.should_receive(:search).with(@adjustment["type"], @adjustment["search"])
|
643
|
+
@scribe.apply_adjustment(@adjustment)
|
644
|
+
end
|
645
|
+
end
|
646
|
+
|
647
|
+
it "checks if the a change to a given object is already pending" do
|
648
|
+
@scribe.changes.should_receive(:has_key?).with(@chef_obj.to_hash["chef_type"] + ":" + @chef_obj.to_hash["name"])
|
649
|
+
@scribe.apply_adjustment(@adjustment)
|
650
|
+
end
|
651
|
+
|
652
|
+
describe "if the key doesn't exist" do
|
653
|
+
it "saves the original in the changes hash" do
|
654
|
+
@scribe.changes.should_receive(:store).with(@chef_obj.to_hash["chef_type"] + ":" + @chef_obj.to_hash["name"],
|
655
|
+
{ "original" => @chef_obj.to_hash }
|
656
|
+
).and_call_original
|
657
|
+
@scribe.apply_adjustment(@adjustment)
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
it "applies each subsequent adjustment to the already adjusted version" do
|
662
|
+
adjusted_hash = @chef_obj.to_hash.dup.merge({"a" => "b"})
|
663
|
+
changes_hash = { "original" => @chef_obj.to_hash, "adjusted" => adjusted_hash }
|
664
|
+
@scribe.changes[@chef_obj.to_hash["chef_type"] + ":" + @chef_obj.to_hash["name"]] = changes_hash
|
665
|
+
@scribe.should_receive(("action_" + @adjustment["action"]).to_sym).with(adjusted_hash, @adjustment["adjustment"])
|
666
|
+
@scribe.apply_adjustment(@adjustment)
|
667
|
+
end
|
668
|
+
|
669
|
+
it "saves the changed version in the changes hash" do
|
670
|
+
adjusted_hash = @chef_obj.to_hash.dup.merge({ "a" => "b" })
|
671
|
+
changes_hash = { "original" => @chef_obj.to_hash, "adjusted" => adjusted_hash }
|
672
|
+
@scribe.changes[@chef_obj.to_hash["chef_type"] + ":" + @chef_obj.to_hash["name"]] = changes_hash
|
673
|
+
adjusted_hash = @scribe.send(("action_" + @adjustment["action"]).to_sym, adjusted_hash, @adjustment["adjustment"])
|
674
|
+
changes_hash.should_receive(:store).with("adjusted", adjusted_hash).and_call_original
|
675
|
+
@scribe.apply_adjustment(@adjustment)
|
676
|
+
end
|
677
|
+
end
|
678
|
+
|
679
|
+
describe "#action_overwrite" do
|
680
|
+
it "performs a standard hash merge when both base and overwrite_with are hashes" do
|
681
|
+
base = { "a" => 1, "b" => [1,2,3], "c" => { "x" => 1, "y" => 2 } }
|
682
|
+
overwrite_with = { "b" => [4], "c" => { "z" => 1, "y" => 3}, "d" => 3 }
|
683
|
+
base.should_receive(:merge).with(overwrite_with)
|
684
|
+
@scribe.action_overwrite(base,overwrite_with)
|
685
|
+
end
|
686
|
+
|
687
|
+
it "returns base hash if overwrite_with is nil" do
|
688
|
+
base = {"foo" => "bar"}
|
689
|
+
overwrite_with = nil
|
690
|
+
result = @scribe.action_overwrite(base,overwrite_with)
|
691
|
+
result.should eq(base)
|
692
|
+
end
|
693
|
+
|
694
|
+
it "returns the overwrite if base is not a hash" do
|
695
|
+
base = "test"
|
696
|
+
overwrite_with = {"a" => 1}
|
697
|
+
result = @scribe.action_overwrite(base,overwrite_with)
|
698
|
+
result.should eq(overwrite_with)
|
699
|
+
end
|
700
|
+
end
|
701
|
+
|
702
|
+
describe "#deep_delete" do
|
703
|
+
it "calls #seedp_delete! with duplicates of it's arguments" do
|
704
|
+
delete_from = double("delete_from")
|
705
|
+
delete_spec = double("delete_spec")
|
706
|
+
delete_from_dup = double("delete_from_dup")
|
707
|
+
delete_spec_dup = double("delete_spec_dup")
|
708
|
+
delete_from.should_receive(:dup).and_return(delete_from_dup)
|
709
|
+
delete_spec.should_receive(:dup).and_return(delete_spec_dup)
|
710
|
+
@scribe.should_receive(:deep_delete!).with(delete_from_dup, delete_spec_dup)
|
711
|
+
@scribe.deep_delete(delete_from, delete_spec)
|
712
|
+
end
|
713
|
+
end
|
714
|
+
|
715
|
+
describe "#deep_delete!" do
|
716
|
+
describe "when both base and overwrite_with are hashes" do
|
717
|
+
before(:each) do
|
718
|
+
@delete_from = { "a" => 1, "b" => [3,2,1], "c" => { "x" => 1, "y" => 2 } }
|
719
|
+
end
|
720
|
+
|
721
|
+
describe "when the spec intructs it to delete a top level key" do
|
722
|
+
before(:each) do
|
723
|
+
@delete_spec = "c"
|
724
|
+
end
|
725
|
+
|
726
|
+
it "deletes it" do
|
727
|
+
@scribe.deep_delete!(@delete_from,@delete_spec).keys.should_not include("c")
|
728
|
+
end
|
729
|
+
end
|
730
|
+
|
731
|
+
describe "when the spec intructs it to delete a nested key" do
|
732
|
+
before(:each) do
|
733
|
+
@delete_spec = { "c" => "x" }
|
734
|
+
end
|
735
|
+
|
736
|
+
it "doesn't delete the top level key" do
|
737
|
+
@scribe.deep_delete!(@delete_from,@delete_spec).keys.should include("c")
|
738
|
+
end
|
739
|
+
|
740
|
+
it "deletes it" do
|
741
|
+
@scribe.deep_delete!(@delete_from,@delete_spec)["c"].keys.should_not include("x")
|
742
|
+
end
|
743
|
+
end
|
744
|
+
|
745
|
+
describe "when the spec intructs it to delete an array key that exists" do
|
746
|
+
before(:each) do
|
747
|
+
@delete_spec = { "b" => 1 }
|
748
|
+
end
|
749
|
+
|
750
|
+
it "deletes it" do
|
751
|
+
@scribe.deep_delete!(@delete_from,@delete_spec)["b"].should eq([3,1])
|
752
|
+
end
|
753
|
+
end
|
754
|
+
|
755
|
+
describe "when the spec intructs it to delete an array key that doesn't exist" do
|
756
|
+
before(:each) do
|
757
|
+
@delete_spec = { "b" => 10 }
|
758
|
+
end
|
759
|
+
|
760
|
+
it "does nothing" do
|
761
|
+
@scribe.deep_delete!(@delete_from,@delete_spec)["b"].should eq(@delete_from["b"])
|
762
|
+
end
|
763
|
+
end
|
764
|
+
|
765
|
+
describe "when the spec intructs it to delete a hash key that doesn't exist" do
|
766
|
+
before(:each) do
|
767
|
+
@delete_spec = { "c" => { "not_here" => [1,2] } }
|
768
|
+
end
|
769
|
+
|
770
|
+
it "does nothing" do
|
771
|
+
@scribe.deep_delete!(@delete_from,@delete_spec)["c"].should eq(@delete_from["c"])
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
describe "when the spec intructs it to delete a set of nested keys" do
|
776
|
+
before(:each) do
|
777
|
+
@delete_spec = { "c" => ["x", "y"] }
|
778
|
+
end
|
779
|
+
|
780
|
+
it "doesn't delete the top level key" do
|
781
|
+
@scribe.deep_delete!(@delete_from,@delete_spec).keys.should include("c")
|
782
|
+
end
|
783
|
+
|
784
|
+
it "deletes both of them" do
|
785
|
+
@scribe.deep_delete!(@delete_from,@delete_spec)["c"].keys.should_not include("x","y")
|
786
|
+
end
|
787
|
+
end
|
788
|
+
|
789
|
+
describe "when the spec intructs it to delete a set of top level keys" do
|
790
|
+
before(:each) do
|
791
|
+
@delete_spec = [ "b", "c"]
|
792
|
+
end
|
793
|
+
|
794
|
+
it "deletes both of them" do
|
795
|
+
@scribe.deep_delete!(@delete_from,@delete_spec).keys.should_not include("b","c")
|
796
|
+
end
|
797
|
+
end
|
798
|
+
|
799
|
+
end
|
800
|
+
|
801
|
+
it "returns delete_from if delete_spec is nil" do
|
802
|
+
delete_from = {"foo" => "bar"}
|
803
|
+
delete_spec = nil
|
804
|
+
result = @scribe.deep_delete!(delete_from,delete_spec)
|
805
|
+
result.should eq(delete_from)
|
806
|
+
end
|
807
|
+
|
808
|
+
it "returns delete_from if delete_from is not a hash or an array" do
|
809
|
+
base = "test"
|
810
|
+
overwrite_with = {"a" => 1}
|
811
|
+
result = @scribe.action_overwrite(base,overwrite_with)
|
812
|
+
result.should eq(overwrite_with)
|
813
|
+
end
|
814
|
+
end
|
815
|
+
|
816
|
+
describe "#errors?" do
|
817
|
+
describe "when no errors have occured" do
|
818
|
+
before(:each) do
|
819
|
+
for i in 1..3
|
820
|
+
@scribe.errors.push({"name" => "filename" + i.to_s, "general" => nil, "adjustments" => {}})
|
821
|
+
end
|
822
|
+
end
|
823
|
+
|
824
|
+
it "returns false" do
|
825
|
+
@scribe.errors?.should be_false
|
826
|
+
end
|
827
|
+
end
|
828
|
+
|
829
|
+
describe "when general error has occured" do
|
830
|
+
before(:each) do
|
831
|
+
for i in 1..3
|
832
|
+
@scribe.errors.push({"name" => "filename" + i.to_s, "general" => nil, "adjustments" => {}})
|
833
|
+
end
|
834
|
+
@scribe.errors.push({"name" => "filename4", "general" => "foo", "adjustments" => {}})
|
835
|
+
end
|
836
|
+
|
837
|
+
it "returns true" do
|
838
|
+
@scribe.errors?.should be_true
|
839
|
+
end
|
840
|
+
end
|
841
|
+
|
842
|
+
describe "when an adjustment specific error has occured" do
|
843
|
+
before(:each) do
|
844
|
+
@scribe.errors.push({"name" => "filename0", "general" => nil, "adjustments" => { 2 => "bar"}})
|
845
|
+
for i in 1..3
|
846
|
+
@scribe.errors.push({"name" => "filename" + i.to_s, "general" => nil, "adjustments" => {}})
|
847
|
+
end
|
848
|
+
end
|
849
|
+
|
850
|
+
it "returns true" do
|
851
|
+
@scribe.errors?.should be_true
|
852
|
+
end
|
853
|
+
end
|
854
|
+
end
|
855
|
+
|
856
|
+
describe "#print_errors" do
|
857
|
+
it "prints all the errors in the right format" do
|
858
|
+
@scribe.errors.push({"name" => "filename1", "general" => nil, "adjustments" => {}})
|
859
|
+
@scribe.errors.push({"name" => "filename2", "general" => nil, "adjustments" => { 2 => "bar"}})
|
860
|
+
@scribe.errors.push({"name" => "filename3", "general" => "Foo", "adjustments" => {}})
|
861
|
+
@scribe.ui.should_receive(:error).with("ERRORS OCCURED:")
|
862
|
+
@scribe.ui.should_receive(:error).with("filename2")
|
863
|
+
@scribe.ui.should_receive(:error).with("\t[Adjustment 2]: bar")
|
864
|
+
@scribe.ui.should_receive(:error).with("filename3")
|
865
|
+
@scribe.ui.should_receive(:error).with("\tFoo")
|
866
|
+
@scribe.print_errors
|
867
|
+
end
|
868
|
+
end
|
869
|
+
|
870
|
+
describe "#diff" do
|
871
|
+
before(:each) do
|
872
|
+
@scribe.changes["environment:test_env"] = {
|
873
|
+
"original" => { "chef_type" => "environment", "name" => "test_env", "default_attributes" => { "foo" => "bar" }},
|
874
|
+
"adjusted" => { "chef_type" => "environment", "name" => "test_env", "default_attributes" => {}}
|
875
|
+
}
|
876
|
+
@scribe.changes["node:test_node"] = {
|
877
|
+
"original" => { "chef_type" => "node", "name" => "test_node" , "attributes" => { "foo" => "bar" }},
|
878
|
+
"adjusted" => { "chef_type" => "node", "name" => "test_node" , "attributes" => { "foo" => "bar2" }}
|
879
|
+
}
|
880
|
+
@original_file = double("original file", :write => nil, :close => nil, :unlink => nil, :path => "original_path", :rewind => nil)
|
881
|
+
@adjusted_file = double("adjusted file", :write => nil, :close => nil, :unlink => nil, :path => "adjusted_path", :rewind => nil)
|
882
|
+
@scribe.ui.stub(:info)
|
883
|
+
@scribe.stub(:shell_out).and_return(double("command", :stdout => "aaa"))
|
884
|
+
end
|
885
|
+
|
886
|
+
it "creates and then deletes two temp files" do
|
887
|
+
Tempfile.should_receive(:new).with("original").and_return(@original_file)
|
888
|
+
Tempfile.should_receive(:new).with("adjusted").and_return(@adjusted_file)
|
889
|
+
@original_file.should_receive(:close)
|
890
|
+
@original_file.should_receive(:unlink)
|
891
|
+
@adjusted_file.should_receive(:close)
|
892
|
+
@adjusted_file.should_receive(:unlink)
|
893
|
+
@scribe.diff
|
894
|
+
end
|
895
|
+
|
896
|
+
describe "for each changed object" do
|
897
|
+
it "saves both original and adjusted hashes to the tempfiles" do
|
898
|
+
Tempfile.stub(:new).with("original").and_return(@original_file)
|
899
|
+
Tempfile.stub(:new).with("adjusted").and_return(@adjusted_file)
|
900
|
+
@scribe.changes.values.each do |change|
|
901
|
+
@original_file.should_receive(:write).with(JSON.pretty_generate(change["original"]))
|
902
|
+
@adjusted_file.should_receive(:write).with(JSON.pretty_generate(change["adjusted"]))
|
903
|
+
end
|
904
|
+
@scribe.diff
|
905
|
+
end
|
906
|
+
|
907
|
+
it "runs a diff on both files" do
|
908
|
+
@scribe.ui.should_receive(:info).with("[environment:test_env]")
|
909
|
+
@scribe.ui.should_receive(:info).with("aaa").twice
|
910
|
+
@scribe.ui.should_receive(:info).with("[node:test_node]")
|
911
|
+
@scribe.diff
|
912
|
+
end
|
913
|
+
end
|
914
|
+
end
|
915
|
+
|
916
|
+
end
|