halite 1.0.0.rc.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +20 -0
  4. data/.travis.yml +14 -4
  5. data/.yardopts +7 -0
  6. data/CHANGELOG.md +5 -0
  7. data/Gemfile +30 -8
  8. data/LICENSE +202 -202
  9. data/README.md +104 -2
  10. data/Rakefile +25 -5
  11. data/gemfiles/default.gemfile +17 -0
  12. data/gemfiles/master.gemfile +21 -0
  13. data/halite.gemspec +21 -10
  14. data/lib/berkshelf/halite.rb +25 -0
  15. data/lib/berkshelf/locations/gem.rb +84 -0
  16. data/lib/halite.rb +34 -4
  17. data/lib/halite/berkshelf/helper.rb +8 -2
  18. data/lib/halite/berkshelf/source.rb +9 -1
  19. data/lib/halite/converter.rb +34 -10
  20. data/lib/halite/converter/chef.rb +43 -0
  21. data/lib/halite/converter/libraries.rb +93 -26
  22. data/lib/halite/converter/metadata.rb +48 -10
  23. data/lib/halite/converter/misc.rb +43 -0
  24. data/lib/halite/dependencies.rb +48 -8
  25. data/lib/halite/error.rb +20 -0
  26. data/lib/halite/gem.rb +106 -21
  27. data/lib/halite/helper_base.rb +129 -0
  28. data/lib/halite/rake_helper.rb +46 -60
  29. data/lib/halite/rake_tasks.rb +17 -1
  30. data/lib/halite/spec_helper.rb +403 -54
  31. data/lib/halite/spec_helper/patcher.rb +130 -0
  32. data/lib/halite/spec_helper/runner.rb +57 -9
  33. data/lib/halite/version.rb +19 -1
  34. data/spec/converter/chef_spec.rb +54 -0
  35. data/spec/converter/libraries_spec.rb +131 -123
  36. data/spec/converter/metadata_spec.rb +61 -8
  37. data/spec/converter/misc_spec.rb +61 -0
  38. data/spec/converter_spec.rb +21 -6
  39. data/spec/dependencies_spec.rb +64 -10
  40. data/spec/example_resources/poise.rb +42 -0
  41. data/spec/example_resources/simple.rb +48 -0
  42. data/spec/{data/gems/test1/lib → fixtures/cookbooks/test1/files/halite_gem}/test1.rb +0 -0
  43. data/spec/{data/gems/test1/lib → fixtures/cookbooks/test1/files/halite_gem}/test1/version.rb +0 -0
  44. data/spec/fixtures/cookbooks/test1/libraries/default.rb +4 -0
  45. data/spec/{data/integration_cookbooks → fixtures/cookbooks}/test1/metadata.rb +0 -0
  46. data/spec/{data/gems/test2/chef → fixtures/cookbooks/test2}/attributes.rb +0 -0
  47. data/spec/{data/gems/test2/lib → fixtures/cookbooks/test2/files/halite_gem}/test2.rb +0 -0
  48. data/spec/{data/gems/test2/lib → fixtures/cookbooks/test2/files/halite_gem}/test2/resource.rb +0 -0
  49. data/spec/{data/gems/test2/lib → fixtures/cookbooks/test2/files/halite_gem}/test2/version.rb +0 -0
  50. data/spec/fixtures/cookbooks/test2/libraries/default.rb +3 -0
  51. data/spec/{data/integration_cookbooks → fixtures/cookbooks}/test2/metadata.rb +1 -1
  52. data/spec/{data/gems/test2/chef → fixtures/cookbooks/test2}/recipes/default.rb +0 -0
  53. data/spec/{data/gems/test2/chef → fixtures/cookbooks/test2}/templates/default/conf.erb +0 -0
  54. data/spec/{data/gems/test3/lib → fixtures/cookbooks/test3/files/halite_gem}/test3.rb +0 -0
  55. data/spec/{data/gems/test3/lib → fixtures/cookbooks/test3/files/halite_gem}/test3/dsl.rb +0 -0
  56. data/spec/{data/gems/test3/lib → fixtures/cookbooks/test3/files/halite_gem}/test3/version.rb +0 -0
  57. data/spec/fixtures/cookbooks/test3/libraries/default.rb +4 -0
  58. data/spec/{data/integration_cookbooks → fixtures/cookbooks}/test3/metadata.rb +0 -0
  59. data/spec/{data/gems/test3/chef → fixtures/cookbooks/test3}/recipes/default.rb +0 -0
  60. data/spec/{data → fixtures}/gems/test1/Rakefile +0 -0
  61. data/spec/fixtures/gems/test1/lib/test1.rb +2 -0
  62. data/spec/fixtures/gems/test1/lib/test1/version.rb +3 -0
  63. data/spec/{data → fixtures}/gems/test1/test1.gemspec +0 -0
  64. data/spec/{data → fixtures}/gems/test2/Rakefile +0 -0
  65. data/spec/{data/integration_cookbooks/test2 → fixtures/gems/test2/chef}/attributes.rb +0 -0
  66. data/spec/{data/integration_cookbooks/test2 → fixtures/gems/test2/chef}/recipes/default.rb +0 -0
  67. data/spec/{data/integration_cookbooks/test2 → fixtures/gems/test2/chef}/templates/default/conf.erb +0 -0
  68. data/spec/fixtures/gems/test2/lib/test2.rb +4 -0
  69. data/spec/{data/integration_cookbooks/test2/libraries/test2__resource.rb → fixtures/gems/test2/lib/test2/resource.rb} +1 -2
  70. data/spec/fixtures/gems/test2/lib/test2/version.rb +3 -0
  71. data/spec/{data → fixtures}/gems/test2/test2.gemspec +0 -0
  72. data/spec/{data → fixtures}/gems/test3/Rakefile +0 -0
  73. data/spec/{data/integration_cookbooks/test3 → fixtures/gems/test3/chef}/recipes/default.rb +0 -0
  74. data/spec/fixtures/gems/test3/lib/test3.rb +4 -0
  75. data/spec/{data/integration_cookbooks/test3/libraries/test3__dsl.rb → fixtures/gems/test3/lib/test3/dsl.rb} +1 -2
  76. data/spec/fixtures/gems/test3/lib/test3/version.rb +3 -0
  77. data/spec/{data → fixtures}/gems/test3/test3.gemspec +1 -0
  78. data/spec/gem_spec.rb +41 -31
  79. data/spec/integration_spec.rb +58 -82
  80. data/spec/runner_spec.rb +108 -0
  81. data/spec/spec_helper.rb +19 -26
  82. data/spec/spec_helper_spec.rb +238 -0
  83. metadata +124 -151
  84. data/lib/halite/converter/other.rb +0 -19
  85. data/lib/halite/converter/readme.rb +0 -20
  86. data/spec/converter/other_spec.rb +0 -56
  87. data/spec/converter/readme_spec.rb +0 -55
  88. data/spec/data/integration_cookbooks/test1/libraries/test1.rb +0 -3
  89. data/spec/data/integration_cookbooks/test1/libraries/test1__version.rb +0 -4
  90. data/spec/data/integration_cookbooks/test2/libraries/test2.rb +0 -5
  91. data/spec/data/integration_cookbooks/test2/libraries/test2__version.rb +0 -4
  92. data/spec/data/integration_cookbooks/test3/libraries/test3.rb +0 -5
  93. data/spec/data/integration_cookbooks/test3/libraries/test3__version.rb +0 -4
@@ -1,2 +1,18 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
1
17
  require 'halite/rake_helper'
2
- ::Halite::RakeHelper.install_tasks
18
+ ::Halite::RakeHelper.install
@@ -14,59 +14,280 @@
14
14
  # limitations under the License.
15
15
  #
16
16
 
17
- require 'halite/spec_helper/runner'
17
+ require 'chef/node'
18
+ require 'chef/provider'
19
+ require 'chef/resource'
20
+ require 'chefspec'
21
+
18
22
 
19
23
  module Halite
24
+ # A helper module for RSpec tests of resource-based cookbooks.
25
+ #
26
+ # @since 1.0.0
27
+ # @example
28
+ # describe MyMixin do
29
+ # resource(:my_thing) do
30
+ # include Poise
31
+ # include MyMixin
32
+ # action(:install)
33
+ # attribute(:path, kind_of: String, default: '/etc/thing')
34
+ # end
35
+ # provider(:my_thing) do
36
+ # include Poise
37
+ # def action_install
38
+ # file new_resource.path do
39
+ # content new_resource.my_mixin
40
+ # end
41
+ # end
42
+ # end
43
+ # recipe do
44
+ # my_thing 'test'
45
+ # end
46
+ #
47
+ # it { is_expected.to create_file('/etc/thing').with(content: 'mixin stuff') }
48
+ # end
20
49
  module SpecHelper
50
+ autoload :Patcher, 'halite/spec_helper/patcher'
51
+ autoload :Runner, 'halite/spec_helper/runner'
21
52
  extend RSpec::SharedContext
53
+
54
+ # @!attribute [r] step_into
55
+ # Resource names to step in to when running this example.
56
+ # @see https://github.com/sethvargo/chefspec#testing-lwrps
57
+ # @return [Array<Symbol>]
58
+ # @example
59
+ # before do
60
+ # step_into << :my_lwrp
61
+ # end
22
62
  let(:step_into) { [] }
63
+ # @!attribute [r] default_attributes
64
+ # Hash to use as default-level node attributes for this example.
65
+ # @return [Hash]
66
+ # @example
67
+ # before do
68
+ # default_attributes['myapp']['url'] = 'http://testserver'
69
+ # end
70
+ let(:default_attributes) { Hash.new }
71
+ # @!attribute [r] normal_attributes
72
+ # Hash to use as normal-level node attributes for this example.
73
+ # @return [Hash]
74
+ # @see #default_attributes
75
+ let(:normal_attributes) { Hash.new }
76
+ # @!attribute [r] override_attributes
77
+ # Hash to use as override-level node attributes for this example.
78
+ # @return [Hash]
79
+ # @see #default_attributes
80
+ let(:override_attributes) { Hash.new }
81
+ # @todo docs
82
+ let(:halite_gemspec) { nil }
83
+ # @!attribute [r] chefspec_options
84
+ # Options hash for the ChefSpec runner instance.
85
+ # @return [Hash<Symbol, Object>]
86
+ # @example Enable Fauxhai attributes
87
+ # let(:chefspec_options) { {platform: 'ubuntu', version: '12.04'} }
88
+ let(:chefspec_options) { Hash.new }
89
+ # @!attribute [r] chef_runner
90
+ # ChefSpec runner for this example.
91
+ # @return [ChefSpec::SoloRunner]
92
+ let(:chef_runner) do
93
+ Halite::SpecHelper::Runner.new(
94
+ {
95
+ step_into: step_into,
96
+ default_attributes: default_attributes,
97
+ normal_attributes: normal_attributes,
98
+ override_attributes: override_attributes,
99
+ halite_gemspec: halite_gemspec,
100
+ }.merge(chefspec_options)
101
+ )
102
+ end
103
+ # @!attribute [r] chef_run
104
+ # Trigger a Chef converge. By default no resources are converged. This is
105
+ # normally overwritten by the {#recipe} helper.
106
+ # @return [ChefSpec::SoloRunner]
107
+ # @see #recipe
108
+ let(:chef_run) { chef_runner.converge() }
23
109
 
24
- # An alias for slightly more semantic meaning, just forces the lazy #subject to run.
110
+ # An alias for slightly more semantic meaning, just forces the lazy #subject
111
+ # to run.
112
+ #
113
+ # @see http://www.relishapp.com/rspec/rspec-core/v/3-2/docs/subject/explicit-subject RSpec's subject helper
114
+ # @example
115
+ # describe 'my recipe' do
116
+ # recipe 'my_recipe'
117
+ # it { run_chef }
118
+ # end
25
119
  def run_chef
26
- subject
120
+ chef_run
27
121
  end
28
122
 
29
- private
123
+ # Return a helper-defined resource.
124
+ #
125
+ # @param name [Symbol] Name of the resource.
126
+ # @return [Class]
127
+ # @example
128
+ # subject { resource(:my_resource) }
129
+ def resource(name)
130
+ self.class.resources[name.to_sym]
131
+ end
30
132
 
31
- def patch_module(mod, name, obj, &block)
32
- class_name = Chef::Mixin::ConvertToClassName.convert_to_class_name(name.to_s)
33
- if mod.const_defined?(class_name, false)
34
- old_class = mod.const_get(class_name, false)
35
- # We are only allowed to patch over things installed by patch_module
36
- raise "#{mod.name}::#{class_name} is already defined" if !old_class.instance_variable_get(:@poise_spec_helper)
37
- # Remove it before setting to avoid the redefinition warning
38
- mod.send(:remove_const, class_name)
39
- end
40
- # Tag our objects so we know we are allows to overwrite those, but not other stuff.
41
- obj.instance_variable_set(:@poise_spec_helper, true)
42
- mod.const_set(class_name, obj)
43
- begin
44
- block.call
45
- ensure
46
- # Same as above, have to remove before set because warnings
47
- mod.send(:remove_const, class_name)
48
- mod.const_set(class_name, old_class) if old_class
49
- end
133
+ # Return a helper-defined provider.
134
+ #
135
+ # @param name [Symbol] Name of the provider.
136
+ # @return [Class]
137
+ # @example
138
+ # subject { provider(:my_provider) }
139
+ def provider(name)
140
+ self.class.providers[name.to_sym]
50
141
  end
51
142
 
143
+ # @!classmethods
52
144
  module ClassMethods
53
- def recipe(&block)
145
+ # Define a recipe to be run via ChefSpec and used as the subject of this
146
+ # example group. You can specify either a single recipe block or
147
+ # one-or-more recipe names.
148
+ #
149
+ # @param recipe_names [Array<String>] Recipe names to converge for this test.
150
+ # @param block [Proc] Recipe to converge for this test.
151
+ # @param subject [Boolean] If true, this recipe should be the subject of
152
+ # this test.
153
+ # @example Using a recipe block
154
+ # describe 'my recipe' do
155
+ # recipe do
156
+ # ruby_block 'test'
157
+ # end
158
+ # it { is_expected.to run_ruby_block('test') }
159
+ # end
160
+ # @example Using external recipes
161
+ # describe 'my recipe' do
162
+ # recipe 'my_recipe'
163
+ # it { is_expected.to run_ruby_block('test') }
164
+ # end
165
+ def recipe(*recipe_names, subject: true, &block)
54
166
  # Keep the actual logic in a let in case I want to define the subject as something else
55
- let(:chef_run) { Halite::SpecHelper::Runner.new(step_into: step_into).converge(&block) }
56
- subject { chef_run }
167
+ let(:chef_run) { chef_runner.converge(*recipe_names, &block) }
168
+ subject { chef_run } if subject
57
169
  end
58
170
 
59
- # A note about the :parent option below: You can't use resources that
60
- # are defined via these helpers because they don't have a global const
61
- # name until the actual tests are executing.
171
+ # Configure ChefSpec to step in to a resource/provider. This will also
172
+ # automatically create ChefSpec matchers for the resource.
173
+ #
174
+ # @overload step_into(name)
175
+ # @param name [String, Symbol] Name of the resource in snake-case.
176
+ # @overload step_into(resource, resource_name)
177
+ # @param resource [Class] Resource class to step in to.
178
+ # @param resource_name [String, Symbol, nil] Name of the given resource in snake-case.
179
+ # @example
180
+ # describe 'my_lwrp' do
181
+ # step_into(:my_lwrp)
182
+ # recipe do
183
+ # my_lwrp 'test'
184
+ # end
185
+ # it { is_expected.to run_ruby_block('test') }
186
+ # end
187
+ def step_into(name, resource_name=nil, unwrap_notifying_block: true)
188
+ resource_class = if name.is_a?(Class)
189
+ name
190
+ elsif resources[name.to_sym]
191
+ # Handle cases where the resource has defined via a helper with
192
+ # step_into:false but a nested example wants to step in.
193
+ resources[name.to_sym]
194
+ else
195
+ # Won't see platform/os specific resources but not sure how to fix
196
+ # that. I need the class here for the matcher creation below.
197
+ Chef::Resource.resource_for_node(name.to_sym, Chef::Node.new)
198
+ end
199
+ resource_name ||= if resource_class.respond_to?(:resource_name)
200
+ resource_class.resource_name
201
+ else
202
+ Chef::Mixin::ConvertToClassName.convert_to_snake_case(resource_class.name.split('::').last)
203
+ end
204
+
205
+ # Add a resource-level matcher to ChefSpec.
206
+ ChefSpec.define_matcher(resource_name)
207
+
208
+ # Figure out the available actions and create ChefSpec matchers.
209
+ resource_class.new(nil, nil).allowed_actions.each do |action|
210
+ define_method("#{action}_#{resource_name}") do |instance_name|
211
+ ChefSpec::Matchers::ResourceMatcher.new(resource_name, action, instance_name)
212
+ end
213
+ end
62
214
 
63
- def resource(name, options={}, &block)
64
- options = {auto: true, parent: Chef::Resource}.merge(options)
65
- # Create the resource class
66
- resource_class = Class.new(options[:parent]) do
215
+ # Patch notifying_block from Poise::Provider to just run directly.
216
+ # This is not a great solution but it is better than nothing for right
217
+ # now. In the future this should maybe do an internal converge but using
218
+ # ChefSpec somehow?
219
+ if unwrap_notifying_block
220
+ old_provider_for_action = resource_class.instance_method(:provider_for_action)
221
+ resource_class.send(:define_method, :provider_for_action) do |*args|
222
+ old_provider_for_action.bind(self).call(*args).tap do |provider|
223
+ if provider.respond_to?(:notifying_block, true)
224
+ provider.define_singleton_method(:notifying_block) do |&block|
225
+ block.call
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
231
+
232
+ # Add to the let variable passed in to ChefSpec.
233
+ before { step_into << resource_name }
234
+ end
235
+
236
+ # Define a resource class for use in an example group. By default the
237
+ # :run action will be set as the default.
238
+ #
239
+ # @param name [Symbol] Name for the resource in snake-case.
240
+ # @param options [Hash] Resource options.
241
+ # @option options [Class, Symbol] :parent (Chef::Resource) Parent class
242
+ # for the resource. If a symbol is given, it corresponds to another
243
+ # resource defined via this helper.
244
+ # @option options [Boolean] :auto (true) Set the resource name correctly
245
+ # and use :run as the default action.
246
+ # @param block [Proc] Body of the resource class. Optional.
247
+ # @example
248
+ # describe MyMixin do
249
+ # resource(:my_resource) do
250
+ # include Poise
251
+ # attribute(:path, kind_of: String)
252
+ # end
253
+ # provider(:my_resource)
254
+ # recipe do
255
+ # my_resource 'test' do
256
+ # path '/tmp'
257
+ # end
258
+ # end
259
+ # it { is_expected.to run_my_resource('test').with(path: '/tmp') }
260
+ # end
261
+ def resource(name, auto: true, parent: Chef::Resource, step_into: true, unwrap_notifying_block: true, defined_at: caller[0], &block)
262
+ parent = resources[parent] if parent.is_a?(Symbol)
263
+ raise Halite::Error.new("Parent class for #{name} is not a class: #{parent.inspect}") unless parent.is_a?(Class)
264
+ # Pull out the example metadata for use in the class.
265
+ metadata = self.metadata
266
+ # Create the resource class.
267
+ resource_class = Class.new(parent) do
268
+ # Make the anonymous class pretend to have a name.
269
+ define_singleton_method(:name) do
270
+ 'Chef::Resource::' + Chef::Mixin::ConvertToClassName.convert_to_class_name(name.to_s)
271
+ end
272
+
273
+ # Helper for debugging, shows where the class was defined.
274
+ define_singleton_method(:halite_defined_at) do
275
+ defined_at
276
+ end
277
+
278
+ # Create magic delegators for various metadata.
279
+ %i{described_class}.each do |key|
280
+ define_method(key) { metadata[key] }
281
+ define_singleton_method(key) { metadata[key] }
282
+ end
283
+
284
+ # Evaluate the class body.
67
285
  class_exec(&block) if block
68
- # Wrap some stuff around initialize because I'm lazy
69
- if options[:auto]
286
+
287
+
288
+ # Optional initialization steps. Disable for special unicorn tests.
289
+ if auto
290
+ # Fill in a :run action by default.
70
291
  old_init = instance_method(:initialize)
71
292
  define_method(:initialize) do |*args|
72
293
  # Fill in the resource name because I know it
@@ -81,45 +302,110 @@ module Halite
81
302
  end
82
303
  end
83
304
 
84
- # Figure out the available actions
85
- resource_class.new(nil, nil).allowed_actions.each do |action|
86
- define_method("#{action}_#{name}") do |resource_name|
87
- ChefSpec::Matchers::ResourceMatcher.new(name, action, resource_name)
88
- end
89
- end
305
+ # Remove from overall descendants tracker.
306
+ Chef::Mixin::DescendantsTracker.direct_descendants(parent).delete(resource_class)
307
+
308
+ # Store for use up with the parent system
309
+ halite_helpers[:resources][name.to_sym] = resource_class
310
+
311
+ # Automatically step in to our new resource
312
+ step_into(resource_class, name, unwrap_notifying_block: unwrap_notifying_block) if step_into
90
313
 
91
314
  around do |ex|
92
- # Automatically step in to our new resource
93
- step_into << name
94
- # Patch the resource in to Chef
95
- patch_module(Chef::Resource, name, resource_class) { ex.run }
315
+ if resource(name) == resource_class
316
+ # We haven't been overridden from a nested scope.
317
+ Patcher.patch(name, resource_class, Chef::Resource) { ex.run }
318
+ else
319
+ ex.run
320
+ end
96
321
  end
97
322
  end
98
323
 
99
- def provider(name, options={}, &block)
100
- options = {auto: true, rspec: true, parent: Chef::Provider}.merge(options)
101
- provider_class = Class.new(options[:parent]) do
102
- # Pull in RSpec expectations
103
- if options[:rspec]
324
+ # Define a provider class for use in an example group. By default a :run
325
+ # action will be created, load_current_resource will be defined as a
326
+ # no-op, and the RSpec matchers will be available inside the provider.
327
+ #
328
+ # @param name [Symbol] Name for the provider in snake-case.
329
+ # @param options [Hash] Provider options.
330
+ # @option options [Class, Symbol] :parent (Chef::Provider) Parent class
331
+ # for the provider. If a symbol is given, it corresponds to another
332
+ # resource defined via this helper.
333
+ # @option options [Boolean] :auto (true) Create action_run and
334
+ # load_current_resource.
335
+ # @option options [Boolean] :rspec (true) Include RSpec matchers in the
336
+ # provider.
337
+ # @param block [Proc] Body of the provider class. Optional.
338
+ # @example
339
+ # describe MyMixin do
340
+ # resource(:my_resource)
341
+ # provider(:my_resource) do
342
+ # include Poise
343
+ # def action_run
344
+ # ruby_block 'test'
345
+ # end
346
+ # end
347
+ # recipe do
348
+ # my_resource 'test'
349
+ # end
350
+ # it { is_expected.to run_my_resource('test') }
351
+ # it { is_expected.to run_ruby_block('test') }
352
+ # end
353
+ def provider(name, auto: true, rspec: true, parent: Chef::Provider, defined_at: caller[0], &block)
354
+ parent = providers[parent] if parent.is_a?(Symbol)
355
+ raise Halite::Error.new("Parent class for #{name} is not a class: #{options[:parent].inspect}") unless parent.is_a?(Class)
356
+ # Pull out the example metadata for use in the class.
357
+ metadata = self.metadata
358
+ # Create the provider class.
359
+ provider_class = Class.new(parent) do
360
+ # Pull in RSpec expectations.
361
+ if rspec
104
362
  include RSpec::Matchers
105
363
  include RSpec::Mocks::ExampleMethods
106
364
  end
107
365
 
108
- if options[:auto]
109
- # Default blank impl to avoid error
366
+ if auto
367
+ # Default blank impl to avoid error.
110
368
  def load_current_resource
111
369
  end
112
370
 
113
- # Blank action because I do that so much
371
+ # Blank action because I do that so much.
114
372
  def action_run
115
373
  end
116
374
  end
117
375
 
376
+ # Make the anonymous class pretend to have a name.
377
+ define_singleton_method(:name) do
378
+ 'Chef::Provider::' + Chef::Mixin::ConvertToClassName.convert_to_class_name(name.to_s)
379
+ end
380
+
381
+ # Helper for debugging, shows where the class was defined.
382
+ define_singleton_method(:halite_defined_at) do
383
+ defined_at
384
+ end
385
+
386
+ # Create magic delegators for various metadata.
387
+ %i{described_class}.each do |key|
388
+ define_method(key) { metadata[key] }
389
+ define_singleton_method(key) { metadata[key] }
390
+ end
391
+
392
+ # Evaluate the class body.
118
393
  class_exec(&block) if block
119
394
  end
120
395
 
396
+ # Remove from overall descendants tracker.
397
+ Chef::Mixin::DescendantsTracker.direct_descendants(parent).delete(provider_class)
398
+
399
+ # Store for use up with the parent system
400
+ halite_helpers[:providers][name.to_sym] = provider_class
401
+
121
402
  around do |ex|
122
- patch_module(Chef::Provider, name, provider_class) { ex.run }
403
+ if provider(name) == provider_class
404
+ # We haven't been overridden from a nested scope.
405
+ Patcher.patch(name, provider_class, Chef::Provider) { ex.run }
406
+ else
407
+ ex.run
408
+ end
123
409
  end
124
410
  end
125
411
 
@@ -127,8 +413,71 @@ module Halite
127
413
  super
128
414
  klass.extend ClassMethods
129
415
  end
416
+
417
+ # Storage for helper-defined resources and providers to find them for
418
+ # parent lookups if needed.
419
+ #
420
+ # @api private
421
+ # @return [Hash<Symbol, Hash<Symbol, Class>>]
422
+ def halite_helpers
423
+ @halite_helpers ||= {resources: {}, providers: {}}
424
+ end
425
+
426
+ # Find all helper-defined resources in the current context and parents.
427
+ #
428
+ # @api private
429
+ # @return [Hash<Symbol, Class>]
430
+ def resources
431
+ ([self] + parent_groups).reverse.inject({}) do |memo, group|
432
+ begin
433
+ memo.merge(group.halite_helpers[:resources] || {})
434
+ rescue NoMethodError
435
+ memo
436
+ end
437
+ end
438
+ end
439
+
440
+ # Find all helper-defined providers in the current context and parents.
441
+ #
442
+ # @api private
443
+ # @return [Hash<Symbol, Class>]
444
+ def providers
445
+ ([self] + parent_groups).reverse.inject({}) do |memo, group|
446
+ begin
447
+ memo.merge(group.halite_helpers[:providers] || {})
448
+ rescue NoMethodError
449
+ memo
450
+ end
451
+ end
452
+ end
130
453
  end
131
454
 
132
455
  extend ClassMethods
133
456
  end
457
+
458
+ # Method version of SpecHelper module. Used to inject a gem data to load
459
+ # a synthetic cookbook during testing.
460
+ #
461
+ # @see Halite::SpecHelper
462
+ # @param gemspec [Gem::Specification] Gem spec to load as cookbook.
463
+ def self.SpecHelper(gemspec)
464
+ # Create a new anonymous module
465
+ mod = Module.new
466
+
467
+ # Fake the name
468
+ def mod.name
469
+ super || 'Halite::SpecHelper'
470
+ end
471
+
472
+ mod.define_singleton_method(:included) do |klass|
473
+ super(klass)
474
+ # Pull in the main helper to cover most of the needed logic
475
+ klass.class_exec do
476
+ include Halite::SpecHelper
477
+ let(:halite_gemspec) { gemspec }
478
+ end
479
+ end
480
+
481
+ mod
482
+ end
134
483
  end