diff options
author | John Keiser <john@johnkeiser.com> | 2015-07-03 14:16:32 -0600 |
---|---|---|
committer | John Keiser <john@johnkeiser.com> | 2015-07-03 14:16:32 -0600 |
commit | cfd2b1fa1b26e8b0aa92a5ba8bd294b96379b2fa (patch) | |
tree | d0715ed8eaed714460b168103ef39b748e85b8ba | |
parent | ed2b0904361448173e5644dddacb48399fd8dc68 (diff) | |
parent | 78f72c82716acbd504f6974555cb45d61e895032 (diff) | |
download | chef-cfd2b1fa1b26e8b0aa92a5ba8bd294b96379b2fa.tar.gz |
Merge branch 'jk/property-state'
-rw-r--r-- | lib/chef/constants.rb | 27 | ||||
-rw-r--r-- | lib/chef/delayed_evaluator.rb | 21 | ||||
-rw-r--r-- | lib/chef/exceptions.rb | 1 | ||||
-rw-r--r-- | lib/chef/mixin/params_validate.rb | 192 | ||||
-rw-r--r-- | lib/chef/property.rb | 539 | ||||
-rw-r--r-- | lib/chef/resource.rb | 371 | ||||
-rw-r--r-- | spec/unit/mixin/params_validate_spec.rb | 6 | ||||
-rw-r--r-- | spec/unit/property/state_spec.rb | 654 | ||||
-rw-r--r-- | spec/unit/property/validation_spec.rb | 54 | ||||
-rw-r--r-- | spec/unit/property_spec.rb | 258 | ||||
-rw-r--r-- | spec/unit/resource_spec.rb | 4 |
11 files changed, 1542 insertions, 585 deletions
diff --git a/lib/chef/constants.rb b/lib/chef/constants.rb new file mode 100644 index 0000000000..d39ce4c68d --- /dev/null +++ b/lib/chef/constants.rb @@ -0,0 +1,27 @@ +# +# Author:: John Keiser <jkeiser@chef.io> +# Copyright:: Copyright (c) 2015 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class Chef + NOT_PASSED = Object.new + def NOT_PASSED.to_s + "NOT_PASSED" + end + def NOT_PASSED.inspect + to_s + end + NOT_PASSED.freeze +end diff --git a/lib/chef/delayed_evaluator.rb b/lib/chef/delayed_evaluator.rb new file mode 100644 index 0000000000..9f18a53445 --- /dev/null +++ b/lib/chef/delayed_evaluator.rb @@ -0,0 +1,21 @@ +# +# Author:: John Keiser <jkeiser@chef.io> +# Copyright:: Copyright (c) 2015 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +class Chef + class DelayedEvaluator < Proc + end +end diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index e7da6fc110..d5c4c4832b 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -103,6 +103,7 @@ class Chef class ProviderNotFound < RuntimeError; end NoProviderAvailable = ProviderNotFound class VerificationNotFound < RuntimeError; end + class MultipleIdentityError < RuntimeError; end # Can't find a Resource of this type that is valid on this platform. class NoSuchResourceType < NameError diff --git a/lib/chef/mixin/params_validate.rb b/lib/chef/mixin/params_validate.rb index 4ab016249f..322caea474 100644 --- a/lib/chef/mixin/params_validate.rb +++ b/lib/chef/mixin/params_validate.rb @@ -15,11 +15,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -class Chef - NOT_PASSED = Object.new +require 'chef/constants' +require 'chef/property' +require 'chef/delayed_evaluator' - class DelayedEvaluator < Proc - end +class Chef module Mixin module ParamsValidate @@ -34,20 +34,55 @@ class Chef # Would raise an exception if the value of :one above is not a kind_of? string. Valid # map options are: # - # :default:: Sets the default value for this parameter. - # :callbacks:: Takes a hash of Procs, which should return true if the argument is valid. - # The key will be inserted into the error message if the Proc does not return true: - # "Option #{key}'s value #{value} #{message}!" - # :kind_of:: Ensure that the value is a kind_of?(Whatever). If passed an array, it will ensure - # that the value is one of those types. - # :respond_to:: Ensure that the value has a given method. Takes one method name or an array of - # method names. - # :required:: Raise an exception if this parameter is missing. Valid values are true or false, - # by default, options are not required. - # :regex:: Match the value of the parameter against a regular expression. - # :equal_to:: Match the value of the parameter with ==. An array means it can be equal to any - # of the values. + # @param opts [Hash<Symbol,Object>] Validation opts. + # @option opts [Object,Array] :is An object, or list of + # objects, that must match the value using Ruby's `===` operator + # (`opts[:is].any? { |v| v === value }`). (See #_pv_is.) + # @option opts [Object,Array] :equal_to An object, or list + # of objects, that must be equal to the value using Ruby's `==` + # operator (`opts[:is].any? { |v| v == value }`) (See #_pv_equal_to.) + # @option opts [Regexp,Array<Regexp>] :regex An object, or + # list of objects, that must match the value with `regex.match(value)`. + # (See #_pv_regex) + # @option opts [Class,Array<Class>] :kind_of A class, or + # list of classes, that the value must be an instance of. (See + # #_pv_kind_of.) + # @option opts [Hash<String,Proc>] :callbacks A hash of + # messages -> procs, all of which match the value. The proc must + # return a truthy or falsey value (true means it matches). (See + # #_pv_callbacks.) + # @option opts [Symbol,Array<Symbol>] :respond_to A method + # name, or list of method names, the value must respond to. (See + # #_pv_respond_to.) + # @option opts [Symbol,Array<Symbol>] :cannot_be A property, + # or a list of properties, that the value cannot have (such as `:nil` or + # `:empty`). The method with a questionmark at the end is called on the + # value (e.g. `value.empty?`). If the value does not have this method, + # it is considered valid (i.e. if you don't respond to `empty?` we + # assume you are not empty). (See #_pv_cannot_be.) + # @option opts [Proc] :coerce A proc which will be called to + # transform the user input to canonical form. The value is passed in, + # and the transformed value returned as output. Lazy values will *not* + # be passed to this method until after they are evaluated. Called in the + # context of the resource (meaning you can access other properties). + # (See #_pv_coerce.) (See #_pv_coerce.) + # @option opts [Boolean] :required `true` if this property + # must be present and not `nil`; `false` otherwise. This is checked + # after the resource is fully initialized. (See #_pv_required.) + # @option opts [Boolean] :name_property `true` if this + # property defaults to the same value as `name`. Equivalent to + # `default: lazy { name }`, except that #property_is_set? will + # return `true` if the property is set *or* if `name` is set. (See + # #_pv_name_property.) + # @option opts [Boolean] :name_attribute Same as `name_property`. + # @option opts [Object] :default The value this property + # will return if the user does not set one. If this is `lazy`, it will + # be run in the context of the instance (and able to access other + # properties). (See #_pv_default.) + # def validate(opts, map) + map = map.validation_options if map.is_a?(Property) + #-- # validate works by taking the keys in the validation map, assuming it's a hash, and # looking for _pv_:symbol as methods. Assuming it find them, it calls the right @@ -84,91 +119,8 @@ class Chef end def set_or_return(symbol, value, validation) - symbol = symbol.to_sym - iv_symbol = :"@#{symbol}" - - # Steal default, coerce, name_property and required from validation - # so that we can handle the order in which they are applied - validation = validation.dup - if validation.has_key?(:default) - default = validation.delete(:default) - elsif validation.has_key?('default') - default = validation.delete('default') - else - default = NOT_PASSED - end - coerce = validation.delete(:coerce) - coerce ||= validation.delete('coerce') - name_property = validation.delete(:name_property) - name_property ||= validation.delete('name_property') - name_property ||= validation.delete(:name_attribute) - name_property ||= validation.delete('name_attribute') - required = validation.delete(:required) - required ||= validation.delete('required') - - opts = {} - # If the user passed NOT_PASSED, or passed nil, then this is a get. - if value == NOT_PASSED || (value.nil? && !explicitly_allows_nil?(symbol, validation)) - - # Get the value if there is one - if self.instance_variable_defined?(iv_symbol) - opts[symbol] = self.instance_variable_get(iv_symbol) - - # Handle lazy values - if opts[symbol].is_a?(DelayedEvaluator) - if opts[symbol].arity >= 1 - opts[symbol] = opts[symbol].call(self) - else - opts[symbol] = opts[symbol].call - end - - # Coerce and validate the default value - _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required - _pv_coerce(opts, symbol, coerce) if coerce - validate(opts, { symbol => validation }) - end - - # Get the default value - else - _pv_default(opts, symbol, default) unless default == NOT_PASSED - _pv_name_property(opts, symbol, name_property) - _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required - - if opts.has_key?(symbol) - # Handle lazy defaults. - if opts[symbol].is_a?(DelayedEvaluator) - if opts[symbol].arity >= 1 - opts[symbol] = opts[symbol].call(self) - else - opts[symbol] = instance_eval(&opts[symbol]) - end - end - - # Coerce and validate the default value - _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required - _pv_coerce(opts, symbol, coerce) if coerce - # We presently do not validate defaults, for backwards compatibility. -# validate(opts, { symbol => validation }) - - # Defaults are presently "stickily" set on the instance - self.instance_variable_set(iv_symbol, opts[symbol]) - end - end - - # Set the value - else - opts[symbol] = value - unless opts[symbol].is_a?(DelayedEvaluator) - # Coerce and validate the value - _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required - _pv_coerce(opts, symbol, coerce) if coerce - validate(opts, { symbol => validation }) - end - - self.instance_variable_set(iv_symbol, opts[symbol]) - end - - opts[symbol] + property = SetOrReturnProperty.new(name: symbol, **validation) + property.call(self, value) end private @@ -193,8 +145,9 @@ class Chef if is_required return true if opts.has_key?(key.to_s) && (explicitly_allows_nil || !opts[key.to_s].nil?) return true if opts.has_key?(key.to_sym) && (explicitly_allows_nil || !opts[key.to_sym].nil?) - raise Exceptions::ValidationFailed, "Required argument #{key} is missing!" + raise Exceptions::ValidationFailed, "Required argument #{key.inspect} is missing!" end + true end # @@ -425,9 +378,9 @@ class Chef # x 1 #=> invalid # ``` # - # @example PropertyType + # @example Property # ```ruby - # type = PropertyType.new(is: String) + # type = Property.new(is: String) # property :x, type # x 'foo' #=> valid # x 1 #=> invalid @@ -448,8 +401,12 @@ class Chef value = _pv_opts_lookup(opts, key) to_be = [ to_be ].flatten(1) to_be.each do |tb| - if tb.is_a?(Proc) + case tb + when Proc return true if instance_exec(value, &tb) + when Property + validate(opts, { key => tb.validation_options }) + return true else return true if tb === value end @@ -484,6 +441,29 @@ class Chef opts[key.to_sym] = instance_exec(opts[key], &coercer) end end + + # Used by #set_or_return to avoid emitting a deprecation warning for + # "value nil" and to keep default stickiness working exactly the same + # @api private + class SetOrReturnProperty < Chef::Property + def get(resource) + value = super + # All values are sticky, frozen or not + if !is_set?(resource) + set_value(resource, value) + end + value + end + + def call(resource, value=NOT_PASSED) + # setting to nil does a get + if value.nil? && !explicitly_accepts_nil?(resource) + get(resource) + else + super + end + end + end end end end diff --git a/lib/chef/property.rb b/lib/chef/property.rb new file mode 100644 index 0000000000..408090d37b --- /dev/null +++ b/lib/chef/property.rb @@ -0,0 +1,539 @@ +# +# Author:: John Keiser <jkeiser@chef.io> +# Copyright:: Copyright (c) 2015 John Keiser. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/exceptions' +require 'chef/delayed_evaluator' + +class Chef + # + # Type and validation information for a property on a resource. + # + # A property named "x" manipulates the "@x" instance variable on a + # resource. The *presence* of the variable (`instance_variable_defined?(@x)`) + # tells whether the variable is defined; it may have any actual value, + # constrained only by validation. + # + # Properties may have validation, defaults, and coercion, and have full + # support for lazy values. + # + # @see Chef::Resource.property + # @see Chef::DelayedEvaluator + # + class Property + # + # Create a reusable property type that can be used in multiple properties + # in different resources. + # + # @param options [Hash<Symbol,Object>] Validation options. See Chef::Resource.property for + # the list of options. + # + # @example + # Property.derive(default: 'hi') + # + def self.derive(**options) + new(**options) + end + + # + # Create a new property. + # + # @param options [Hash<Symbol,Object>] Property options, including + # control options here, as well as validation options (see + # Chef::Mixin::ParamsValidate#validate for a description of validation + # options). + # @option options [Symbol] :name The name of this property. + # @option options [Class] :declared_in The class this property comes from. + # @option options [Symbol] :instance_variable_name The instance variable + # tied to this property. Must include a leading `@`. Defaults to `@<name>`. + # `nil` means the property is opaque and not tied to a specific instance + # variable. + # @option options [Boolean] :desired_state `true` if this property is part of desired + # state. Defaults to `true`. + # @option options [Boolean] :identity `true` if this property is part of object + # identity. Defaults to `false`. + # @option options [Boolean] :name_property `true` if this + # property defaults to the same value as `name`. Equivalent to + # `default: lazy { name }`, except that #property_is_set? will + # return `true` if the property is set *or* if `name` is set. + # @option options [Object] :default The value this property + # will return if the user does not set one. If this is `lazy`, it will + # be run in the context of the instance (and able to access other + # properties) and cached. If not, the value will be frozen with Object#freeze + # to prevent users from modifying it in an instance. + # @option options [Proc] :coerce A proc which will be called to + # transform the user input to canonical form. The value is passed in, + # and the transformed value returned as output. Lazy values will *not* + # be passed to this method until after they are evaluated. Called in the + # context of the resource (meaning you can access other properties). + # @option options [Boolean] :required `true` if this property + # must be present; `false` otherwise. This is checked after the resource + # is fully initialized. + # + def initialize(**options) + options.each { |k,v| options[k.to_sym] = v if k.is_a?(String) } + options[:name_property] = options.delete(:name_attribute) unless options.has_key?(:name_property) + @options = options + + if options.has_key?(:default) + options[:default] = options[:default].freeze + end + options[:name] = options[:name].to_sym if options[:name] + options[:instance_variable_name] = options[:instance_variable_name].to_sym if options[:instance_variable_name] + end + + # + # The name of this property. + # + # @return [String] + # + def name + options[:name] + end + + # + # The class this property was defined in. + # + # @return [Class] + # + def declared_in + options[:declared_in] + end + + # + # The instance variable associated with this property. + # + # Defaults to `@<name>` + # + # @return [Symbol] + # + def instance_variable_name + if options.has_key?(:instance_variable_name) + options[:instance_variable_name] + elsif name + :"@#{name}" + end + end + + # + # The raw default value for this resource. + # + # Does not coerce or validate the default. Does not evaluate lazy values. + # + # Defaults to `lazy { name }` if name_property is true; otherwise defaults to + # `nil` + # + def default + return options[:default] if options.has_key?(:default) + return Chef::DelayedEvaluator.new { name } if name_property? + nil + end + + # + # Whether this is part of the resource's natural identity or not. + # + # @return [Boolean] + # + def identity? + options[:identity] + end + + # + # Whether this is part of desired state or not. + # + # Defaults to true. + # + # @return [Boolean] + # + def desired_state? + return true if !options.has_key?(:desired_state) + options[:desired_state] + end + + # + # Whether this is name_property or not. + # + # @return [Boolean] + # + def name_property? + options[:name_property] + end + + # + # Whether this property has a default value. + # + # @return [Boolean] + # + def has_default? + options.has_key?(:default) || name_property? + end + + # + # Whether this property is required or not. + # + # @return [Boolean] + # + def required? + options[:required] + end + + # + # Validation options. (See Chef::Mixin::ParamsValidate#validate.) + # + # @return [Hash<Symbol,Object>] + # + def validation_options + @validation_options ||= options.reject { |k,v| + [:declared_in,:name,:instance_variable_name,:desired_state,:identity,:default,:name_property,:coerce,:required].include?(k) + } + end + + # + # Handle the property being called. + # + # The base implementation does the property get-or-set: + # + # ```ruby + # resource.myprop # get + # resource.myprop value # set + # ``` + # + # Subclasses may implement this with any arguments they want, as long as + # the corresponding DSL calls it correctly. + # + # @param resource [Chef::Resource] The resource to get the property from. + # @param value The value to set (or NOT_PASSED if it is a get). + # + # @return The current value of the property. If it is a `set`, lazy values + # will be returned without running, validating or coercing. If it is a + # `get`, the non-lazy, coerced, validated value will always be returned. + # + def call(resource, value=NOT_PASSED) + if value == NOT_PASSED + return get(resource) + end + + # myprop nil is sometimes a get (backcompat) + if value.nil? && !explicitly_accepts_nil?(resource) + # If you say "my_property nil" and the property explicitly accepts + # nil values, we consider this a get. + Chef::Log.deprecation("#{name} nil currently does not overwrite the value of #{name}. This will change in Chef 13, and the value will be set to nil instead. Please change your code to explicitly accept nil using \"property :#{name}, [MyType, nil]\", or stop setting this value to nil.") + return get(resource) + end + + # Anything else (myprop value) is a set + set(resource, value) + end + + # + # Get the property value from the resource, handling lazy values, + # defaults, and validation. + # + # - If the property's value is lazy, it is evaluated, coerced and validated. + # - If the property has no value, and is required, raises ValidationFailed. + # - If the property has no value, but has a lazy default, it is evaluated, + # coerced and validated. If the evaluated value is frozen, the resulting + # - If the property has no value, but has a default, the default value + # will be returned and frozen. If the default value is lazy, it will be + # evaluated, coerced and validated, and the result stored in the property. + # - If the property has no value, but is name_property, `resource.name` + # is retrieved, coerced, validated and stored in the property. + # - Otherwise, `nil` is returned. + # + # @param resource [Chef::Resource] The resource to get the property from. + # + # @return The value of the property. + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property, or if the value is required and not set. + # + def get(resource) + if is_set?(resource) + value = get_value(resource) + if value.is_a?(DelayedEvaluator) + value = exec_in_resource(resource, value) + value = coerce(resource, value) + validate(resource, value) + end + value + + else + if has_default? + value = default + if value.is_a?(DelayedEvaluator) + value = exec_in_resource(resource, value) + end + + value = coerce(resource, value) + + # We don't validate defaults + + # If the value is mutable (non-frozen), we set it on the instance + # so that people can mutate it. (All constant default values are + # frozen.) + if !value.frozen? + set_value(resource, value) + end + + value + + elsif required? + raise Chef::Exceptions::ValidationFailed, "#{name} is required" + end + end + end + + # + # Set the value of this property in the given resource. + # + # Non-lazy values are coerced and validated before being set. Coercion + # and validation of lazy values is delayed until they are first retrieved. + # + # @param resource [Chef::Resource] The resource to set this property in. + # @param value The value to set. + # + # @return The value that was set, after coercion (if lazy, still returns + # the lazy value) + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property. + # + def set(resource, value) + unless value.is_a?(DelayedEvaluator) + value = coerce(resource, value) + validate(resource, value) + end + set_value(resource, value) + end + + # + # Find out whether this property has been set. + # + # This will be true if: + # - The user explicitly set the value + # - The property has a default, and the value was retrieved. + # + # From this point of view, it is worth looking at this as "what does the + # user think this value should be." In order words, if the user grabbed + # the value, even if it was a default, they probably based calculations on + # it. If they based calculations on it and the value changes, the rest of + # the world gets inconsistent. + # + # @param resource [Chef::Resource] The resource to get the property from. + # + # @return [Boolean] + # + def is_set?(resource) + value_is_set?(resource) + end + + # + # Reset the value of this property so that is_set? will return false and the + # default will be returned in the future. + # + # @param resource [Chef::Resource] The resource to get the property from. + # + def reset(resource) + reset_value(resource) + end + + # + # Coerce an input value into canonical form for the property. + # + # After coercion, the value is suitable for storage in the resource. + # You must validate values after coercion, however. + # + # Does no special handling for lazy values. + # + # @param resource [Chef::Resource] The resource we're coercing against + # (to provide context for the coerce). + # @param value The value to coerce. + # + # @return The coerced value. + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property. + # + def coerce(resource, value) + if options.has_key?(:coerce) + value = exec_in_resource(resource, options[:coerce], value) + end + value + end + + # + # Validate a value. + # + # Calls Chef::Mixin::ParamsValidate#validate with #validation_options as + # options. + # + # @param resource [Chef::Resource] The resource we're validating against + # (to provide context for the validate). + # @param value The value to validate. + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property. + # + def validate(resource, value) + resource.validate({ name => value }, { name => validation_options }) + end + + # + # Derive a new Property that is just like this one, except with some added or + # changed options. + # + # @param options [Hash<Symbol,Object>] List of options that would be passed + # to #initialize. + # + # @return [Property] The new property type. + # + def derive(**modified_options) + Property.new(**options.merge(**modified_options)) + end + + # + # Emit the DSL for this property into the resource class (`declared_in`). + # + # Creates a getter and setter for the property. + # + def emit_dsl + # We don't create the getter/setter if it's a custom property; we will + # be using the existing getter/setter to manipulate it instead. + return if !instance_variable_name + + # We prefer this form because the property name won't show up in the + # stack trace if you use `define_method`. + declared_in.class_eval <<-EOM, __FILE__, __LINE__+1 + def #{name}(value=NOT_PASSED) + self.class.properties[#{name.inspect}].call(self, value) + end + def #{name}=(value) + self.class.properties[#{name.inspect}].set(self, value) + end + EOM + rescue SyntaxError + # If the name is not a valid ruby name, we use define_method. + resource_class.define_method(name) do |value=NOT_PASSED| + self.class.properties[name].call(self, value) + end + resource_class.define_method("#{name}=") do |value| + self.class.properties[name].set(self, value) + end + end + + protected + + # + # The options this Property will use for get/set behavior and validation. + # + # @see #initialize for a list of valid options. + # + attr_reader :options + + # + # Find out whether this type accepts nil explicitly. + # + # A type accepts nil explicitly if "is" allows nil, it validates as nil, *and* is not simply + # an empty type. + # + # These examples accept nil explicitly: + # ```ruby + # property :a, [ String, nil ] + # property :a, [ String, NilClass ] + # property :a, [ String, proc { |v| v.nil? } ] + # ``` + # + # This does not (because the "is" doesn't exist or doesn't have nil): + # + # ```ruby + # property :x, String + # ``` + # + # These do not, even though nil would validate fine (because they do not + # have "is"): + # + # ```ruby + # property :a + # property :a, equal_to: [ 1, 2, 3, nil ] + # property :a, kind_of: [ String, NilClass ] + # property :a, respond_to: [ ] + # property :a, callbacks: { "a" => proc { |v| v.nil? } } + # ``` + # + # @param resource [Chef::Resource] The resource we're coercing against + # (to provide context for the coerce). + # + # @return [Boolean] Whether this value explicitly accepts nil. + # + # @api private + def explicitly_accepts_nil?(resource) + options.has_key?(:is) && resource.send(:_pv_is, { name => nil }, name, options[:is], raise_error: false) + end + + def get_value(resource) + if instance_variable_name + resource.instance_variable_get(instance_variable_name) + else + resource.send(name) + end + end + + def set_value(resource, value) + if instance_variable_name + resource.instance_variable_set(instance_variable_name, value) + else + resource.send(name, value) + end + end + + def value_is_set?(resource) + if instance_variable_name + resource.instance_variable_defined?(instance_variable_name) + else + true + end + end + + def reset_value(resource) + if instance_variable_name + if value_is_set?(resource) + resource.remove_instance_variable(instance_variable_name) + end + else + raise ArgumentError, "Property #{name} has no instance variable defined and cannot be reset" + end + end + + def exec_in_resource(resource, proc, *args) + if resource + if proc.arity > args.size + value = proc.call(resource, *args) + else + value = resource.instance_exec(*args, &proc) + end + else + value = proc.call + end + + if value.is_a?(DelayedEvaluator) + value = coerce(resource, value) + validate(resource, value) + end + value + end + end +end diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 696089fe3e..5e9ba42703 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -101,7 +101,7 @@ class Chef # @param run_context The context of the Chef run. Corresponds to #run_context. # def initialize(name, run_context=nil) - name(name) + name(name) unless name.nil? @run_context = run_context @noop = nil @before = nil @@ -130,37 +130,27 @@ class Chef end # - # The name of this particular resource. + # The list of properties defined on this resource. # - # This special resource attribute is set automatically from the declaration - # of the resource, e.g. + # Everything defined with `property` is in this list. # - # execute 'Vitruvius' do - # command 'ls' - # end - # - # Will set the name to "Vitruvius". + # @param include_superclass [Boolean] `true` to include properties defined + # on superclasses; `false` or `nil` to return the list of properties + # directly on this class. # - # This is also used in to_s to show the resource name, e.g. `execute[Vitruvius]`. - # - # This is also used for resource notifications and subscribes in the same manner. - # - # This will coerce any object into a string via #to_s. Arrays are a special case - # so that `package ["foo", "bar"]` becomes package[foo, bar] instead of the more - # awkward `package[["foo", "bar"]]` that #to_s would produce. - # - # @param name [Object] The name to set, typically a String or Array - # @return [String] The name of this Resource. + # @return [Hash<Symbol,Property>] The list of property names and types. # - def name(name=nil) - if !name.nil? - if name.is_a?(Array) - @name = name.join(', ') + def self.properties(include_superclass=true) + @properties ||= {} + if include_superclass + if superclass.respond_to?(:properties) + superclass.properties.merge(@properties) else - @name = name.to_s + @properties.dup end + else + @properties end - @name end # @@ -475,13 +465,21 @@ class Chef # # Get the value of the state attributes in this resource as a hash. # + # Does not include properties that are not set (unless they are identity + # properties). + # # @return [Hash{Symbol => Object}] A Hash of attribute => value for the # Resource class's `state_attrs`. + # def state_for_resource_reporter - self.class.state_attrs.inject({}) do |state_attrs, attr_name| - state_attrs[attr_name] = send(attr_name) - state_attrs + state = {} + state_properties = self.class.state_properties + state_properties.each do |property| + if property.identity? || property.is_set?(self) + state[property.name] = send(property.name) + end end + state end # @@ -494,17 +492,22 @@ class Chef alias_method :state, :state_for_resource_reporter # - # The value of the identity attribute, if declared. Falls back to #name if - # no identity attribute is declared. + # The value of the identity of this resource. # - # @return The value of the identity attribute. + # - If there are no identity properties on the resource, `name` is returned. + # - If there is exactly one identity property on the resource, it is returned. + # - If there are more than one, they are returned in a hash. + # + # @return [Object,Hash<Symbol,Object>] The identity of this resource. # def identity - if identity_attr = self.class.identity_attr - send(identity_attr) - else - name + result = {} + identity_properties = self.class.identity_properties + identity_properties.each do |property| + result[property.name] = send(property.name) end + return result.values.first if identity_properties.size == 1 + result end # @@ -756,6 +759,10 @@ class Chef # will return if the user does not set one. If this is `lazy`, it will # be run in the context of the instance (and able to access other # properties). + # @option options [Boolean] :desired_state `true` if this property is + # part of desired state. Defaults to `true`. + # @option options [Boolean] :identity `true` if this property + # is part of object identity. Defaults to `false`. # # @example Bare property # property :x @@ -772,29 +779,100 @@ class Chef def self.property(name, type=NOT_PASSED, **options) name = name.to_sym - if type != NOT_PASSED + options.merge!(name: name, declared_in: self) + + if type == NOT_PASSED + # If a type is not passed, the property derives from the + # superclass property (if any) + if properties.has_key?(name) + property = properties[name].derive(**options) + else + property = property_type(**options) + end + + # If a Property is specified, derive a new one from that. + elsif type.is_a?(Property) || (type.is_a?(Class) && type <= Property) + property = type.derive(**options) + + # If a primitive type was passed, combine it with "is" + else if options[:is] options[:is] = ([ type ] + [ options[:is] ]).flatten(1) else options[:is] = type end + property = property_type(**options) end - define_method(name) do |value=NOT_PASSED| - set_or_return(name, value, options) - end - define_method("#{name}=") do |value| - set_or_return(name, value, options) - end + local_properties = properties(false) + local_properties[name] = property + + property.emit_dsl end # + # Create a reusable property type that can be used in multiple properties + # in different resources. + # + # @param options [Hash<Symbol,Object>] Validation options. see #property for + # the list of options. + # + # @example + # property_type(default: 'hi') + # + def self.property_type(**options) + Property.derive(**options) + end + + # + # The name of this particular resource. + # + # This special resource attribute is set automatically from the declaration + # of the resource, e.g. + # + # execute 'Vitruvius' do + # command 'ls' + # end + # + # Will set the name to "Vitruvius". + # + # This is also used in to_s to show the resource name, e.g. `execute[Vitruvius]`. + # + # This is also used for resource notifications and subscribes in the same manner. + # + # This will coerce any object into a string via #to_s. Arrays are a special case + # so that `package ["foo", "bar"]` becomes package[foo, bar] instead of the more + # awkward `package[["foo", "bar"]]` that #to_s would produce. + # + # @param name [Object] The name to set, typically a String or Array + # @return [String] The name of this Resource. + # + property :name, String, coerce: proc { |v| v.is_a?(Array) ? v.join(', ') : v.to_s }, desired_state: false + + # # Whether this property has been set (or whether it has a default that has # been retrieved). # + # @param name [Symbol] The name of the property. + # @return [Boolean] `true` if the property has been set. + # def property_is_set?(name) - name = name.to_sym - instance_variable_defined?("@#{name}") + property = self.class.properties[name.to_sym] + raise ArgumentError, "Property #{name} is not defined in class #{self}" if !property + property.is_set?(self) + end + + # + # Clear this property as if it had never been set. It will thereafter return + # the default. + # been retrieved). + # + # @param name [Symbol] The name of the property. + # + def reset_property(name) + property = self.class.properties[name.to_sym] + raise ArgumentError, "Property #{name} is not defined in class #{self}" if !property + property.reset(self) end # @@ -808,46 +886,186 @@ class Chef DelayedEvaluator.new(&block) end - # Set or return the list of "state attributes" implemented by the Resource - # subclass. State attributes are attributes that describe the desired state - # of the system, such as file permissions or ownership. In general, state - # attributes are attributes that could be populated by examining the state - # of the system (e.g., File.stat can tell you the permissions on an - # existing file). Contrarily, attributes that are not "state attributes" - # usually modify the way Chef itself behaves, for example by providing - # additional options for a package manager to use when installing a - # package. + # + # Get or set the list of desired state properties for this resource. + # + # State properties are properties that describe the desired state + # of the system, such as file permissions or ownership. + # In general, state properties are properties that could be populated by + # examining the state of the system (e.g., File.stat can tell you the + # permissions on an existing file). Contrarily, properties that are not + # "state properties" usually modify the way Chef itself behaves, for example + # by providing additional options for a package manager to use when + # installing a package. # # This list is used by the Chef client auditing system to extract # information from resources to describe changes made to the system. - def self.state_attrs(*attr_names) - @state_attrs ||= [] - @state_attrs = attr_names unless attr_names.empty? + # + # This method is unnecessary when declaring properties with `property`; + # properties are added to state_properties by default, and can be turned off + # with `desired_state: false`. + # + # ```ruby + # property :x # part of desired state + # property :y, desired_state: false # not part of desired state + # ``` + # + # @param names [Array<Symbol>] A list of property names to set as desired + # state. + # + # @return [Array<Property>] All properties in desired state. + # + def self.state_properties(*names) + if !names.empty? + names = names.map { |name| name.to_sym }.uniq - # Return *all* state_attrs that this class has, including inherited ones - if superclass.respond_to?(:state_attrs) - superclass.state_attrs + @state_attrs - else - @state_attrs + local_properties = properties(false) + # Add new properties to the list. + names.each do |name| + property = properties[name] + if !property + self.property name, instance_variable_name: false, desired_state: true + elsif !property.desired_state? + self.property name, desired_state: true + end + end + + # If state_attrs *excludes* something which is currently desired state, + # mark it as desired_state: false. + local_properties.each do |name,property| + if property.desired_state? && !names.include?(name) + self.property name, desired_state: false + end + end end + + properties.values.select { |property| property.desired_state? } end - # Set or return the "identity attribute" for this resource class. This is - # generally going to be the "name attribute" for this resource. In other - # words, the resource type plus this attribute uniquely identify a given - # bit of state that chef manages. For a File resource, this would be the - # path, for a package resource, it will be the package name. This will show - # up in chef-client's audit records as a searchable field. - def self.identity_attr(attr_name=nil) - @identity_attr ||= nil - @identity_attr = attr_name if attr_name + # + # Set or return the list of "state properties" implemented by the Resource + # subclass. + # + # Equivalent to calling #state_properties and getting `state_properties.keys`. + # + # @deprecated Use state_properties.keys instead. Note that when you declare + # properties with `property`: properties are added to state_properties by + # default, and can be turned off with `desired_state: false` + # + # ```ruby + # property :x # part of desired state + # property :y, desired_state: false # not part of desired state + # ``` + # + # @param names [Array<Symbol>] A list of property names to set as desired + # state. + # + # @return [Array<Symbol>] All property names with desired state. + # + def self.state_attrs(*names) + state_properties(*names).map { |property| property.name } + end - # If this class doesn't have an identity attr, we'll defer to the superclass: - if @identity_attr || !superclass.respond_to?(:identity_attr) - @identity_attr - else - superclass.identity_attr + # + # Set the identity of this resource to a particular set of properties. + # + # This drives #identity, which returns data that uniquely refers to a given + # resource on the given node (in such a way that it can be correlated + # across Chef runs). + # + # This method is unnecessary when declaring properties with `property`; + # properties can be added to identity during declaration with + # `identity: true`. + # + # ```ruby + # property :x, identity: true # part of identity + # property :y # not part of identity + # ``` + # + # If no properties are marked as identity, "name" is considered the identity. + # + # @param names [Array<Symbol>] A list of property names to set as the identity. + # + # @return [Array<Property>] All identity properties. + # + def self.identity_properties(*names) + if !names.empty? + names = names.map { |name| name.to_sym } + + # Add or change properties that are not part of the identity. + names.each do |name| + property = properties[name] + if !property + self.property name, instance_variable_name: false, identity: true + elsif !property.identity? + self.property name, identity: true + end + end + + # If state_attrs *excludes* something which is currently part of the + # identity, mark it as identity: false. + properties.each do |name,property| + if property.identity? && !names.include?(name) + self.property name, identity: false + end + end + end + + result = properties.values.select { |property| property.identity? } + result = [ properties[:name] ] if result.empty? + result + end + + # + # Set the identity of this resource to a particular property. + # + # This drives #identity, which returns data that uniquely refers to a given + # resource on the given node (in such a way that it can be correlated + # across Chef runs). + # + # This method is unnecessary when declaring properties with `property`; + # properties can be added to identity during declaration with + # `identity: true`. + # + # ```ruby + # property :x, identity: true # part of identity + # property :y # not part of identity + # ``` + # + # @param name [Symbol] A list of property names to set as the identity. + # + # @return [Symbol] The identity property if there is only one; or `nil` if + # there are more than one. + # + # @raise [ArgumentError] If no arguments are passed and the resource has + # more than one identity property. + # + def self.identity_property(name=nil) + result = identity_properties(*Array(name)) + if result.size > 1 + raise Chef::Exceptions::MultipleIdentityError, "identity_property cannot be called on an object with more than one identity property (#{result.map { |r| r.name }.join(", ")})." end + result.first + end + + # + # Set a property as the "identity attribute" for this resource. + # + # Identical to calling #identity_property.first.key. + # + # @param name [Symbol] The name of the property to set. + # + # @return [Symbol] + # + # @deprecated `identity_property` should be used instead. + # + # @raise [ArgumentError] If no arguments are passed and the resource has + # more than one identity property. + # + def self.identity_attr(name=nil) + property = identity_property(name) + return nil if !property + property.name end # @@ -1199,9 +1417,6 @@ class Chef define_method(:load_current_resource) {} end end - def self.default_action=(action_name) - default_action(action_name) - end # # Internal Resource Interface (for Chef) diff --git a/spec/unit/mixin/params_validate_spec.rb b/spec/unit/mixin/params_validate_spec.rb index 85e1c1abab..3724bbf583 100644 --- a/spec/unit/mixin/params_validate_spec.rb +++ b/spec/unit/mixin/params_validate_spec.rb @@ -21,6 +21,8 @@ require 'spec_helper' class TinyClass include Chef::Mixin::ParamsValidate + attr_reader :name + def music(is_good=true) is_good end @@ -331,11 +333,11 @@ describe Chef::Mixin::ParamsValidate do it "asserts that a value returns false from a predicate method" do expect do @vo.validate({:not_blank => "should pass"}, - {:not_blank => {:cannot_be => :nil, :cannot_be => :empty}}) + {:not_blank => {:cannot_be => [ :nil, :empty ]}}) end.not_to raise_error expect do @vo.validate({:not_blank => ""}, - {:not_blank => {:cannot_be => :nil, :cannot_be => :empty}}) + {:not_blank => {:cannot_be => [ :nil, :empty ]}}) end.to raise_error(Chef::Exceptions::ValidationFailed) end diff --git a/spec/unit/property/state_spec.rb b/spec/unit/property/state_spec.rb index 80ebe01a41..bf2de178c9 100644 --- a/spec/unit/property/state_spec.rb +++ b/spec/unit/property/state_spec.rb @@ -50,20 +50,20 @@ describe "Chef::Resource#identity and #state" do end # identity - context "Chef::Resource#identity_attr" do + context "Chef::Resource#identity_properties" do with_property ":x" do - # it "name is the default identity" do - # expect(resource_class.identity_attr).to eq :name - # expect(resource_class.properties[:name].identity?).to be_falsey - # expect(resource.name).to eq 'blah' - # expect(resource.identity).to eq 'blah' - # end + it "name is the default identity" do + expect(resource_class.identity_properties).to eq [ Chef::Resource.properties[:name] ] + expect(Chef::Resource.properties[:name].identity?).to be_falsey + expect(resource.name).to eq 'blah' + expect(resource.identity).to eq 'blah' + end - it "identity_attr :x changes the identity" do - expect(resource_class.identity_attr :x).to eq :x - expect(resource_class.identity_attr).to eq :x - # expect(resource_class.properties[:name].identity?).to be_falsey - # expect(resource_class.properties[:x].identity?).to be_truthy + it "identity_properties :x changes the identity" do + expect(resource_class.identity_properties :x).to eq [ resource_class.properties[:x] ] + expect(resource_class.identity_properties).to eq [ resource_class.properties[:x] ] + expect(Chef::Resource.properties[:name].identity?).to be_falsey + expect(resource_class.properties[:x].identity?).to be_truthy expect(resource.x 'woo').to eq 'woo' expect(resource.x).to eq 'woo' @@ -72,28 +72,31 @@ describe "Chef::Resource#identity and #state" do expect(resource.identity).to eq 'woo' end - # with_property ":y, identity: true" do - # context "and identity_attr :x" do - # before do - # resource_class.class_eval do - # identity_attr :x - # end - # end - # - # it "only returns :x as identity" do - # resource.x 'foo' - # resource.y 'bar' - # expect(resource_class.identity_attr).to eq :x - # expect(resource.identity).to eq 'foo' - # end - # it "does not flip y.desired_state off" do - # resource.x 'foo' - # resource.y 'bar' - # expect(resource_class.state_attrs).to eq [ :x, :y ] - # expect(resource.state).to eq({ x: 'foo', y: 'bar' }) - # end - # end - # end + with_property ":y, identity: true" do + context "and identity_properties :x" do + before do + resource_class.class_eval do + identity_properties :x + end + end + + it "only returns :x as identity" do + resource.x 'foo' + resource.y 'bar' + expect(resource_class.identity_properties).to eq [ resource_class.properties[:x] ] + expect(resource.identity).to eq 'foo' + end + it "does not flip y.desired_state off" do + resource.x 'foo' + resource.y 'bar' + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x], + resource_class.properties[:y] + ] + expect(resource.state_for_resource_reporter).to eq(x: 'foo', y: 'bar') + end + end + end context "With a subclass" do let(:subresource_class) do @@ -107,57 +110,63 @@ describe "Chef::Resource#identity and #state" do end it "name is the default identity on the subclass" do - # expect(subresource_class.identity_attr).to eq :name - # expect(subresource_class.properties[:name].identity?).to be_falsey + expect(subresource_class.identity_properties).to eq [ Chef::Resource.properties[:name] ] + expect(Chef::Resource.properties[:name].identity?).to be_falsey expect(subresource.name).to eq 'sub' expect(subresource.identity).to eq 'sub' end - context "With identity_attr :x on the superclass" do + context "With identity_properties :x on the superclass" do before do resource_class.class_eval do - identity_attr :x + identity_properties :x end end it "The subclass inherits :x as identity" do - expect(subresource_class.identity_attr).to eq :x - # expect(subresource_class.properties[:name].identity?).to be_falsey - # expect(subresource_class.properties[:x].identity?).to be_truthy + expect(subresource_class.identity_properties).to eq [ subresource_class.properties[:x] ] + expect(Chef::Resource.properties[:name].identity?).to be_falsey + expect(subresource_class.properties[:x].identity?).to be_truthy subresource.x 'foo' expect(subresource.identity).to eq 'foo' end - # context "With property :y, identity: true on the subclass" do - # before do - # subresource_class.class_eval do - # property :y, identity: true - # end - # end - # it "The subclass's identity includes both x and y" do - # expect(subresource_class.identity_attr).to eq :x - # subresource.x 'foo' - # subresource.y 'bar' - # expect(subresource.identity).to eq({ x: 'foo', y: 'bar' }) - # end - # end + context "With property :y, identity: true on the subclass" do + before do + subresource_class.class_eval do + property :y, identity: true + end + end + it "The subclass's identity includes both x and y" do + expect(subresource_class.identity_properties).to eq [ + subresource_class.properties[:x], + subresource_class.properties[:y] + ] + subresource.x 'foo' + subresource.y 'bar' + expect(subresource.identity).to eq(x: 'foo', y: 'bar') + end + end with_property ":y, String" do - context "With identity_attr :y on the subclass" do + context "With identity_properties :y on the subclass" do before do subresource_class.class_eval do - identity_attr :y + identity_properties :y end end - # it "y is part of state" do - # subresource.x 'foo' - # subresource.y 'bar' - # expect(subresource.state).to eq({ x: 'foo', y: 'bar' }) - # expect(subresource_class.state_attrs).to eq [ :x, :y ] - # end + it "y is part of state" do + subresource.x 'foo' + subresource.y 'bar' + expect(subresource.state_for_resource_reporter).to eq(x: 'foo', y: 'bar') + expect(subresource_class.state_properties).to eq [ + subresource_class.properties[:x], + subresource_class.properties[:y] + ] + end it "y is the identity" do - expect(subresource_class.identity_attr).to eq :y + expect(subresource_class.identity_properties).to eq [ subresource_class.properties[:y] ] subresource.x 'foo' subresource.y 'bar' expect(subresource.identity).to eq 'bar' @@ -171,24 +180,24 @@ describe "Chef::Resource#identity and #state" do end end - # with_property ":string_only, String, identity: true", ":string_only2, String" do - # it "identity_attr does not change validation" do - # resource_class.identity_attr :string_only - # expect { resource.string_only 12 }.to raise_error Chef::Exceptions::ValidationFailed - # expect { resource.string_only2 12 }.to raise_error Chef::Exceptions::ValidationFailed - # end - # end - # - # with_property ":x, desired_state: false" do - # it "identity_attr does not flip on desired_state" do - # resource_class.identity_attr :x - # resource.x 'hi' - # expect(resource.identity).to eq 'hi' - # # expect(resource_class.properties[:x].desired_state?).to be_falsey - # expect(resource_class.state_attrs).to eq [] - # expect(resource.state).to eq({}) - # end - # end + with_property ":string_only, String, identity: true", ":string_only2, String" do + it "identity_properties does not change validation" do + resource_class.identity_properties :string_only + expect { resource.string_only 12 }.to raise_error Chef::Exceptions::ValidationFailed + expect { resource.string_only2 12 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + with_property ":x, desired_state: false" do + it "identity_properties does not change desired_state" do + resource_class.identity_properties :x + resource.x 'hi' + expect(resource.identity).to eq 'hi' + expect(resource_class.properties[:x].desired_state?).to be_falsey + expect(resource_class.state_properties).to eq [] + expect(resource.state_for_resource_reporter).to eq({}) + end + end context "With custom property custom_property defined only as methods, using different variables for storage" do before do @@ -202,24 +211,26 @@ describe "Chef::Resource#identity and #state" do end end - context "And identity_attr :custom_property" do + context "And identity_properties :custom_property" do before do resource_class.class_eval do - identity_attr :custom_property + identity_properties :custom_property end end - it "identity_attr comes back as :custom_property" do - # expect(resource_class.properties[:custom_property].identity?).to be_truthy - expect(resource_class.identity_attr).to eq :custom_property + it "identity_properties comes back as :custom_property" do + expect(resource_class.properties[:custom_property].identity?).to be_truthy + expect(resource_class.identity_properties).to eq [ resource_class.properties[:custom_property] ] + end + it "custom_property becomes part of desired_state" do + resource.custom_property = 1 + expect(resource.state_for_resource_reporter).to eq(custom_property: 6) + expect(resource_class.properties[:custom_property].desired_state?).to be_truthy + expect(resource_class.state_properties).to eq [ + resource_class.properties[:custom_property] + ] end - # it "custom_property becomes part of desired_state" do - # resource.custom_property = 1 - # expect(resource.state).to eq({ custom_property: 6 }) - # expect(resource_class.properties[:custom_property].desired_state?).to be_truthy - # expect(resource_class.state_attrs).to eq [ :custom_property ] - # end - it "identity_attr does not change custom_property's getter or setter" do + it "identity_properties does not change custom_property's getter or setter" do resource.custom_property = 1 expect(resource.custom_property).to eq 6 end @@ -232,111 +243,111 @@ describe "Chef::Resource#identity and #state" do end end - # context "PropertyType#identity" do - # with_property ":x, identity: true" do - # it "name is only part of the identity if an identity attribute is defined" do - # expect(resource_class.identity_attr).to eq :x - # resource.x 'woo' - # expect(resource.identity).to eq 'woo' - # end - # end - # - # with_property ":x, identity: true, default: 'xxx'", - # ":y, identity: true, default: 'yyy'", - # ":z, identity: true, default: 'zzz'" do - # it "identity_attr returns the first identity attribute if multiple are defined" do - # expect(resource_class.identity_attr).to eq :x - # end - # it "identity returns all identity values in a hash if multiple are defined" do - # resource.x 'foo' - # resource.y 'bar' - # resource.z 'baz' - # expect(resource.identity).to eq({ x: 'foo', y: 'bar', z: 'baz' }) - # end - # it "identity returns only identity values that are set, and does not include defaults" do - # resource.x 'foo' - # resource.z 'baz' - # expect(resource.identity).to eq({ x: 'foo', z: 'baz' }) - # end - # it "identity returns only set identity values in a hash, if there is only one set identity value" do - # resource.x 'foo' - # expect(resource.identity).to eq({ x: 'foo' }) - # end - # it "identity returns an empty hash if no identity values are set" do - # expect(resource.identity).to eq({}) - # end - # it "identity_attr wipes out any other identity attributes if multiple are defined" do - # resource_class.identity_attr :y - # resource.x 'foo' - # resource.y 'bar' - # resource.z 'baz' - # expect(resource.identity).to eq 'bar' - # end - # end - # - # with_property ":x, identity: true, name_property: true" do - # it "identity when x is not defined returns the value of x" do - # expect(resource.identity).to eq 'blah' - # end - # it "state when x is not defined returns the value of x" do - # expect(resource.state).to eq({ x: 'blah' }) - # end - # end - # end - - # state_attrs - context "Chef::Resource#state_attrs" do - it "name is not part of state_attrs" do - expect(Chef::Resource.state_attrs).to eq [] - expect(resource_class.state_attrs).to eq [] - expect(resource.state).to eq({}) + context "Property#identity" do + with_property ":x, identity: true" do + it "name is only part of the identity if an identity attribute is defined" do + expect(resource_class.identity_properties).to eq [ resource_class.properties[:x] ] + resource.x 'woo' + expect(resource.identity).to eq 'woo' + end + end + + with_property ":x, identity: true, default: 'xxx'", + ":y, identity: true, default: 'yyy'", + ":z, identity: true, default: 'zzz'" do + it "identity_property raises an error if multiple identity values are defined" do + expect { resource_class.identity_property }.to raise_error Chef::Exceptions::MultipleIdentityError + end + it "identity_attr raises an error if multiple identity values are defined" do + expect { resource_class.identity_attr }.to raise_error Chef::Exceptions::MultipleIdentityError + end + it "identity returns all identity values in a hash if multiple are defined" do + resource.x 'foo' + resource.y 'bar' + resource.z 'baz' + expect(resource.identity).to eq(x: 'foo', y: 'bar', z: 'baz') + end + it "identity returns all values whether any value is set or not" do + expect(resource.identity).to eq(x: 'xxx', y: 'yyy', z: 'zzz') + end + it "identity_properties wipes out any other identity attributes if multiple are defined" do + resource_class.identity_properties :y + resource.x 'foo' + resource.y 'bar' + resource.z 'baz' + expect(resource.identity).to eq 'bar' + end + end + + with_property ":x, identity: true, name_property: true" do + it "identity when x is not defined returns the value of x" do + expect(resource.identity).to eq 'blah' + end + it "state when x is not defined returns the value of x" do + expect(resource.state_for_resource_reporter).to eq(x: 'blah') + end end + end - # with_property ":x", ":y", ":z" do - # it "x, y and z are state attributes" do - # resource.x 1 - # resource.y 2 - # resource.z 3 - # expect(resource_class.state_attrs).to eq [ :x, :y, :z ] - # expect(resource.state).to eq(x: 1, y: 2, z: 3) - # end - # it "values that are not set are not included in state" do - # resource.x 1 - # expect(resource.state).to eq(x: 1) - # end - # it "when no values are set, nothing is included in state" do - # end - # end - # - # with_property ":x", ":y, desired_state: false", ":z, desired_state: true" do - # it "x and z are state attributes, and y is not" do - # resource.x 1 - # resource.y 2 - # resource.z 3 - # expect(resource_class.state_attrs).to eq [ :x, :z ] - # expect(resource.state).to eq(x: 1, z: 3) - # end - # end - - # with_property ":x, name_property: true" do - # it "Unset values with name_property are included in state" do - # expect(resource.state).to eq(x: 'blah') - # end - # it "Set values with name_property are included in state" do - # resource.x 1 - # expect(resource.state).to eq(x: 1) - # end - # end - - # with_property ":x, default: 1" do - # it "Unset values with defaults are not included in state" do - # expect(resource.state).to eq({}) - # end - # it "Set values with defaults are included in state" do - # resource.x 1 - # expect(resource.state).to eq(x: 1) - # end - # end + # state_properties + context "Chef::Resource#state_properties" do + it "state_properties is empty by default" do + expect(Chef::Resource.state_properties).to eq [] + expect(resource.state_for_resource_reporter).to eq({}) + end + + with_property ":x", ":y", ":z" do + it "x, y and z are state attributes" do + resource.x 1 + resource.y 2 + resource.z 3 + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x], + resource_class.properties[:y], + resource_class.properties[:z] + ] + expect(resource.state_for_resource_reporter).to eq(x: 1, y: 2, z: 3) + end + it "values that are not set are not included in state" do + resource.x 1 + expect(resource.state_for_resource_reporter).to eq(x: 1) + end + it "when no values are set, nothing is included in state" do + end + end + + with_property ":x", ":y, desired_state: false", ":z, desired_state: true" do + it "x and z are state attributes, and y is not" do + resource.x 1 + resource.y 2 + resource.z 3 + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x], + resource_class.properties[:z] + ] + expect(resource.state_for_resource_reporter).to eq(x: 1, z: 3) + end + end + + with_property ":x, name_property: true" do + # it "Unset values with name_property are included in state" do + # expect(resource.state_for_resource_reporter).to eq({ x: 'blah' }) + # end + it "Set values with name_property are included in state" do + resource.x 1 + expect(resource.state_for_resource_reporter).to eq(x: 1) + end + end + + with_property ":x, default: 1" do + it "Unset values with defaults are not included in state" do + expect(resource.state_for_resource_reporter).to eq({}) + end + it "Set values with defaults are included in state" do + resource.x 1 + expect(resource.state_for_resource_reporter).to eq(x: 1) + end + end context "With a class with a normal getter and setter" do before do @@ -349,143 +360,132 @@ describe "Chef::Resource#identity and #state" do end end end - it "state_attrs(:x) causes the value to be included in properties" do - resource_class.state_attrs(:x) + it "state_properties(:x) causes the value to be included in properties" do + resource_class.state_properties(:x) resource.x = 1 expect(resource.x).to eq 6 - expect(resource.state).to eq(x: 6) + expect(resource.state_for_resource_reporter).to eq(x: 6) end end - # with_property ":x, Integer, identity: true" do - # it "state_attrs(:x) leaves the property in desired_state" do - # resource_class.state_attrs(:x) - # resource.x 10 - # - # # expect(resource_class.properties[:x].desired_state?).to be_truthy - # expect(resource_class.state_attrs).to eq [ :x ] - # expect(resource.state).to eq(x: 10) - # end - # it "state_attrs(:x) does not turn off validation" do - # resource_class.state_attrs(:x) - # expect { resource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed - # end - # it "state_attrs(:x) does not turn off identity" do - # resource_class.state_attrs(:x) - # resource.x 10 - # - # expect(resource_class.identity_attr).to eq :x - # # expect(resource_class.properties[:x].identity?).to be_truthy - # expect(resource.identity).to eq 10 - # end - # end - - # with_property ":x, Integer, identity: true, desired_state: false" do - # before do - # resource_class.class_eval do - # def y - # 20 - # end - # end - # end - # it "state_attrs(:x) sets the property in desired_state" do - # resource_class.state_attrs(:x) - # resource.x 10 - # - # # expect(resource_class.properties[:x].desired_state?).to be_truthy - # expect(resource_class.state_attrs).to eq [ :x ] - # expect(resource.state).to eq(x: 10) - # end - # it "state_attrs(:x) does not turn off validation" do - # resource_class.state_attrs(:x) - # expect { resource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed - # end - # it "state_attrs(:x) does not turn off identity" do - # resource_class.state_attrs(:x) - # resource.x 10 - # - # expect(resource_class.identity_attr).to eq :x - # # expect(resource_class.properties[:x].identity?).to be_truthy - # expect(resource.identity).to eq 10 - # end - # it "state_attrs(:y) adds y and removes x from desired state" do - # resource_class.state_attrs(:y) - # resource.x 10 - # - # # expect(resource_class.properties[:x].desired_state?).to be_falsey - # # expect(resource_class.properties[:y].desired_state?).to be_truthy - # expect(resource_class.state_attrs).to eq [ :y ] - # expect(resource.state).to eq(y: 20) - # end - # it "state_attrs(:y) does not turn off validation" do - # resource_class.state_attrs(:y) - # - # expect { resource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed - # end - # it "state_attrs(:y) does not turn off identity" do - # resource_class.state_attrs(:y) - # resource.x 10 - # - # expect(resource_class.identity_attr).to eq :x - # # expect(resource_class.properties[:x].identity?).to be_truthy - # expect(resource.identity).to eq 10 - # end - # - # context "With a subclassed resource" do - # let(:resource_subclass) do - # new_resource_name = self.class.new_resource_name - # Class.new(resource_class) do - # resource_name new_resource_name - # end - # end - # let(:subresource) do - # resource_subclass.new('blah') - # end - # it "state_attrs(:x) sets the property in desired_state" do - # resource_subclass.state_attrs(:x) - # subresource.x 10 - # - # # expect(resource_subclass.properties[:x].desired_state?).to be_truthy - # expect(resource_subclass.state_attrs).to eq [ :x ] - # expect(subresource.state).to eq(x: 10) - # end - # it "state_attrs(:x) does not turn off validation" do - # resource_subclass.state_attrs(:x) - # expect { subresource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed - # end - # it "state_attrs(:x) does not turn off identity" do - # resource_subclass.state_attrs(:x) - # subresource.x 10 - # - # expect(resource_subclass.identity_attr).to eq :x - # # expect(resource_subclass.properties[:x].identity?).to be_truthy - # expect(subresource.identity).to eq 10 - # end - # it "state_attrs(:y) adds y and removes x from desired state" do - # resource_subclass.state_attrs(:y) - # subresource.x 10 - # - # # expect(resource_subclass.properties[:x].desired_state?).to be_falsey - # # expect(resource_subclass.properties[:y].desired_state?).to be_truthy - # expect(resource_subclass.state_attrs).to eq [ :y ] - # expect(subresource.state).to eq(y: 20) - # end - # it "state_attrs(:y) does not turn off validation" do - # resource_subclass.state_attrs(:y) - # - # expect { subresource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed - # end - # it "state_attrs(:y) does not turn off identity" do - # resource_subclass.state_attrs(:y) - # subresource.x 10 - # - # expect(resource_subclass.identity_attr).to eq :x - # # expect(resource_subclass.properties[:x].identity?).to be_truthy - # expect(subresource.identity).to eq 10 - # end - # end - # end + with_property ":x, Integer, identity: true" do + it "state_properties(:x) leaves the property in desired_state" do + resource_class.state_properties(:x) + resource.x 10 + + expect(resource_class.properties[:x].desired_state?).to be_truthy + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x] + ] + expect(resource.state_for_resource_reporter).to eq(x: 10) + end + it "state_properties(:x) does not turn off validation" do + resource_class.state_properties(:x) + expect { resource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed + end + it "state_properties(:x) does not turn off identity" do + resource_class.state_properties(:x) + resource.x 10 + + expect(resource_class.identity_properties).to eq [ resource_class.properties[:x] ] + expect(resource_class.properties[:x].identity?).to be_truthy + expect(resource.identity).to eq 10 + end + end + + with_property ":x, Integer, identity: true, desired_state: false" do + before do + resource_class.class_eval do + def y + 20 + end + end + end + + it "state_properties(:x) leaves x identical" do + old_value = resource_class.properties[:y] + resource_class.state_properties(:x) + resource.x 10 + + expect(resource_class.properties[:y].object_id).to eq old_value.object_id + + expect(resource_class.properties[:x].desired_state?).to be_truthy + expect(resource_class.properties[:x].identity?).to be_truthy + expect(resource_class.identity_properties).to eq [ + resource_class.properties[:x] + ] + expect(resource.identity).to eq(10) + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x] + ] + expect(resource.state_for_resource_reporter).to eq(x: 10) + end + + it "state_properties(:y) adds y to desired state" do + old_value = resource_class.properties[:x] + resource_class.state_properties(:y) + resource.x 10 + + expect(resource_class.properties[:x].object_id).to eq old_value.object_id + expect(resource_class.properties[:x].desired_state?).to be_falsey + expect(resource_class.properties[:y].desired_state?).to be_truthy + expect(resource_class.state_properties).to eq [ + resource_class.properties[:y] + ] + expect(resource.state_for_resource_reporter).to eq(y: 20) + end + + context "With a subclassed resource" do + let(:subresource_class) do + new_resource_name = self.class.new_resource_name + Class.new(resource_class) do + resource_name new_resource_name + end + end + let(:subresource) do + subresource_class.new('blah') + end + + it "state_properties(:x) adds x to desired state" do + old_value = resource_class.properties[:y] + subresource_class.state_properties(:x) + subresource.x 10 + + expect(subresource_class.properties[:y].object_id).to eq old_value.object_id + + expect(subresource_class.properties[:x].desired_state?).to be_truthy + expect(subresource_class.properties[:x].identity?).to be_truthy + expect(subresource_class.identity_properties).to eq [ + subresource_class.properties[:x] + ] + expect(subresource.identity).to eq(10) + expect(subresource_class.state_properties).to eq [ + subresource_class.properties[:x] + ] + expect(subresource.state_for_resource_reporter).to eq(x: 10) + end + + it "state_properties(:y) adds y to desired state" do + old_value = resource_class.properties[:x] + subresource_class.state_properties(:y) + subresource.x 10 + + expect(subresource_class.properties[:x].object_id).to eq old_value.object_id + expect(subresource_class.properties[:y].desired_state?).to be_truthy + expect(subresource_class.state_properties).to eq [ + subresource_class.properties[:y] + ] + expect(subresource.state_for_resource_reporter).to eq(y: 20) + + expect(subresource_class.properties[:x].identity?).to be_truthy + expect(subresource_class.identity_properties).to eq [ + subresource_class.properties[:x] + ] + expect(subresource.identity).to eq(10) + end + end + end end end diff --git a/spec/unit/property/validation_spec.rb b/spec/unit/property/validation_spec.rb index 7ce6a45167..a3649d228c 100644 --- a/spec/unit/property/validation_spec.rb +++ b/spec/unit/property/validation_spec.rb @@ -111,10 +111,6 @@ describe "Chef::Resource.property validation" do it "get succeeds" do expect(resource.x).to eq 'default' end - it "set(nil) = get" do - expect(resource.x nil).to eq 'default' - expect(resource.x).to eq 'default' - end it "set to valid value succeeds" do expect(resource.x 'str').to eq 'str' expect(resource.x).to eq 'str' @@ -122,15 +118,18 @@ describe "Chef::Resource.property validation" do it "set to invalid value raises ValidationFailed" do expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed end + it "set to nil emits a deprecation warning and does a get" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 'str' + expect(resource.x nil).to eq 'str' + expect(resource.x).to eq 'str' + end end context "when the variable does not have an initial value" do it "get succeeds" do expect(resource.x).to be_nil end - it "set(nil) = get" do - expect(resource.x nil).to be_nil - expect(resource.x).to be_nil - end it "set to valid value succeeds" do expect(resource.x 'str').to eq 'str' expect(resource.x).to eq 'str' @@ -138,6 +137,13 @@ describe "Chef::Resource.property validation" do it "set to invalid value raises ValidationFailed" do expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed end + it "set to nil emits a deprecation warning and does a get" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 'str' + expect(resource.x nil).to eq 'str' + expect(resource.x).to eq 'str' + end end end with_property ":x, [ String, nil ]" do @@ -255,10 +261,10 @@ describe "Chef::Resource.property validation" do [ '', 'abac' ], [ nil ] - # PropertyType - # validation_test 'is: PropertyType.new(is: :a)', - # [ :a ], - # [ :b, nil ] + # Property + validation_test 'is: Chef::Property.new(is: :a)', + [ :a ], + [ :b, nil ] # RSpec Matcher class Globalses @@ -523,10 +529,11 @@ describe "Chef::Resource.property validation" do expect(resource.x 1).to eq 1 expect(resource.x).to eq 1 end - it "value nil does a get" do + it "value nil emits a deprecation warning and does a get" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError Chef::Config[:treat_deprecation_warnings_as_errors] = false resource.x 1 - resource.x nil + expect(resource.x nil).to eq 1 expect(resource.x).to eq 1 end end @@ -549,16 +556,18 @@ describe "Chef::Resource.property validation" do end with_property ':x, name_property: true, required: true' do - it "if x is not specified, retrieval succeeds" do + it "if x is not specified, the name property is returned" do expect(resource.x).to eq 'blah' end it "value 1 is valid" do expect(resource.x 1).to eq 1 expect(resource.x).to eq 1 end - it "value nil does a get" do + it "value nil emits a deprecation warning and does a get" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false resource.x 1 - resource.x nil + expect(resource.x nil).to eq 1 expect(resource.x).to eq 1 end end @@ -571,9 +580,11 @@ describe "Chef::Resource.property validation" do expect(resource.x 1).to eq 1 expect(resource.x).to eq 1 end - it "value nil does a get" do + it "value nil is invalid" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false resource.x 1 - resource.x nil + expect(resource.x nil).to eq 1 expect(resource.x).to eq 1 end end @@ -596,11 +607,6 @@ describe "Chef::Resource.property validation" do end end - # it "getting the value causes a deprecation warning" do - # Chef::Config[:treat_deprecation_warnings_as_errors] = true - # expect { resource.x }.to raise_error Chef::Exceptions::DeprecatedFeatureError - # end - it "value 1 is valid" do expect(resource.x 1).to eq 1 expect(resource.x).to eq 1 diff --git a/spec/unit/property_spec.rb b/spec/unit/property_spec.rb index ce0552c564..09f7e52329 100644 --- a/spec/unit/property_spec.rb +++ b/spec/unit/property_spec.rb @@ -58,11 +58,15 @@ describe "Chef::Resource.property" do else tags = [] end - properties = properties.map { |property| "property #{property}" } - context "With properties #{english_join(properties)}", *tags do + if properties.size == 1 + description = "With property #{properties.first}" + else + description = "With properties #{english_join(properties.map { |property| "#{property.inspect}" })}" + end + context description, *tags do before do properties.each do |property_str| - resource_class.class_eval(property_str, __FILE__, __LINE__) + resource_class.class_eval("property #{property_str}", __FILE__, __LINE__) end end instance_eval(&block) @@ -75,11 +79,10 @@ describe "Chef::Resource.property" do expect(resource.bare_property 10).to eq 10 expect(resource.bare_property).to eq 10 end - # it "emits a deprecation warning and does a get, if set to nil" do it "emits a deprecation warning and does a get, if set to nil" do expect(resource.bare_property 10).to eq 10 - # expect { resource.bare_property nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError - # Chef::Config[:treat_deprecation_warnings_as_errors] = false + expect { resource.bare_property nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false expect(resource.bare_property nil).to eq 10 expect(resource.bare_property).to eq 10 end @@ -92,11 +95,11 @@ describe "Chef::Resource.property" do expect(resource.bare_property 10).to eq 10 expect(resource.bare_property).to eq 10 end - # it "can be set to nil with =" do - # expect(resource.bare_property 10).to eq 10 - # expect(resource.bare_property = nil).to be_nil - # expect(resource.bare_property).to be_nil - # end + it "can be set to nil with =" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property = nil).to be_nil + expect(resource.bare_property).to be_nil + end it "can be updated with =" do expect(resource.bare_property 10).to eq 10 expect(resource.bare_property = 20).to eq 20 @@ -121,7 +124,7 @@ describe "Chef::Resource.property" do expect(subresource.x).to eq 10 expect(subresource.x = 20).to eq 20 expect(subresource.x).to eq 20 - # expect(subresource_class.properties[:x]).not_to be_nil + expect(subresource_class.properties[:x]).not_to be_nil end it "x's validation is inherited" do @@ -140,18 +143,18 @@ describe "Chef::Resource.property" do expect(subresource.x).to eq 10 expect(subresource.x = 20).to eq 20 expect(subresource.x).to eq 20 - # expect(subresource_class.properties[:x]).not_to be_nil + expect(subresource_class.properties[:x]).not_to be_nil end it "y is there" do expect(subresource.y 10).to eq 10 expect(subresource.y).to eq 10 expect(subresource.y = 20).to eq 20 expect(subresource.y).to eq 20 - # expect(subresource_class.properties[:y]).not_to be_nil + expect(subresource_class.properties[:y]).not_to be_nil end it "y is not on the superclass" do expect { resource_class.y 10 }.to raise_error - # expect(resource_class.properties[:y]).to be_nil + expect(resource_class.properties[:y]).to be_nil end end @@ -167,17 +170,37 @@ describe "Chef::Resource.property" do expect(subresource.x).to eq 10 expect(subresource.x = 20).to eq 20 expect(subresource.x).to eq 20 - # expect(subresource_class.properties[:x]).not_to be_nil - # expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x] + expect(subresource_class.properties[:x]).not_to be_nil + expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x] end - it "x's validation is overwritten" do - expect(subresource.x 'ohno').to eq 'ohno' - expect(subresource.x).to eq 'ohno' + it "x's validation is inherited" do + expect { subresource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed end + end - it "the superclass's validation for x is still there" do - expect { resource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + context "with property :x, default: 80 on the subclass" do + before do + subresource_class.class_eval do + property :x, default: 80 + end + end + + it "x is still there" do + expect(subresource.x 10).to eq 10 + expect(subresource.x).to eq 10 + expect(subresource.x = 20).to eq 20 + expect(subresource.x).to eq 20 + expect(subresource_class.properties[:x]).not_to be_nil + expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x] + end + + it "x defaults to 80" do + expect(subresource.x).to eq 80 + end + + it "x's validation is inherited" do + expect { subresource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed end end @@ -193,8 +216,8 @@ describe "Chef::Resource.property" do expect(subresource.x).to eq "10" expect(subresource.x = "20").to eq "20" expect(subresource.x).to eq "20" - # expect(subresource_class.properties[:x]).not_to be_nil - # expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x] + expect(subresource_class.properties[:x]).not_to be_nil + expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x] end it "x's validation is overwritten" do @@ -212,14 +235,89 @@ describe "Chef::Resource.property" do end end - context "Chef::Resource::PropertyType#property_is_set?" do + context "Chef::Resource::Property#reset_property" do + it "when a resource is newly created, reset_property(:name) sets property to nil" do + expect(resource.property_is_set?(:name)).to be_truthy + resource.reset_property(:name) + expect(resource.property_is_set?(:name)).to be_falsey + expect(resource.name).to be_nil + end + + it "when referencing an undefined property, reset_property(:x) raises an error" do + expect { resource.reset_property(:x) }.to raise_error(ArgumentError) + end + + with_property ':x' do + it "when the resource is newly created, reset_property(:x) does nothing" do + expect(resource.property_is_set?(:x)).to be_falsey + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to be_nil + end + it "when x is set, reset_property resets it" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to be_nil + end + end + + with_property ':x, Integer' do + it "when the resource is newly created, reset_property(:x) does nothing" do + expect(resource.property_is_set?(:x)).to be_falsey + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to be_nil + end + it "when x is set, reset_property resets it even though `nil` is technically invalid" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to be_nil + end + end + + with_property ':x, default: 10' do + it "when the resource is newly created, reset_property(:x) does nothing" do + expect(resource.property_is_set?(:x)).to be_falsey + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to eq 10 + end + it "when x is set, reset_property resets it and it returns the default" do + resource.x 20 + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to eq 10 + end + end + + with_property ':x, default: lazy { 10 }' do + it "when the resource is newly created, reset_property(:x) does nothing" do + expect(resource.property_is_set?(:x)).to be_falsey + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to eq 10 + end + it "when x is set, reset_property resets it and it returns the default" do + resource.x 20 + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to eq 10 + end + end + end + + context "Chef::Resource::Property#property_is_set?" do it "when a resource is newly created, property_is_set?(:name) is true" do expect(resource.property_is_set?(:name)).to be_truthy end - # it "when referencing an undefined property, property_is_set?(:x) raises an error" do - # expect { resource.property_is_set?(:x) }.to raise_error(ArgumentError) - # end + it "when referencing an undefined property, property_is_set?(:x) raises an error" do + expect { resource.property_is_set?(:x) }.to raise_error(ArgumentError) + end with_property ':x' do it "when the resource is newly created, property_is_set?(:x) is false" do @@ -259,9 +357,9 @@ describe "Chef::Resource.property" do resource.x lazy { 10 } expect(resource.property_is_set?(:x)).to be_truthy end - it "when x is retrieved, property_is_set?(:x) is true" do + it "when x is retrieved, property_is_set?(:x) is false" do resource.x - expect(resource.property_is_set?(:x)).to be_truthy + expect(resource.property_is_set?(:x)).to be_falsey end end @@ -281,9 +379,9 @@ describe "Chef::Resource.property" do resource.x lazy { 10 } expect(resource.property_is_set?(:x)).to be_truthy end - it "when x is retrieved, property_is_set?(:x) is true" do + it "when x is retrieved, property_is_set?(:x) is false" do resource.x - expect(resource.property_is_set?(:x)).to be_truthy + expect(resource.property_is_set?(:x)).to be_falsey end end @@ -299,14 +397,14 @@ describe "Chef::Resource.property" do resource.x = 10 expect(resource.property_is_set?(:x)).to be_truthy end - it "when x is retrieved, property_is_set?(:x) is true" do + it "when x is retrieved, property_is_set?(:x) is false" do resource.x - expect(resource.property_is_set?(:x)).to be_truthy + expect(resource.property_is_set?(:x)).to be_falsey end end end - context "Chef::Resource::PropertyType#default" do + context "Chef::Resource::Property#default" do with_property ':x, default: 10' do it "when x is set, it returns its value" do expect(resource.x 20).to eq 20 @@ -317,7 +415,7 @@ describe "Chef::Resource.property" do expect(resource.x).to eq 10 end it "when x is not set, it is not included in state" do - expect(resource.state).to eq({}) + expect(resource.state_for_resource_reporter).to eq({}) end it "when x is set to nil, it returns nil" do resource.instance_eval { @x = nil } @@ -339,8 +437,15 @@ describe "Chef::Resource.property" do end with_property ':x, default: 10, identity: true' do - it "when x is not set, it is not included in identity" do - expect(resource.state).to eq({}) + it "when x is not set, it is included in identity" do + expect(resource.identity).to eq(10) + end + end + + with_property ':x, default: 1, identity: true', ':y, default: 2, identity: true' do + it "when x is not set, it is still included in identity" do + resource.y 20 + expect(resource.identity).to eq(x: 1, y: 20) end end @@ -462,21 +567,35 @@ describe "Chef::Resource.property" do # end end - with_property ":x, default: lazy { Namer.next_index }, is: proc { |v| Namer.next_index; true }" do + with_property ":x, default: lazy { Namer.next_index.to_s }, is: proc { |v| Namer.next_index; true }" do it "validation is not run at all on the default value" do - expect(resource.x).to eq 1 + expect(resource.x).to eq '1' + expect(Namer.current_index).to eq 1 + end + # it "validation is run each time" do + # expect(resource.x).to eq '1' + # expect(Namer.current_index).to eq 2 + # expect(resource.x).to eq '1' + # expect(Namer.current_index).to eq 2 + # end + end + + with_property ":x, default: lazy { Namer.next_index.to_s.freeze }, is: proc { |v| Namer.next_index; true }" do + it "validation is not run at all on the default value" do + expect(resource.x).to eq '1' expect(Namer.current_index).to eq 1 end # it "validation is only run the first time" do - # expect(resource.x).to eq 1 + # expect(resource.x).to eq '1' # expect(Namer.current_index).to eq 2 - # expect(resource.x).to eq 1 + # expect(resource.x).to eq '1' # expect(Namer.current_index).to eq 2 # end end end context "coercion of defaults" do + # Frozen default, non-frozen coerce with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do it "when the resource is created, the proc is not yet run" do resource @@ -487,13 +606,32 @@ describe "Chef::Resource.property" do expect(resource.x).to eq 'hi1' expect(Namer.current_index).to eq 1 end - it "when x is retrieved, coercion is run, no more than once" do + it "when x is retrieved, coercion is run exactly once" do expect(resource.x).to eq '101' expect(resource.x).to eq '101' expect(Namer.current_index).to eq 1 end end + # Frozen default, frozen coerce + with_property ':x, coerce: proc { |v| "#{v}#{next_index}".freeze }, default: 10' do + it "when the resource is created, the proc is not yet run" do + resource + expect(Namer.current_index).to eq 0 + end + it "when x is set, coercion is run" do + expect(resource.x 'hi').to eq 'hi1' + expect(resource.x).to eq 'hi1' + expect(Namer.current_index).to eq 1 + end + it "when x is retrieved, coercion is run each time" do + expect(resource.x).to eq '101' + expect(resource.x).to eq '102' + expect(Namer.current_index).to eq 2 + end + end + + # Frozen lazy default, non-frozen coerce with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do it "when the resource is created, the proc is not yet run" do resource @@ -504,6 +642,29 @@ describe "Chef::Resource.property" do expect(resource.x).to eq 'hi1' expect(Namer.current_index).to eq 1 end + it "when x is retrieved, coercion is run exactly once" do + expect(resource.x).to eq '101' + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + end + end + + # Non-frozen lazy default, frozen coerce + with_property ':x, coerce: proc { |v| "#{v}#{next_index}".freeze }, default: lazy { "10" }' do + it "when the resource is created, the proc is not yet run" do + resource + expect(Namer.current_index).to eq 0 + end + it "when x is set, coercion is run" do + expect(resource.x 'hi').to eq 'hi1' + expect(resource.x).to eq 'hi1' + expect(Namer.current_index).to eq 1 + end + it "when x is retrieved, coercion is run each time" do + expect(resource.x).to eq '101' + expect(resource.x).to eq '102' + expect(Namer.current_index).to eq 2 + end end with_property ':x, proc { |v| Namer.next_index; true }, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do @@ -706,7 +867,7 @@ describe "Chef::Resource.property" do end end - context "Chef::Resource::PropertyType#coerce" do + context "Chef::Resource::Property#coerce" do with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do it "coercion runs on set" do expect(resource.x 10).to eq "101" @@ -740,7 +901,7 @@ describe "Chef::Resource.property" do end end - context "Chef::Resource::PropertyType validation" do + context "Chef::Resource::Property validation" do with_property ':x, is: proc { |v| Namer.next_index; v.is_a?(Integer) }' do it "validation runs on set" do expect(resource.x 10).to eq 10 @@ -768,7 +929,7 @@ describe "Chef::Resource.property" do end [ 'name_attribute', 'name_property' ].each do |name| - context "Chef::Resource::PropertyType##{name}" do + context "Chef::Resource::Property##{name}" do with_property ":x, #{name}: true" do it "defaults x to resource.name" do expect(resource.x).to eq 'blah' @@ -797,6 +958,11 @@ describe "Chef::Resource.property" do expect(resource.x).to eq 10 end end + with_property ":x, #{name}: true, required: true" do + it "defaults x to resource.name" do + expect(resource.x).to eq 'blah' + end + end end end end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index 353c5ec129..1377950c99 100644 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -59,8 +59,8 @@ describe Chef::Resource do end describe "when declaring the identity attribute" do - it "has no identity attribute by default" do - expect(Chef::Resource.identity_attr).to be_nil + it "has :name as identity attribute by default" do + expect(Chef::Resource.identity_attr).to eq(:name) end it "sets an identity attribute" do |