poise 2.2.3 → 2.3.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +14 -0
  4. data/Gemfile +0 -2
  5. data/lib/poise.rb +3 -1
  6. data/lib/poise/backports.rb +27 -0
  7. data/lib/poise/backports/not_passed.rb +52 -0
  8. data/lib/poise/helpers.rb +1 -0
  9. data/lib/poise/helpers/chefspec_matchers.rb +5 -1
  10. data/lib/poise/helpers/inversion.rb +36 -13
  11. data/lib/poise/helpers/option_collector.rb +15 -4
  12. data/lib/poise/helpers/resource_name.rb +1 -1
  13. data/lib/poise/helpers/resource_subclass.rb +81 -0
  14. data/lib/poise/helpers/subresources/child.rb +50 -9
  15. data/lib/poise/helpers/subresources/container.rb +33 -6
  16. data/lib/poise/resource.rb +3 -1
  17. data/lib/poise/subcontext/resource_collection.rb +20 -1
  18. data/lib/poise/utils.rb +79 -7
  19. data/lib/poise/utils/resource_provider_mixin.rb +2 -2
  20. data/lib/poise/version.rb +1 -1
  21. data/test/spec/backports/not_passed_spec.rb +29 -0
  22. data/test/spec/helpers/chefspec_matchers_spec.rb +17 -0
  23. data/test/spec/helpers/inversion_spec.rb +72 -0
  24. data/test/spec/helpers/lwrp_polyfill_spec.rb +9 -0
  25. data/test/spec/helpers/option_collector_spec.rb +66 -30
  26. data/test/spec/helpers/resource_name_spec.rb +15 -2
  27. data/test/spec/helpers/resource_subclass_spec.rb +97 -0
  28. data/test/spec/helpers/subresources/child_spec.rb +234 -2
  29. data/test/spec/helpers/subresources/container_spec.rb +37 -0
  30. data/test/spec/resource_spec.rb +31 -2
  31. data/test/spec/subcontext/resource_collection_spec.rb +99 -0
  32. data/test/spec/utils/resource_provider_mixin_spec.rb +22 -0
  33. data/test/spec/utils_spec.rb +187 -1
  34. metadata +11 -3
  35. data/Berksfile.lock +0 -10
@@ -48,8 +48,9 @@ module Poise
48
48
 
49
49
  def after_created
50
50
  super
51
- # Register
52
- Poise::Helpers::Subresources::DefaultContainers.register!(self, run_context)
51
+ # Register as a default container if needed.
52
+ Poise::Helpers::Subresources::DefaultContainers.register!(self, run_context) if self.class.container_default
53
+ # Add all internal subresources to the resource collection.
53
54
  unless @subresources.empty?
54
55
  Chef::Log.debug("[#{self}] Adding subresources to collection:")
55
56
  # Because after_create is run before adding the container to the resource collection
@@ -113,8 +114,13 @@ module Poise
113
114
  end
114
115
  resource << super(type, sub_name, created_at) do
115
116
  # Apply the correct parent before anything else so it is available
116
- # in after_created for the subresource.
117
- parent(self_) if respond_to?(:parent)
117
+ # in after_created for the subresource. It might raise
118
+ # NoMethodError is there isn't a real parent.
119
+ begin
120
+ parent(self_) if respond_to?(:parent)
121
+ rescue NoMethodError
122
+ # This space left intentionally blank.
123
+ end
118
124
  # Run the resource block.
119
125
  instance_exec(&block) if block
120
126
  end
@@ -147,13 +153,34 @@ module Poise
147
153
  def container_namespace(val=nil)
148
154
  @container_namespace = val unless val.nil?
149
155
  if @container_namespace.nil?
150
- # Not set here, look at the superclass of true by default for backwards compat.
151
- superclass.respond_to?(:container_namespace) ? superclass.container_namespace : true
156
+ # Not set here, look at the superclass or true by default for backwards compat.
157
+ Poise::Utils.ancestor_send(self, :container_namespace, default: true)
152
158
  else
153
159
  @container_namespace
154
160
  end
155
161
  end
156
162
 
163
+ # @overload container_default()
164
+ # Get the default mode for this resource. If false, this resource
165
+ # class will not be used for default container lookups. Defaults to
166
+ # true.
167
+ # @since 2.3.0
168
+ # @return [Boolean]
169
+ # @overload container_default(val)
170
+ # Set the default mode for this resource.
171
+ # @since 2.3.0
172
+ # @param val [Boolean] Default mode to set.
173
+ # @return [Boolean]
174
+ def container_default(val=nil)
175
+ @container_default = val unless val.nil?
176
+ if @container_default.nil?
177
+ # Not set here, look at the superclass or true by default for backwards compat.
178
+ Poise::Utils.ancestor_send(self, :container_default, default: true)
179
+ else
180
+ @container_default
181
+ end
182
+ end
183
+
157
184
  def included(klass)
158
185
  super
159
186
  klass.extend(ClassMethods)
@@ -39,14 +39,16 @@ module Poise
39
39
  include Poise::Helpers::OptionCollector
40
40
  include Poise::Helpers::ResourceCloning
41
41
  include Poise::Helpers::ResourceName
42
+ include Poise::Helpers::ResourceSubclass
42
43
  include Poise::Helpers::TemplateContent
43
44
 
44
45
  # @!classmethods
45
46
  module ClassMethods
46
- def poise_subresource_container(namespace=nil)
47
+ def poise_subresource_container(namespace=nil, default=nil)
47
48
  include Poise::Helpers::Subresources::Container
48
49
  # false is a valid value.
49
50
  container_namespace(namespace) unless namespace.nil?
51
+ container_default(default) unless default.nil?
50
52
  end
51
53
 
52
54
  def poise_subresource(parent_type=nil, parent_optional=nil, parent_auto=nil)
@@ -40,7 +40,10 @@ module Poise
40
40
  @parent.lookup(resource)
41
41
  end
42
42
 
43
- # Iterate and expand all nested contexts
43
+ # Iterate over all resources, expanding parent context in order.
44
+ #
45
+ # @param block [Proc] Iteration block
46
+ # @return [void]
44
47
  def recursive_each(&block)
45
48
  if @parent
46
49
  if @parent.respond_to?(:recursive_each)
@@ -51,6 +54,22 @@ module Poise
51
54
  end
52
55
  each(&block)
53
56
  end
57
+
58
+ # Iterate over all resources in reverse order.
59
+ #
60
+ # @since 2.3.0
61
+ # @param block [Proc] Iteration block
62
+ # @return [void]
63
+ def reverse_recursive_each(&block)
64
+ reverse_each(&block)
65
+ if @parent
66
+ if @parent.respond_to?(:recursive_each)
67
+ @parent.reverse_recursive_each(&block)
68
+ else
69
+ @parent.reverse_each(&block)
70
+ end
71
+ end
72
+ end
54
73
  end
55
74
  end
56
75
  end
@@ -43,7 +43,6 @@ module Poise
43
43
  if ver.respond_to?(:halite_root)
44
44
  # The join is there because ../poise-ruby/lib starts with ../poise so
45
45
  # we want a trailing /.
46
- Chef::Log.debug("")
47
46
  if filename.start_with?(File.join(ver.halite_root, ''))
48
47
  Chef::Log.debug("[Poise] Found matching halite_root in #{name}: #{ver.halite_root.inspect}")
49
48
  possibles[ver.halite_root] = name
@@ -53,7 +52,7 @@ module Poise
53
52
  ver.segment_filenames(seg).each do |file|
54
53
  # Put this behind an environment variable because it is verbose
55
54
  # even for normal debugging-level output.
56
- Chef::Log.debug("[Poise] Checking #{seg} in #{name}: #{file.inspect}") if ENV['POISE_DEBUG']
55
+ Chef::Log.debug("[Poise] Checking #{seg} in #{name}: #{file.inspect}") if ENV['POISE_DEBUG'] || run_context.node['POISE_DEBUG']
57
56
  if file == filename
58
57
  Chef::Log.debug("[Poise] Found matching #{seg} in #{name}: #{file.inspect}")
59
58
  possibles[file] = name
@@ -70,14 +69,17 @@ module Poise
70
69
  # Try to find an ancestor to call a method on.
71
70
  #
72
71
  # @since 2.2.3
72
+ # @since 2.3.0
73
+ # Added ignore parameter.
73
74
  # @param obj [Object] Self from the caller.
74
75
  # @param msg [Symbol] Method to try to call.
75
76
  # @param args [Array<Object>] Method arguments.
76
77
  # @param default [Object] Default return value if no valid ancestor exists.
78
+ # @param ignore [Array<Object>] Return value to ignore when scanning ancesors.
77
79
  # @return [Object]
78
80
  # @example
79
81
  # val = @val || Poise::Utils.ancestor_send(self, :val)
80
- def ancestor_send(obj, msg, *args, default: nil)
82
+ def ancestor_send(obj, msg, *args, default: nil, ignore: [default])
81
83
  # Class is a subclass of Module, if we get something else use its class.
82
84
  obj = obj.class unless obj.is_a?(Module)
83
85
  ancestors = []
@@ -87,11 +89,81 @@ module Poise
87
89
  end
88
90
  # Make sure we don't check obj itself.
89
91
  ancestors.concat(obj.ancestors.drop(1))
90
- ancestor = ancestors.find {|mod| mod.respond_to?(msg) }
91
- if ancestor
92
- ancestor.send(msg, *args)
92
+ ancestors.each do |mod|
93
+ if mod.respond_to?(msg)
94
+ val = mod.send(msg, *args)
95
+ # If we get the default back, assume we should keep trying.
96
+ return val unless ignore.include?(val)
97
+ end
98
+ end
99
+ # Nothing valid found, use the default.
100
+ default
101
+ end
102
+
103
+ # Create a helper to invoke a module with some parameters.
104
+ #
105
+ # @since 2.3.0
106
+ # @param mod [Module] The module to wrap.
107
+ # @param block [Proc] The module to implement to parameterization.
108
+ # @return [void]
109
+ # @example
110
+ # module MyMixin
111
+ # def self.my_mixin_name(name)
112
+ # # ...
113
+ # end
114
+ # end
115
+ #
116
+ # Poise::Utils.parameterized_module(MyMixin) do |name|
117
+ # my_mixin_name(name)
118
+ # end
119
+ def parameterized_module(mod, &block)
120
+ raise Poise::Error.new("Cannot parameterize an anonymous module") unless mod.name && !mod.name.empty?
121
+ parent_name_parts = mod.name.split(/::/)
122
+ # Grab the last piece which will be the method name.
123
+ mod_name = parent_name_parts.pop
124
+ # Find the enclosing module or class object.
125
+ parent = parent_name_parts.inject(Object) {|memo, name| memo.const_get(name) }
126
+ # Object is a special case since we need #define_method instead.
127
+ method_type = if parent == Object
128
+ :define_method
93
129
  else
94
- default
130
+ :define_singleton_method
131
+ end
132
+ # Scoping hack.
133
+ self_ = self
134
+ # Construct the method.
135
+ parent.send(method_type, mod_name) do |*args|
136
+ self_.send(:check_block_arity!, block, args)
137
+ # Create a new anonymous module to be returned from the method.
138
+ Module.new do
139
+ # Fake the name.
140
+ define_singleton_method(:name) do
141
+ super() || mod.name
142
+ end
143
+
144
+ # When the stub module gets included, activate our behaviors.
145
+ define_singleton_method(:included) do |klass|
146
+ super(klass)
147
+ klass.send(:include, mod)
148
+ klass.instance_exec(*args, &block)
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ private
155
+
156
+ # Check that the given arguments match the given block. This is needed
157
+ # because Ruby will nil-pad mismatched argspecs on blocks rather than error.
158
+ #
159
+ # @since 2.3.0
160
+ # @param block [Proc] Block to check.
161
+ # @param args [Array<Object>] Arguments to check.
162
+ # @return [void]
163
+ def check_block_arity!(block, args)
164
+ required_args = block.arity < 0 ? ~block.arity : block.arity
165
+ if args.length < required_args || (block.arity >= 0 && args.length > block.arity)
166
+ raise ArgumentError.new("wrong number of arguments (#{args.length} for #{required_args}#{block.arity < 0 ? '+' : ''})")
95
167
  end
96
168
  end
97
169
 
@@ -48,10 +48,10 @@ module Poise
48
48
  # Cargo this .included to things which include us.
49
49
  inner_klass.extend(mod)
50
50
  # Dispatch to submodules, inner_klass is the most recent includer.
51
- if inner_klass < Chef::Resource
51
+ if inner_klass < Chef::Resource || inner_klass.name.to_s.end_with?('::Resource')
52
52
  # Use klass::Resource to look up relative to the original module.
53
53
  inner_klass.class_exec { include klass::Resource }
54
- elsif inner_klass < Chef::Provider
54
+ elsif inner_klass < Chef::Provider || inner_klass.name.to_s.end_with?('::Provider')
55
55
  # As above, klass::Provider.
56
56
  inner_klass.class_exec { include klass::Provider }
57
57
  end
@@ -16,5 +16,5 @@
16
16
 
17
17
 
18
18
  module Poise
19
- VERSION = '2.2.3'
19
+ VERSION = '2.3.0'
20
20
  end
@@ -0,0 +1,29 @@
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
+
17
+ require 'spec_helper'
18
+
19
+ describe Poise::Backports::NOT_PASSED do
20
+ it { is_expected.to be_truthy }
21
+ its(:to_s) { is_expected.to eq 'NOT_PASSED' }
22
+ its(:inspect) { is_expected.to eq 'NOT_PASSED' }
23
+ end
24
+
25
+ describe Poise::NOT_PASSED do
26
+ it { is_expected.to be_truthy }
27
+ its(:to_s) { is_expected.to eq 'NOT_PASSED' }
28
+ its(:inspect) { is_expected.to eq 'NOT_PASSED' }
29
+ end
@@ -42,4 +42,21 @@ describe Poise::Helpers::ChefspecMatchers do
42
42
  it { is_expected.to run_poise_other('test') }
43
43
  it { expect(chef_run.poise_other('test')).to be_a Chef::Resource }
44
44
  end # /context with an explicit name
45
+
46
+ context 'with a subclass' do
47
+ resource(:poise_test, auto: false, step_into: false) do
48
+ include described_class
49
+ provides(:poise_other)
50
+ actions(:run)
51
+ end
52
+ resource(:poise_sub, parent: :poise_test, auto: false, step_into: false) do
53
+ provides(:poise_sub)
54
+ end
55
+ recipe do
56
+ poise_sub 'test'
57
+ end
58
+
59
+ it { is_expected.to run_poise_sub('test') }
60
+ it { expect(chef_run.poise_sub('test')).to be_a Chef::Resource }
61
+ end # /context with a subclass
45
62
  end
@@ -269,5 +269,77 @@ describe Poise::Helpers::Inversion do
269
269
  end
270
270
  end # /context with a provider
271
271
  end # /describe .resolve_inversion_provider
272
+
273
+ describe 'provider resolution' do
274
+ before do
275
+ default_attributes['poise'] ||= {}
276
+ default_attributes['poise']['provider'] = 'auto'
277
+ end
278
+ resource(:poise_test_inversion, step_into: false) do
279
+ include Poise
280
+ provides(:poise_test_inversion)
281
+ end
282
+ provider(:poise_test_inversion) do
283
+ include described_class
284
+ inversion_resource(:poise_test_inversion)
285
+ inversion_attribute([])
286
+ provides(:inverted)
287
+ def self.provides_auto?(*args); true; end
288
+ end
289
+ provider(:poise_test_inversion_other, parent: :poise_test_inversion) do
290
+ provides(:other)
291
+ end
292
+ let(:test_resource) { nil }
293
+ subject { Chef::ProviderResolver.new(chef_run.node, test_resource, :run) }
294
+
295
+ context 'with an inversion resource' do
296
+ recipe(subject: false) do
297
+ poise_test_inversion 'test'
298
+ end
299
+ let(:test_resource) { chef_run.poise_test_inversion('test') }
300
+
301
+ its(:enabled_handlers) { is_expected.to eq [provider(:poise_test_inversion), provider(:poise_test_inversion_other)] }
302
+ its(:resolve) { is_expected.to eq provider(:poise_test_inversion) }
303
+ end # /context with an inversion resource
304
+
305
+ context 'with a resource that has no providers' do
306
+ resource(:poise_not_found, step_into: false)
307
+ recipe(subject: false) do
308
+ poise_not_found 'test'
309
+ end
310
+ let(:test_resource) { chef_run.find_resource(:poise_not_found, 'test') }
311
+
312
+ its(:enabled_handlers) { is_expected.to eq [] }
313
+ it { expect { subject.resolve }.to raise_error defined?(Chef::Exceptions::ProviderNotFound) ? Chef::Exceptions::ProviderNotFound : ArgumentError }
314
+ end # /context with a resource that has no providers
315
+
316
+ context 'with a subclassed resource' do
317
+ resource(:poise_inversion_sub, parent: :poise_test_inversion, step_into: false) do
318
+ provides(:poise_inversion_sub)
319
+ end
320
+ recipe(subject: false) do
321
+ poise_inversion_sub 'test'
322
+ end
323
+ let(:test_resource) { chef_run.find_resource(:poise_inversion_sub, 'test') }
324
+
325
+ its(:enabled_handlers) { is_expected.to eq [] }
326
+ it { expect { subject.resolve }.to raise_error defined?(Chef::Exceptions::ProviderNotFound) ? Chef::Exceptions::ProviderNotFound : ArgumentError }
327
+ end # /context with a subclassed resource
328
+
329
+ context 'with a subclassed resource using subclass_providers!' do
330
+ resource(:poise_inversion_subproviders, parent: :poise_test_inversion, step_into: false) do
331
+ include Poise::Helpers::ResourceSubclass
332
+ provides(:poise_inversion_subproviders)
333
+ subclass_providers!
334
+ end
335
+ recipe(subject: false) do
336
+ poise_inversion_subproviders 'test'
337
+ end
338
+ let(:test_resource) { chef_run.find_resource(:poise_inversion_subproviders, 'test') }
339
+
340
+ its(:enabled_handlers) { is_expected.to eq [provider(:poise_test_inversion), provider(:poise_test_inversion_other)] }
341
+ its(:resolve) { is_expected.to eq provider(:poise_test_inversion) }
342
+ end # /context with a subclassed resource using subclass_providers!
343
+ end # /describe provider resolution
272
344
  end # /describe Poise::Helpers::Inversion::Provider
273
345
  end
@@ -140,6 +140,15 @@ describe Poise::Helpers::LWRPPolyfill do
140
140
 
141
141
  it { is_expected.to eq 'helper' }
142
142
  end # /context with an intermediary class
143
+
144
+ context 'with no new_resource' do
145
+ provider(:poise_test, auto: false) do
146
+ include described_class
147
+ end
148
+ subject { provider(:poise_test).new(nil, nil).load_current_resource }
149
+ it { is_expected.to be_a Chef::Resource }
150
+ it { is_expected.to_not be_a resource(:poise_test) }
151
+ end # context with no new_resource
143
152
  end # /describe load_current_resource override
144
153
 
145
154
  describe 'Chef::DSL::Recipe include' do
@@ -78,49 +78,85 @@ describe Poise::Helpers::OptionCollector do
78
78
  it { is_expected.to run_poise_test('test').with(options: {'one' => '1'}, value: 2) }
79
79
  end # /context with a normal attribute too
80
80
 
81
- context 'with a parser Proc' do
82
- resource(:poise_test) do
83
- include Poise::Helpers::LWRPPolyfill
84
- include described_class
85
- attribute(:options, option_collector: true, parser: proc {|val| parse(val) })
86
- def parse(val)
87
- {name: val}
88
- end
89
- end
81
+ context 'with an invalid key' do
90
82
  recipe do
91
83
  poise_test 'test' do
92
- options '1'
84
+ options do
85
+ one two
86
+ end
93
87
  end
94
88
  end
95
89
 
96
- it { is_expected.to run_poise_test('test').with(options: {'name' => '1'}) }
97
- end # /context with a parser Proc
90
+ it { expect { subject }.to raise_error NoMethodError }
91
+ end # /context with an invalid key
92
+
93
+ describe 'parser' do
94
+ context 'with a parser Proc' do
95
+ resource(:poise_test) do
96
+ include Poise::Helpers::LWRPPolyfill
97
+ include described_class
98
+ attribute(:options, option_collector: true, parser: proc {|val| parse(val) })
99
+ def parse(val)
100
+ {name: val}
101
+ end
102
+ end
103
+ recipe do
104
+ poise_test 'test' do
105
+ options '1'
106
+ end
107
+ end
98
108
 
99
- context 'with a parser Symbol' do
100
- resource(:poise_test) do
101
- include Poise::Helpers::LWRPPolyfill
102
- include described_class
103
- attribute(:options, option_collector: true, parser: :parse)
104
- def parse(val)
105
- {name: val}
109
+ it { is_expected.to run_poise_test('test').with(options: {'name' => '1'}) }
110
+ end # /context with a parser Proc
111
+
112
+ context 'with a parser Symbol' do
113
+ resource(:poise_test) do
114
+ include Poise::Helpers::LWRPPolyfill
115
+ include described_class
116
+ attribute(:options, option_collector: true, parser: :parse)
117
+ def parse(val)
118
+ {name: val}
119
+ end
106
120
  end
107
- end
121
+ recipe do
122
+ poise_test 'test' do
123
+ options '1'
124
+ end
125
+ end
126
+
127
+ it { is_expected.to run_poise_test('test').with(options: {'name' => '1'}) }
128
+ end # /context with a parser Symbol
129
+
130
+ context 'with an invalid parser' do
131
+ it do
132
+ expect do
133
+ resource(:poise_test).send(:attribute, :options, option_collector: true, parser: 'invalid')
134
+ end.to raise_error(Poise::Error)
135
+ end
136
+ end # /context with an invalid parser
137
+ end # /describe parser
138
+
139
+ describe 'forced_keys' do
108
140
  recipe do
109
141
  poise_test 'test' do
110
- options '1'
142
+ options do
143
+ name 'foo'
144
+ end
111
145
  end
112
146
  end
113
147
 
114
- it { is_expected.to run_poise_test('test').with(options: {'name' => '1'}) }
115
- end # /context with a parser Symbol
148
+ context 'without forced_keys' do
149
+ it { is_expected.to run_poise_test('test').with(options: {}) }
150
+ end # /context without forced_keys
116
151
 
117
- context 'with an invalid parse' do
118
- it do
119
- expect do
120
- resource(:poise_test).send(:attribute, :options, option_collector: true, parser: 'invalid')
121
- end.to raise_error(Poise::Error)
122
- end
123
- end # /context with an invalid parser
152
+ context 'with forced_keys' do
153
+ resource(:poise_test) do
154
+ include described_class
155
+ attribute(:options, option_collector: true, forced_keys: %i{name})
156
+ end
157
+ it { is_expected.to run_poise_test('test').with(options: {'name' => 'foo'}) }
158
+ end # /context with forced_keys
159
+ end # /describe forced_keys
124
160
 
125
161
  # TODO: Write tests for mixed symbol/string data
126
162
  end