chef-dk 0.10.0 → 0.11.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -0
  3. data/README.md +19 -4
  4. data/Rakefile +9 -0
  5. data/chef-dk.gemspec +3 -1
  6. data/lib/chef-dk/chef_runner.rb +9 -0
  7. data/lib/chef-dk/command/export.rb +6 -0
  8. data/lib/chef-dk/command/generator_commands.rb +3 -3
  9. data/lib/chef-dk/command/generator_commands/base.rb +27 -0
  10. data/lib/chef-dk/command/update.rb +19 -0
  11. data/lib/chef-dk/configurable.rb +13 -1
  12. data/lib/chef-dk/exceptions.rb +3 -0
  13. data/lib/chef-dk/policyfile/cookbook_location_specification.rb +13 -0
  14. data/lib/chef-dk/policyfile/cookbook_locks.rb +1 -1
  15. data/lib/chef-dk/policyfile/dsl.rb +40 -2
  16. data/lib/chef-dk/policyfile_compiler.rb +43 -4
  17. data/lib/chef-dk/policyfile_services/export_repo.rb +156 -51
  18. data/lib/chef-dk/policyfile_services/install.rb +1 -0
  19. data/lib/chef-dk/policyfile_services/push_archive.rb +33 -2
  20. data/lib/chef-dk/skeletons/code_generator/files/default/chefignore +7 -5
  21. data/lib/chef-dk/skeletons/code_generator/files/default/repo/policies/README.md +1 -1
  22. data/lib/chef-dk/version.rb +1 -1
  23. data/lib/kitchen/provisioner/policyfile_zero.rb +8 -3
  24. data/spec/shared/custom_generator_cookbook.rb +15 -2
  25. data/spec/unit/chef_runner_spec.rb +28 -0
  26. data/spec/unit/command/export_spec.rb +11 -0
  27. data/spec/unit/command/generator_commands/base_spec.rb +136 -0
  28. data/spec/unit/command/update_spec.rb +24 -0
  29. data/spec/unit/configurable_spec.rb +41 -0
  30. data/spec/unit/fixtures/configurable/test_config_loader.rb +5 -0
  31. data/spec/unit/fixtures/configurable/test_configurable.rb +10 -0
  32. data/spec/unit/policyfile/cookbook_location_specification_spec.rb +21 -1
  33. data/spec/unit/policyfile/cookbook_locks_spec.rb +1 -1
  34. data/spec/unit/policyfile_demands_spec.rb +206 -0
  35. data/spec/unit/policyfile_evaluation_spec.rb +85 -0
  36. data/spec/unit/policyfile_lock_serialization_spec.rb +1 -1
  37. data/spec/unit/policyfile_services/export_repo_spec.rb +78 -36
  38. data/spec/unit/policyfile_services/install_spec.rb +20 -0
  39. data/spec/unit/policyfile_services/push_archive_spec.rb +41 -8
  40. metadata +27 -11
@@ -81,6 +81,34 @@ describe ChefDK::ChefRunner do
81
81
  expect(test_state[:converged_recipes]).to eq([ "recipe_one", "recipe_two" ])
82
82
  end
83
83
 
84
+ context "when policyfile options are set in the workstation config" do
85
+
86
+ before do
87
+ Chef::Config.use_policyfile true
88
+ Chef::Config.policy_name "workstation"
89
+ Chef::Config.policy_group "test"
90
+
91
+ # chef-client ignores `deployment_group` unless
92
+ # `policy_document_native_api` is set to false
93
+ Chef::Config.deployment_group "workstation-test"
94
+ Chef::Config.policy_document_native_api false
95
+ end
96
+
97
+ it "unsets the options" do
98
+ chef_runner.configure
99
+
100
+ expect(Chef::Config.use_policyfile).to be(false)
101
+ expect(Chef::Config.policy_name).to be_nil
102
+ expect(Chef::Config.policy_group).to be_nil
103
+ expect(Chef::Config.deployment_group).to be_nil
104
+ end
105
+
106
+ it "converges successfully" do
107
+ expect { chef_runner.converge }.to_not raise_error
108
+ end
109
+
110
+ end
111
+
84
112
  context "when the embedded chef run fails" do
85
113
 
86
114
  let(:embedded_runner) { instance_double("Chef::Runner") }
@@ -128,6 +128,17 @@ describe ChefDK::Command::Export do
128
128
  it "returns 0" do
129
129
  expect(command.run(params)).to eq(0)
130
130
  end
131
+
132
+ it "prints instructions for running chef-client in the repo" do
133
+ command.run(params)
134
+
135
+ expected_message = <<-MESSAGE
136
+ To converge this system with the exported policy, run:
137
+ cd /path/to/export
138
+ chef-client -z
139
+ MESSAGE
140
+ expect(ui.output).to include(expected_message)
141
+ end
131
142
  end
132
143
 
133
144
  context "when the command is unsuccessful" do
@@ -0,0 +1,136 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2014 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'pry'
19
+ require 'spec_helper'
20
+ require 'chef-dk/command/generator_commands/base'
21
+
22
+ describe ChefDK::Command::GeneratorCommands::Base do
23
+ describe 'parsing Chef configuration' do
24
+ let(:cli_args) do
25
+ [
26
+ "-C", "Business Man",
27
+ "-I", "Serious Business",
28
+ "-m", "business.man@corporation.com"
29
+ ]
30
+ end
31
+
32
+ before do
33
+ Chef::Config.reset
34
+ end
35
+
36
+ context 'when generator configuration is defined' do
37
+ before do
38
+ Chef::Config.reset
39
+ Chef::Config.chefdk.generator.copyright_holder = "This Guy"
40
+ Chef::Config.chefdk.generator.email = "this.guy@twothumbs.net"
41
+ Chef::Config.chefdk.generator.license = "Two Thumbs License"
42
+ end
43
+
44
+ it 'uses the defined values' do
45
+ cmd = ChefDK::Command::GeneratorCommands::Base.new([])
46
+ cmd.parse_options
47
+ cmd.setup_context
48
+ cfg = cmd.config
49
+ expect(cfg[:copyright_holder]).to eq('This Guy')
50
+ expect(cfg[:email]).to eq('this.guy@twothumbs.net')
51
+ expect(cfg[:license]).to eq('Two Thumbs License')
52
+ end
53
+
54
+ context 'when cli overrides are provided' do
55
+ before do
56
+ Chef::Config.reset
57
+ Chef::Config.chefdk.generator.copyright_holder = "This Guy"
58
+ Chef::Config.chefdk.generator.email = "this.guy@twothumbs.net"
59
+ Chef::Config.chefdk.generator.license = "Two Thumbs License"
60
+ end
61
+
62
+ it 'uses the cli args' do
63
+ cmd = ChefDK::Command::GeneratorCommands::Base.new(cli_args)
64
+ cmd.parse_options(cli_args)
65
+ cmd.setup_context
66
+ cfg = cmd.config
67
+ expect(cfg[:copyright_holder]).to eq('Business Man')
68
+ expect(cfg[:email]).to eq('business.man@corporation.com')
69
+ expect(cfg[:license]).to eq('Serious Business')
70
+ end
71
+ end
72
+
73
+ context 'when knife configuration is also defined' do
74
+
75
+ before do
76
+ Chef::Config.reset
77
+ Chef::Config.chefdk.generator.copyright_holder = "This Guy"
78
+ Chef::Config.chefdk.generator.email = "this.guy@twothumbs.net"
79
+ Chef::Config.chefdk.generator.license = "Two Thumbs License"
80
+ Chef::Config.knife.cookbook_copyright = "Knife User"
81
+ Chef::Config.knife.cookbook_email = "knife.user@example.com"
82
+ Chef::Config.knife.cookbook_license = "GPLv9000"
83
+ end
84
+
85
+ it 'uses the generator configuration' do
86
+ cmd = ChefDK::Command::GeneratorCommands::Base.new([])
87
+ cmd.parse_options
88
+ cmd.setup_context
89
+ cfg = cmd.config
90
+ expect(cfg[:copyright_holder]).to eq('This Guy')
91
+ expect(cfg[:email]).to eq('this.guy@twothumbs.net')
92
+ expect(cfg[:license]).to eq('Two Thumbs License')
93
+ end
94
+ end
95
+ end
96
+
97
+ context 'when knife configuration is defined' do
98
+ before do
99
+ Chef::Config.reset
100
+ Chef::Config.knife.cookbook_copyright = "Knife User"
101
+ Chef::Config.knife.cookbook_email = "knife.user@example.com"
102
+ Chef::Config.knife.cookbook_license = "GPLv9000"
103
+ end
104
+
105
+ it 'uses the defined values' do
106
+ cmd = ChefDK::Command::GeneratorCommands::Base.new([])
107
+ cmd.parse_options
108
+ cmd.setup_context
109
+ cfg = cmd.config
110
+ expect(cfg[:copyright_holder]).to eq('Knife User')
111
+ expect(cfg[:email]).to eq('knife.user@example.com')
112
+ expect(cfg[:license]).to eq('GPLv9000')
113
+ end
114
+
115
+ context 'when cli overrides are provided' do
116
+
117
+ before do
118
+ Chef::Config.reset
119
+ Chef::Config.knife.cookbook_copyright = "Knife User"
120
+ Chef::Config.knife.cookbook_email = "knife.user@example.com"
121
+ Chef::Config.knife.cookbook_license = "GPLv9000"
122
+ end
123
+
124
+ it 'uses the cli args' do
125
+ cmd = ChefDK::Command::GeneratorCommands::Base.new(cli_args)
126
+ cmd.parse_options(cli_args)
127
+ cmd.setup_context
128
+ cfg = cmd.config
129
+ expect(cfg[:copyright_holder]).to eq('Business Man')
130
+ expect(cfg[:email]).to eq('business.man@corporation.com')
131
+ expect(cfg[:license]).to eq('Serious Business')
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -39,6 +39,10 @@ describe ChefDK::Command::Update do
39
39
  expect(command.debug?).to be(false)
40
40
  end
41
41
 
42
+ it "doesn't set a config path by default" do
43
+ expect(command.config_path).to be_nil
44
+ end
45
+
42
46
  context "when debug mode is set" do
43
47
 
44
48
  let(:params) { [ "-D" ] }
@@ -48,6 +52,26 @@ describe ChefDK::Command::Update do
48
52
  end
49
53
  end
50
54
 
55
+ context "when an explicit config file path is given" do
56
+
57
+ let(:params) { %w[ -c ~/.chef/alternate_config.rb ] }
58
+
59
+ let(:chef_config_loader) { instance_double("Chef::WorkstationConfigLoader") }
60
+
61
+ it "sets the config file path to the given value" do
62
+ expect(command.config_path).to eq("~/.chef/alternate_config.rb")
63
+ end
64
+
65
+ it "loads the config from the given path" do
66
+ expect(Chef::WorkstationConfigLoader).to receive(:new).
67
+ with("~/.chef/alternate_config.rb").
68
+ and_return(chef_config_loader)
69
+ expect(chef_config_loader).to receive(:load)
70
+ expect(command.chef_config).to eq(Chef::Config)
71
+ end
72
+
73
+ end
74
+
51
75
  context "when attributes update mode is set" do
52
76
 
53
77
  let(:params) { ["-a"] }
@@ -0,0 +1,41 @@
1
+ #
2
+ # Copyright:: Copyright (c) 2014 Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'spec_helper'
19
+ require 'chef-dk/configurable'
20
+ require 'unit/fixtures/configurable/test_configurable'
21
+
22
+ describe ChefDK::Configurable do
23
+
24
+ let(:includer) { TestConfigurable.new }
25
+
26
+ it 'provides chef_config' do
27
+ expect(includer.chef_config).to eq Chef::Config
28
+ end
29
+
30
+ it 'provides chefdk_config' do
31
+ expect(includer.chefdk_config).to eq Chef::Config.chefdk
32
+ end
33
+
34
+ it 'provides knife_config' do
35
+ expect(includer.knife_config).to eq Chef::Config.knife
36
+ end
37
+
38
+ it 'provides generator_config' do
39
+ expect(includer.generator_config).to eq Chef::Config.chefdk.generator
40
+ end
41
+ end
@@ -0,0 +1,5 @@
1
+ class TestConfigLoader
2
+ def load
3
+ true
4
+ end
5
+ end
@@ -0,0 +1,10 @@
1
+ require 'unit/fixtures/configurable/test_config_loader'
2
+
3
+ class TestConfigurable
4
+ include ChefDK::Configurable
5
+
6
+ # don't use the workstation config loader
7
+ def config_loader
8
+ @config_loader ||= TestConfigLoader.new
9
+ end
10
+ end
@@ -30,7 +30,9 @@ describe ChefDK::Policyfile::CookbookLocationSpecification do
30
30
 
31
31
  let(:cached_cookbook) { double("ChefDK::CookbookMetadata") }
32
32
 
33
- let(:installer) { double("CookbookOmnifetch location", cached_cookbook: cached_cookbook) }
33
+ let(:install_path) { Pathname.new("~/.chefdk/cache/cookbooks/my_cookbook-1.0.0") }
34
+
35
+ let(:installer) { double("CookbookOmnifetch location", cached_cookbook: cached_cookbook, install_path: install_path) }
34
36
 
35
37
  let(:storage_config) do
36
38
  ChefDK::Policyfile::StorageConfig.new.use_policyfile(policyfile_filename)
@@ -135,6 +137,24 @@ describe ChefDK::Policyfile::CookbookLocationSpecification do
135
137
  expect(cookbook_location_spec.dependencies).to eq("apt" => "~> 1.2.3")
136
138
  end
137
139
 
140
+ it "determines whether a cookbook has a given recipe" do
141
+ cookbook_path = cookbook_location_spec.cookbook_path
142
+ # "cache" cookbook_path so we stub the correct object
143
+ allow(cookbook_location_spec).to receive(:cookbook_path).and_return(cookbook_path)
144
+
145
+ default_recipe_path = install_path.join("recipes/default.rb")
146
+ nope_recipe_path = install_path.join("recipes/nope.rb")
147
+
148
+ expect(cookbook_path).to receive(:join).with("recipes/default.rb").and_return(default_recipe_path)
149
+ expect(cookbook_path).to receive(:join).with("recipes/nope.rb").and_return(nope_recipe_path)
150
+
151
+ expect(default_recipe_path).to receive(:exist?).and_return(true)
152
+ expect(nope_recipe_path).to receive(:exist?).and_return(false)
153
+
154
+ expect(cookbook_location_spec.cookbook_has_recipe?("default")).to be(true)
155
+ expect(cookbook_location_spec.cookbook_has_recipe?("nope")).to be(false)
156
+ end
157
+
138
158
  end
139
159
 
140
160
  describe "when created with no source" do
@@ -468,7 +468,7 @@ describe ChefDK::Policyfile::ArchivedCookbook do
468
468
  described_class.new(wrapped_cookbook_lock, storage_config)
469
469
  end
470
470
 
471
- let(:archived_cookbook_path) { File.join(storage_config.relative_paths_root, "cookbooks", "nginx-111.222.333") }
471
+ let(:archived_cookbook_path) { File.join(storage_config.relative_paths_root, "cookbook_artifacts", "nginx-abc123") }
472
472
 
473
473
  it "sets cookbook_path to the path within the archive" do
474
474
  expect(cookbook_lock.cookbook_path).to eq(archived_cookbook_path)
@@ -182,6 +182,212 @@ describe ChefDK::PolicyfileCompiler, "when expressing the Policyfile graph deman
182
182
  expect(policyfile.errors).to eq([])
183
183
  end
184
184
 
185
+ context "Given resolvable cookbook demands" do
186
+
187
+ let(:default_source) { [:supermarket] }
188
+
189
+ let(:trimmed_cookbook_universe) do
190
+ {
191
+ "remote-cb" => {
192
+ "1.1.1" => [ ]
193
+ }
194
+
195
+ }
196
+ end
197
+
198
+ let(:remote_cb_source_opts) do
199
+ { artifactserver: "https://supermarket.example/c/remote-cb/1.1.1/download", version: "1.1.1" }
200
+ end
201
+
202
+ let(:default_source_obj) do
203
+ instance_double("ChefDK::Policyfile::CommunityCookbookSource")
204
+ end
205
+
206
+ let(:cb_location_spec) do
207
+ s = "Cookbook 'remote-cb'"
208
+ s << " = 1.1.1"
209
+ s << " #{remote_cb_source_opts}"
210
+
211
+ instance_double("ChefDK::Policyfile::CookbookLocationSpecification",
212
+ name: "remote-cb",
213
+ version_constraint: Semverse::Constraint.new("= 1.1.1"),
214
+ ensure_cached: nil,
215
+ to_s: s)
216
+ end
217
+
218
+ before do
219
+ policyfile.default_source.replace([ default_source_obj ])
220
+
221
+ allow(default_source_obj).to receive(:universe_graph).
222
+ and_return(trimmed_cookbook_universe)
223
+
224
+ allow(default_source_obj).to receive(:preferred_source_for?).
225
+ with("remote-cb").
226
+ and_return(true)
227
+
228
+ allow(default_source_obj).to receive(:source_options_for).
229
+ with("remote-cb", "1.1.1").
230
+ and_return(remote_cb_source_opts)
231
+
232
+
233
+ allow(ChefDK::Policyfile::CookbookLocationSpecification).to receive(:new).
234
+ with("remote-cb", "= 1.1.1", remote_cb_source_opts, policyfile.storage_config).
235
+ and_return(cb_location_spec)
236
+
237
+ allow(cb_location_spec).to receive(:installed?).and_return(true)
238
+ end
239
+
240
+ context "when the resolved cookbooks have the recipes requested by the run list" do
241
+
242
+ context "with an implied default recipe" do
243
+
244
+ before do
245
+ expect(cb_location_spec).to receive(:cookbook_has_recipe?).
246
+ with("default").
247
+ and_return(true)
248
+ end
249
+
250
+ let(:run_list) { ["remote-cb"] }
251
+
252
+ it "installs without error" do
253
+ expect { policyfile.install }.to_not raise_error
254
+ end
255
+
256
+ end
257
+
258
+ context "with an explicit recipe name" do
259
+
260
+ before do
261
+ expect(cb_location_spec).to receive(:cookbook_has_recipe?).
262
+ with("this_exists").
263
+ and_return(true)
264
+ end
265
+
266
+ let(:run_list) { ["remote-cb::this_exists"] }
267
+
268
+ it "installs without error" do
269
+ expect { policyfile.install }.to_not raise_error
270
+ end
271
+
272
+ end
273
+
274
+ context "with a fully qualified recipe name" do
275
+
276
+ before do
277
+ expect(cb_location_spec).to receive(:cookbook_has_recipe?).
278
+ with("this_exists").
279
+ and_return(true)
280
+ end
281
+
282
+ let(:run_list) { ["recipe[remote-cb::this_exists]"] }
283
+
284
+ it "installs without error" do
285
+ expect { policyfile.install }.to_not raise_error
286
+ end
287
+
288
+ end
289
+
290
+ end
291
+
292
+ context "when the resolved cookbooks do not have the recipes requested by the run list" do
293
+
294
+ context "when the cookbook with a missing recipe appears once in the run list" do
295
+ before do
296
+ expect(cb_location_spec).to receive(:cookbook_has_recipe?).
297
+ with("this_recipe_doesnt_exist").
298
+ and_return(false)
299
+ end
300
+
301
+ let(:run_list) { ["remote-cb::this_recipe_doesnt_exist"] }
302
+
303
+ it "emits an error" do
304
+ message =<<-MESSAGE
305
+ The installed cookbooks do not contain all the recipes required by your run list(s):
306
+ Cookbook 'remote-cb' = 1.1.1 {:artifactserver=>"https://supermarket.example/c/remote-cb/1.1.1/download", :version=>"1.1.1"}
307
+ is missing the following required recipes:
308
+ * this_recipe_doesnt_exist
309
+
310
+ You may have specified an incorrect recipe in your run list,
311
+ or this recipe may not be available in that version of the cookbook
312
+ MESSAGE
313
+
314
+ expect { policyfile.install }.to raise_error do |e|
315
+ expect(e).to be_a(ChefDK::CookbookDoesNotContainRequiredRecipe)
316
+ expect(e.message).to eq(message)
317
+ end
318
+ end
319
+ end
320
+
321
+ context "when there is one valid item and one invalid item in the run list" do
322
+
323
+ before do
324
+ expect(cb_location_spec).to receive(:cookbook_has_recipe?).
325
+ with("default").
326
+ and_return(true)
327
+ expect(cb_location_spec).to receive(:cookbook_has_recipe?).
328
+ with("this_recipe_doesnt_exist").
329
+ and_return(false)
330
+ end
331
+
332
+
333
+ let(:run_list) { ["remote-cb::default", "remote-cb::this_recipe_doesnt_exist"] }
334
+
335
+ it "emits an error" do
336
+ message =<<-MESSAGE
337
+ The installed cookbooks do not contain all the recipes required by your run list(s):
338
+ Cookbook 'remote-cb' = 1.1.1 {:artifactserver=>"https://supermarket.example/c/remote-cb/1.1.1/download", :version=>"1.1.1"}
339
+ is missing the following required recipes:
340
+ * this_recipe_doesnt_exist
341
+
342
+ You may have specified an incorrect recipe in your run list,
343
+ or this recipe may not be available in that version of the cookbook
344
+ MESSAGE
345
+
346
+ expect { policyfile.install }.to raise_error do |e|
347
+ expect(e).to be_a(ChefDK::CookbookDoesNotContainRequiredRecipe)
348
+ expect(e.message).to eq(message)
349
+ end
350
+ end
351
+
352
+ end
353
+
354
+ context "when there are multiple invalid items in the run list" do
355
+
356
+ before do
357
+ expect(cb_location_spec).to receive(:cookbook_has_recipe?).
358
+ with("this_recipe_doesnt_exist").
359
+ and_return(false)
360
+ expect(cb_location_spec).to receive(:cookbook_has_recipe?).
361
+ with("this_also_doesnt_exist").
362
+ and_return(false)
363
+ end
364
+
365
+ let(:run_list) { ["remote-cb::this_recipe_doesnt_exist", "remote-cb::this_also_doesnt_exist"] }
366
+
367
+ it "emits an error" do
368
+ message =<<-MESSAGE
369
+ The installed cookbooks do not contain all the recipes required by your run list(s):
370
+ Cookbook 'remote-cb' = 1.1.1 {:artifactserver=>"https://supermarket.example/c/remote-cb/1.1.1/download", :version=>"1.1.1"}
371
+ is missing the following required recipes:
372
+ * this_recipe_doesnt_exist
373
+ * this_also_doesnt_exist
374
+
375
+ You may have specified an incorrect recipe in your run list,
376
+ or this recipe may not be available in that version of the cookbook
377
+ MESSAGE
378
+
379
+ expect { policyfile.install }.to raise_error do |e|
380
+ expect(e).to be_a(ChefDK::CookbookDoesNotContainRequiredRecipe)
381
+ expect(e.message).to eq(message)
382
+ end
383
+ end
384
+
385
+ end
386
+
387
+
388
+ end
389
+ end
390
+
185
391
  context "Given no local or git cookbooks, no default source, and an empty run list" do
186
392
 
187
393
  let(:run_list) { [] }