diff options
author | John Keiser <john@johnkeiser.com> | 2015-05-29 11:44:25 -0700 |
---|---|---|
committer | John Keiser <john@johnkeiser.com> | 2015-06-02 19:33:49 -0700 |
commit | 8cf24935c261ae26b74fbcfd74f9c5e7f002f3f8 (patch) | |
tree | 534946ad8dbdca14123268b7aba4d3672c9fea07 | |
parent | e3c455bd083ec7f87b8935a54cfc53692fdb1311 (diff) | |
download | chef-jk/property.tar.gz |
Create property and alias attribute to itjk/property
-rw-r--r-- | lib/chef/delayed_evaluator.rb | 33 | ||||
-rw-r--r-- | lib/chef/mixin/params_validate.rb | 6 | ||||
-rw-r--r-- | lib/chef/resource.rb | 343 | ||||
-rw-r--r-- | lib/chef/resource/deploy.rb | 2 | ||||
-rw-r--r-- | lib/chef/resource/implicit_property_type.rb | 22 | ||||
-rw-r--r-- | lib/chef/resource/lwrp_base.rb | 11 | ||||
-rw-r--r-- | lib/chef/resource/property_type.rb | 583 | ||||
-rw-r--r-- | spec/integration/recipes/property_spec.rb | 16 | ||||
-rw-r--r-- | spec/integration/recipes/property_validation_spec.rb | 16 | ||||
-rw-r--r-- | spec/unit/resource_property_spec.rb | 744 | ||||
-rw-r--r-- | spec/unit/resource_property_state_spec.rb | 492 | ||||
-rw-r--r-- | spec/unit/resource_property_validation_spec.rb | 333 | ||||
-rw-r--r-- | spec/unit/resource_spec.rb | 4 |
13 files changed, 2545 insertions, 60 deletions
diff --git a/lib/chef/delayed_evaluator.rb b/lib/chef/delayed_evaluator.rb new file mode 100644 index 0000000000..640b2c7b69 --- /dev/null +++ b/lib/chef/delayed_evaluator.rb @@ -0,0 +1,33 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Copyright:: Copyright (c) 2008 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 + # + # A lazy value which may be assigned to a Resource property value or default. + # + # The proc will be run to determine the value, passed no parameters, and the + # result will be used as the value. + # + # Run no more than once per instance, but may be run multiple times per + # instance. + # + # @see Chef::Resource.lazy + # @see Chef::Mixin::ParamsValidate#lazy + # + class DelayedEvaluator < Proc + end +end diff --git a/lib/chef/mixin/params_validate.rb b/lib/chef/mixin/params_validate.rb index 78d72dc801..3e4a61b417 100644 --- a/lib/chef/mixin/params_validate.rb +++ b/lib/chef/mixin/params_validate.rb @@ -15,12 +15,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'chef/delayed_evaluator' + class Chef - class DelayedEvaluator < Proc - end module Mixin module ParamsValidate - # Takes a hash of options, along with a map to validate them. Returns the original # options hash, plus any changes that might have been made (through things like setting # default values in the validation map) @@ -239,4 +238,3 @@ class Chef end end end - diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index ac98df5513..9bcfb1e5e6 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -34,6 +34,8 @@ require 'chef/platform' require 'chef/resource/resource_notification' require 'chef/provider_resolver' require 'chef/resource_resolver' +require 'chef/resource/property_type' +require 'chef/resource/implicit_property_type' require 'chef/mixin/deprecation' require 'chef/mixin/provides' @@ -97,12 +99,12 @@ class Chef # # Create a new Resource. # - # @param name The name of this resource (corresponds to the #name attribute, + # @param name The name of this resource (corresponds to the #name property, # used for notifications to this resource). # @param run_context The context of the Chef run. Corresponds to #run_context. # def initialize(name, run_context=nil) - name(name) + name(name) if name @run_context = run_context @noop = nil @before = nil @@ -131,6 +133,199 @@ class Chef end # + # Properties + # + + # + # Find out whether a property has been set on this resource or not. + # + # This will be true if: + # - The user explicitly set the value + # - The property has a default, and the value has been retrieved. + # - The property has name_property = true and name is set. + # + # 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. + # + # As far as name_property goes, the option name implies that it is more of + # an alias: when `name` is set, this property gets set as well. + # + # @param name [Symbol] The property name to check. + # + # @return [Boolean] Whether the property name has been set or not. + # + # @see Chef::Resource::PropertyType#is_set? + # + def property_is_set?(name) + name = name.to_sym + raise ArgumentError, "Property #{name} does not exist on #{self.class}" if !self.class.properties[name] + type = self.class.properties[name] + type.is_set?(self, name) + end + + # + # Create a property on this resource class. + # + # If a superclass has this property, or if this property has already been + # defined by this resource, this will *override* the previous value. + # + # @param name [Symbol] The name of the property. + # @param type [Object,Array<Object>] The type(s) of this property. + # If present, this is prepended to the `is` validation option. + # @param options [Hash<Symbol,Object>] Validation options. + # @option options [Object,Array] :is An object, or list of + # objects, that must match the value using Ruby's `===` operator + # (`options[:is].any? { |v| v === value }`). + # @option options [Object,Array] :equal_to An object, or list + # of objects, that must be equal to the value using Ruby's `==` + # operator (`options[:is].any? { |v| v == value }`) + # @option options [Regexp,Array<Regexp>] :regex An object, or + # list of objects, that must match the value with `regex.match(value)`. + # @option options [Class,Array<Class>] :kind_of A class, or + # list of classes, that the value must be an instance of. + # @option options [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). + # @option options [Symbol,Array<Symbol>] :respond_to A method + # name, or list of method names, the value must respond to. + # @option options [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). + # @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. + # @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 [Boolean] :name_attribute Same as `name_property`. + # @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). + # @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 desired_state Whether this property is desired state or not. + # Defaults to true. + # + # @return [Chef::Resource::PropertyType] The property type. + # + # @example With nothing + # property :x + # + # @example With just a type + # property :x, String + # + # @example With just options + # property :x, default: 'hi' + # + # @example With type and options + # property :x, String, default: 'hi' + # + def self.property(name, type=NULL_ARG, **options) + name = name.to_sym + + # Handle name_attribute -> name_property + options[:name_property] ||= options.delete(:name_attribute) if options.has_key?(:name_attribute) + + # Create the type that will drive the get/set + local_properties[name] = property_type(type, options) + + # Make the getter and setter methods + # + # NOTE: We eval a string so that the name of the property will show up in + # the stack rather than "property" + class_eval <<-EOM, __FILE__, __LINE__+1 + def #{name}(value=NULL_ARG) + if value == NULL_ARG + self.class.properties[#{name.inspect}].get(self, #{name.inspect}) + elsif value.nil? && !self.class.properties[#{name.inspect}].explicitly_accepts_nil?(self, #{name.inspect}) + # 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.") + self.class.properties[#{name.inspect}].get(self, #{name.inspect}) + else + self.class.properties[#{name.inspect}].set(self, #{name.inspect}, value) + end + end + def #{name}=(value) + self.class.properties[#{name.inspect}].set(self, #{name.inspect}, value) + end + EOM + end + + # + # Create a property type that can be attached to multiple properties. + # + # @param name [Symbol] The name of the property. + # @param type [Object,Array<Object>] The type(s) of this property. + # If present, this is prepended to the `is` validation option. + # @param options [Hash<Symbol,Object>] Validation options. + # See #property for a list of valid options. + # + # @return [Chef::Resource::PropertyType] The created property type + # + def self.property_type(type=NULL_ARG, **options) + return type if type.is_a?(PropertyType) && options.empty? + + # Add type to options[:is] + if type != NULL_ARG + is = options[:is] + + type = [ type ] if !type.is_a?(Array) + options[:is] = type + if is + is = [ is ] if !is.is_a?(Array) + options[:is] += is + end + end + + PropertyType.new(options) + end + + # + # The list of properties on this resource. + # + # Includes properties from the superclass. + # + # @return [Hash<Symbol,Chef::Resource::PropertyType>] A hash from property + # name to property type. + # + def self.properties + if superclass.respond_to?(:properties) + superclass.properties.merge(local_properties) + else + local_properties + end + end + + # + # The list of properties on this resource. + # + # Does *not* include properties from the superclass. + # + # @return [Hash<Symbol,Chef::Resource::PropertyType>] A hash from property + # name to property type. + # + def self.local_properties + @local_properties ||= {} + end + class<<self; protected :local_properties; end + + # # The name of this particular resource. # # This special resource attribute is set automatically from the declaration @@ -153,16 +348,7 @@ class Chef # @param name [Object] The name to set, typically a String or Array # @return [String] The name of this Resource. # - def name(name=nil) - if !name.nil? - if name.is_a?(Array) - @name = name.join(', ') - else - @name = name.to_s - end - end - @name - end + property :name, String, coerce: proc { |name| name.is_a?(Array) ? name.join(', ') : name.to_s }, desired_state: false # # The action or actions that will be taken when this resource is run. @@ -475,13 +661,18 @@ class Chef # # Get the value of the state attributes in this resource as a hash. # + # Does not include properties that are not set. + # # @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 = {} + self.class.state_attrs.each do |name| + next if self.class.properties[name] && !property_is_set?(name) + state[name] = send(name) end + state end # @@ -494,17 +685,25 @@ 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. Properties that + # are not set are not included in the 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 + identity_properties = self.class.properties.select { |name,type| type.identity? } + identity_properties = { name: self.class.properties[:name] } if identity_properties.empty? + + result = {} + identity_properties.each do |name, type| + result[name] = send(name) if property_is_set?(name) end + return result.values.first if identity_properties.size == 1 + result end # @@ -703,6 +902,7 @@ class Chef provider(arg) 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 @@ -715,36 +915,93 @@ class Chef # # This list is used by the Chef client auditing system to extract # information from resources to describe changes made to the system. + # + # @deprecated This is not deprecated, but it is no longer preferred: + # you should use the `desired_state` option to Chef::Resource.property + # to denote non-shared state instead: + # + # ```ruby + # property :x, desired_state: false + # ``` + # def self.state_attrs(*attr_names) - @state_attrs ||= [] - @state_attrs = attr_names unless attr_names.empty? + if !attr_names.empty? + attr_names = attr_names.map { |name| name.to_sym } + + # attr_names *always* includes superclass.attr_names + attr_names -= superclass.attr_names if superclass.respond_to?(:attr_names) + + # Add new properties to the list. + attr_names.each do |name| + type = properties[name] + if type + local_properties[name] = type.specialize(desired_state: true) if !type.desired_state? + else + local_properties[name] = ImplicitPropertyType.new + end + end - # 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 + # If state_attrs *excludes* something which is currently desired state, + # mark it as not desired state. + local_properties.each do |name,type| + if type.desired_state? && !attr_names.include?(name) + local_properties[name] = type.specialize(desired_state: false) + end + end end + + # Grab properties representing desired state + properties.select { |name,type| type.desired_state? }.map { |name,type| name } 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 + # + # Create a lazy value. + # + # @param block The block to run to get the value. This block will be invoked + # with no parameters and the returned value will be used as the lazy value. + # + # @return [Chef::DelayedEvaluator] A lazy value object assignable to resource + # properties and defaults. + # + def self.lazy(&block) + Chef::DelayedEvaluator.new(&block) + 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 a property as the "identity attribute" for this resource. + # + # Unsets "identity attribute" on all other property. + # + # @param name [Symbol] + # + # @return [Symbol] + # + # @deprecated This is no longer the preferred way of doing this: instead, + # pass identity: true to `Chef::Resource.property`. + # + def self.identity_attr(name=nil) + if name + name = name.to_sym + # Switch off + properties.each do |prop_name,type| + if type.identity? && prop_name != name + local_properties[prop_name] = type.specialize(identity: false) + end + end + # Grab the existing type, and specialize it if it exists. + type = properties[name] + if type + type = type.specialize(identity: true) + else + type = ImplicitPropertyType.new(identity: true) + end + local_properties[name] = type end + + properties.select { |name,type| type.identity? }.map { |name,type| name }.first || :name end + # # The guard interpreter that will be used to process `only_if` and `not_if` # statements by default. If left unset, or set to `:default`, the guard diff --git a/lib/chef/resource/deploy.rb b/lib/chef/resource/deploy.rb index 8d007df348..067c6f5533 100644 --- a/lib/chef/resource/deploy.rb +++ b/lib/chef/resource/deploy.rb @@ -52,7 +52,7 @@ class Chef class Deploy < Chef::Resource use_automatic_resource_name - identity_attr :repository + identity_attr :repo state_attrs :deploy_to, :revision diff --git a/lib/chef/resource/implicit_property_type.rb b/lib/chef/resource/implicit_property_type.rb new file mode 100644 index 0000000000..f9920f9216 --- /dev/null +++ b/lib/chef/resource/implicit_property_type.rb @@ -0,0 +1,22 @@ +require 'chef/resource/property_type' + +class Chef + class Resource + # + # When the Resource class creates a property by itself, the user is using + # their own methods to manage state. We don't make any assumptions about + # where the data is stored, in that case. + # + class ImplicitPropertyType < PropertyType + def get_value(resource, name) + resource.send(name) + end + def set_value(resource, name, value) + resource.send(name, value) + end + def value_is_set?(resource, name) + true + end + end + end +end diff --git a/lib/chef/resource/lwrp_base.rb b/lib/chef/resource/lwrp_base.rb index 129fc38d6f..5fe427cc28 100644 --- a/lib/chef/resource/lwrp_base.rb +++ b/lib/chef/resource/lwrp_base.rb @@ -25,7 +25,6 @@ require 'chef/log' require 'chef/exceptions' require 'chef/mixin/convert_to_class_name' require 'chef/mixin/from_file' -require 'chef/mixin/params_validate' # for DelayedEvaluator class Chef class Resource @@ -76,11 +75,7 @@ class Chef # Define an attribute on this resource, including optional validation # parameters. - def attribute(attr_name, validation_opts={}) - define_method(attr_name) do |arg=nil| - set_or_return(attr_name.to_sym, arg, validation_opts) - end - end + alias :attribute :property # Sets the default action def default_action(action_name=NULL_ARG) @@ -128,10 +123,6 @@ class Chef run_context ? run_context.node : nil end - def lazy(&block) - DelayedEvaluator.new(&block) - end - protected def loaded_lwrps diff --git a/lib/chef/resource/property_type.rb b/lib/chef/resource/property_type.rb new file mode 100644 index 0000000000..8393f8cefd --- /dev/null +++ b/lib/chef/resource/property_type.rb @@ -0,0 +1,583 @@ +require 'chef/exceptions' +require 'chef/delayed_evaluator' + +class Chef + class Resource + # + # 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 fully + # support for lazy values. + # + # @see Chef::Resource.property + # @see Chef::DelayedEvaluator + # + class PropertyType + # + # Create a new property type. + # + # @raise ArgumentError If `:callbacks` is not a Hash. + # + def initialize( + is: NULL_ARG, + equal_to: NULL_ARG, + regex: NULL_ARG, + kind_of: NULL_ARG, + respond_to: NULL_ARG, + cannot_be: NULL_ARG, + callbacks: NULL_ARG, + + coerce: NULL_ARG, + required: NULL_ARG, + name_property: NULL_ARG, + default: NULL_ARG, + desired_state: NULL_ARG, + identity: NULL_ARG + ) + # Validation args + @is = [ is ].flatten(1) unless is == NULL_ARG + @equal_to = [ equal_to ].flatten(1) unless equal_to == NULL_ARG + @regex = [ regex ].flatten(1) unless regex == NULL_ARG + @kind_of = [ kind_of ].flatten(1) unless kind_of == NULL_ARG + @respond_to = [ respond_to ].flatten(1).map { |v| v.to_sym } unless respond_to == NULL_ARG + @cannot_be = [ cannot_be ].flatten(1).map { |v| v.to_sym } unless cannot_be == NULL_ARG + @callbacks = callbacks unless callbacks == NULL_ARG + + # Other properties + @coerce = coerce unless coerce == NULL_ARG + @required = required unless required == NULL_ARG + @name_property = name_property unless name_property == NULL_ARG + @default = default unless default == NULL_ARG + @desired_state = desired_state unless desired_state == NULL_ARG + @identity = identity unless identity == NULL_ARG + + raise ArgumentError, "Callback list must be a hash, is #{callbacks.inspect}!" if callbacks != NULL_ARG && !callbacks.is_a?(Hash) + end + + # + # List of valid things values can be. + # + # Uses Ruby's `===` to evaluate (is === value). At least one must match + # for the value to be valid. + # + # If a proc is passed, it is instance_eval'd in the resource, passed the + # value, and must return a truthy or falsey value. + # + # @example Class + # ```ruby + # property :x, String + # x 'valid' #=> valid + # x 1 #=> invalid + # x nil #=> invalid + # + # @example Value + # ```ruby + # property :x, [ :a, :b, :c, nil ] + # x :a #=> valid + # x nil #=> valid + # ``` + # + # @example Regex + # ```ruby + # property :x, /bar/ + # x 'foobar' #=> valid + # x 'foo' #=> invalid + # x nil #=> invalid + # ``` + # + # @example Proc + # ```ruby + # property :x, proc { |x| x > y } + # property :y, default: 2 + # x 3 #=> valid + # x 1 #=> invalid + # ``` + # + # @example PropertyType + # ```ruby + # type = PropertyType.new(is: String) + # property :x, type + # x 'foo' #=> valid + # x 1 #=> invalid + # x nil #=> invalid + # ``` + # + # @example RSpec Matcher + # ```ruby + # include RSpec::Matchers + # property :x, a_string_matching /bar/ + # x 'foobar' #=> valid + # x 'foo' #=> invalid + # x nil #=> invalid + # ``` + # + # @return [Array,nil] List of things this is, or nil if "is" is unspecified. + # + attr_reader :is + + # + # List of things values must be equal to. + # + # Uses Ruby's `==` to evaluate (equal_to == value). At least one must + # match for the value to be valid. + # + # @return [Array,nil] List of things values must be equal to, or nil if + # equal_to is unspecified. + # + attr_reader :equal_to + + # + # List of regexes values must match. + # + # Uses regex.match() to evaluate. At least one must match for the value to + # be valid. + # + # @return [Array<Regex>,nil] List of regexes values must match, or nil if + # regex is unspecified. + # + attr_reader :regex + + # + # List of things values must be equal to. + # + # Uses value.kind_of?(kind_of) to evaluate. At least one must match for + # the value to be valid. + # + # @return [Array<Class>,nil] List of classes values must be equal to, or nil if + # kind_of is unspecified. + # + attr_reader :kind_of + + # + # List of method names values must respond to. + # + # Uses value.respond_to?(respond_to) to evaluate. At least one must match + # for the value to be valid. + # + # @return [Array<Symbol>,nil] List of classes values must be equal to, or + # `nil` if respond_to is unspecified. + # + attr_reader :respond_to + + # + # List of things that must not be true about the value. + # + # Calls `value.<thing>?` All responses must be false. Values which do not + # respond to <thing>? are considered valid (because if a value doesn't + # respond to `:readable?`, then it probably isn't readable.) + # + # @return [Array<Symbol>,nil] List of classes values must be equal to, or + # `nil` if cannot_be is unspecified. + # + # @example + # ```ruby + # property :x, cannot_be: [ :nil, :empty ] + # x [ 1, 2 ] #=> valid + # x 1 #=> valid + # x [] #=> invalid + # x nil #=> invalid + # ``` + # + attr_reader :cannot_be + + # + # List of procs we pass the value to. + # + # All procs must return true for the value to be valid. If any procs do + # not return true, the key will be used for the message: `"Property x's + # value :y <message>"`. + # + # @return [Hash<String,Proc>,nil] Hash of procs which must match, with + # their messages as the key. `nil` if callbacks is unspecified. + # + attr_reader :callbacks + + # + # Whether this is required or not. + # + # @return [Boolean] + # + # @deprecated use default: lazy { name } instead. + def required? + @required + end + + # + # Whether this is part of the resource's natural identity or not. + # + # @return [Boolean] + # + # @deprecated use default: lazy { name } instead. + def identity? + @identity + end + + # + # Whether this is part of desired state or not. + # + # @return [Boolean] + # + # @deprecated use default: lazy { name } instead. + def desired_state? + defined?(@desired_state) ? @desired_state : true + end + + # + # Whether this is name_property or not. + # + # @return [Boolean] + # + # @deprecated use default: lazy { name } instead. + def name_property? + @name_property + end + + # + # Whether this has a default value or not. + # + # @return [Boolean] + # + def default? + defined?(@default) + end + + # + # Get the property value from the resource, handling lazy values, + # defaults, and validation. + # + # - If the property's value is lazy, the lazy value is evaluated, coerced + # and validated, and the result stored in the property (it will not be + # evaluated twice). + # - If the property has no value, but has a default, the default value + # will be returned. 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. + # @param name [Symbol] The name of the property to set. + # + # @return The value of the property. + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property. + # + def get(resource, name) + # Grab the value + if value_is_set?(resource, name) + value = get_value(resource, name) + + # Use the default if it is there. + elsif default? + value = set(resource, name, default) + + # Last ditch: if name_property is set, get that + elsif name_property? && name != :name + value = set(resource, name, resource.name) + end + + # If the value is lazy, pop it open and store it + if value.is_a?(DelayedEvaluator) + value = set(resource, name, resource.instance_eval(&value)) + end + + value + end + + # + # Get the default value for this property. + # + # - If the property has no value, but has a default, the default value + # will be returned. If the default value is lazy, it will be evaluated, + # coerced and validated. + # - If the property has no value, but is name_property, `resource.name` + # is returned. + # - Otherwise, `nil` is returned. + # + # This differs from `get` in that it will *not* store the default value in + # the given resource. + # + # If resource and name are not passed, the default is returned without + # evaluation, coercion or validation, and name_property is not honored. + # + # @param resource [Chef::Resource] The resource to get the default against. + # @param name [Symbol] The name of the property to get the default of. + # + # @return The default value for the property. + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property. + # + def default(resource=nil, name=nil) + return @default if !resource && !name + + if defined?(@default) + coerce(resource, name, @default) + elsif name_property? && name != :name + resource.name + else + nil + 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 name [Symbol] The name of the property to set. + # @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, name, value) + value = coerce(resource, name, value) + set_value(resource, name, value) + end + + # + # Find out whether this property has been set. + # + # This will be true if: + # - The user explicitly set the value + # - The property is name_property and name has been set + # - 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. + # @param name [Symbol] The name of the property to get. + # + # @return [Boolean] + # + def is_set?(resource, name) + value_is_set?(resource, name) || + (name_property? && value_is_set?(resource, :name)) + end + + # + # Coerce an input value into canonical form for the property, validating + # it in the process. + # + # After coercion, the value is suitable for storage in the resource. + # + # Does not coerce or validate lazy values. + # + # @param resource [Chef::Resource] The resource we're coercing against + # (to provide context for the coerce). + # @param name [Symbol] The name of the property we're coercing (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, name, value) + if !value.is_a?(DelayedEvaluator) + value = resource.instance_exec(value, &@coerce) if @coerce + errors = validate(resource, name, value) + raise Chef::Exceptions::ValidationFailed, errors.map { |e| "Property #{name}'s #{e}" }.join("\n") if errors + end + value + end + + # + # Validate a value. + # + # Honors #is, #equal_to, #regex, #kind_of, #respond_to, #cannot_be, and + # #callbacks. + # + # @param resource [Chef::Resource] The resource we're validating against + # (to provide context for the validate). + # @param name [Symbol] The name of the property we're validating (to provide + # context for the validate). + # @param value The value to validate. + # + # @return [Array<String>,nil] A list of errors, or nil if there was no error. + # + def validate(resource, name, value) + errors = [] + + # "is": capture the first type match so we can use it as the supertype + # for this value. + error_unless_any_match(errors, value, is, "is not") do |v| + case v + when Proc + resource.instance_exec(value, &v) + when PropertyType + got_errors = v.validate(resource, name, value) + errors += got_errors if got_errors + true + else + v === value + end + end + + # equal_to + error_unless_any_match(errors, value, equal_to, "does not equal") do |v| + v == value + end + + # regex + error_unless_any_match(errors, value, regex, "does not match") do |v| + value.is_a?(String) && v.match(value) + end + + # kind_of + error_unless_any_match(errors, value, kind_of, "is not of type") do |v| + value.kind_of?(v) + end + + # respond_to + error_unless_all_match(errors, value, respond_to, "does not respond to") do |v| + value.respond_to?(v) + end + + # cannot_be + error_unless_all_match(errors, value, cannot_be, "is") do |v| + !(value.respond_to?("#{v}?") && value.send("#{v}?")) + end + + # callbacks + error_unless_callbacks_match(errors, value, callbacks) + + errors.empty? ? nil : errors + end + + # + # Find out whether this type accepts nil explicitly. + # + # A type accepts nil explicitly if it validates as nil, *and* is not simply + # an empty type. + # + # These examples accept nil explicitly: + # ```ruby + # property :a, [ String, nil ] + # property :a, is: [ String, nil ] + # property :a, equal_to: [ 1, 2, 3, nil ] + # property :a, kind_of: [ String, NilClass ] + # property :a, respond_to: [ ] + # ``` + # + # These do not: + # ```ruby + # property :a, [ String, nil ], cannot_be: :nil + # property :a, callbacks: { x: } + # ``` + # + # This does not either (accepts nil implicitly only): + # ```ruby + # property :a + # ``` + # + # @param resource [Chef::Resource] The resource we're coercing against + # (to provide context for the coerce). + # @param name [Symbol] The name of the property we're coercing (to provide + # context for the coerce). + # + # @return [Boolean] Whether this value explicitly accepts nil. + # + # @api private + def explicitly_accepts_nil?(resource, name) + return false if !validates_values? + + !validate(resource, name, nil) + end + + # + # Specialize this PropertyType by adding or changing some options. + # + def specialize(**options) + options[:is] = [ self ] + (options[:is] || []) + options[:coerce] = @coerce if defined?(@coerce) && !options.has_key?(:coerce) + options[:required] = @required if defined?(@required) && !options.has_key?(:coerce) + options[:name_property] = @name_property if defined?(@name_property) && !options.has_key?(:name_property) + options[:default] = @default if defined?(@default) && !options.has_key?(:default) + options[:desired_state] = @desired_state if defined?(@desired_state) && !options.has_key?(:desired_state) + options[:identity] = @identity if defined?(@identity) && !options.has_key?(:identity) + self.class.new(options) + end + + protected + + def error_unless_all_match(errors, value, match_values, message, &matcher) + if match_values && !match_values.empty? + match_values.each do |v| + if !matcher.call(v) + errors << "value #{value.inspect} #{message} #{v.inspect}" + end + end + end + end + + def error_unless_any_match(errors, value, match_values, message, &matcher) + if match_values && !match_values.empty? + if !match_values.any?(&matcher) + errors << "value #{value.inspect} #{message} #{english_join(match_values)}" + end + end + end + + def error_unless_callbacks_match(errors, value, callbacks) + if callbacks && !callbacks.empty? + callbacks.each do |message, callback| + if !callback.call(value) + errors << "value #{value.inspect} #{message}" + end + end + end + end + + def english_join(values) + return '<nothing>' if values.size == 0 + return values[0].inspect if values.size == 1 + "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}" + end + + # + # Whether this resource actually validates values. + # + # Returns true if there are any validation options that depend on the + # actual value. Does not check for coerce, default, required or + # name_property. + # + # @return [Boolean] Whether this resource validates anything. + # + def validates_values? + # If any validation option exists and *isn't* an empty hash / array, we + # will indeed spend time validating values. + %w(is equal_to regex kind_of respond_to cannot_be callbacks).any? do |option| + send(option) && !send(option).empty? + end + end + + def get_value(resource, name) + resource.instance_variable_get(:"@#{name}") + end + def set_value(resource, name, value) + resource.instance_variable_set(:"@#{name}", value) + end + def value_is_set?(resource, name) + resource.instance_variable_defined?(:"@#{name}") + end + end + end +end diff --git a/spec/integration/recipes/property_spec.rb b/spec/integration/recipes/property_spec.rb new file mode 100644 index 0000000000..4e7e0e4362 --- /dev/null +++ b/spec/integration/recipes/property_spec.rb @@ -0,0 +1,16 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource.property" do + include IntegrationSupport + + # Basic properties + # Inheritance + # default + # name_attribute + # coerce + # lazy + # identity + # desired_state + # to hash, json + # TODO "is" and types: coercion, defaults, name_attribute, identity, lazy values, and validation +end diff --git a/spec/integration/recipes/property_validation_spec.rb b/spec/integration/recipes/property_validation_spec.rb new file mode 100644 index 0000000000..c6811d43bc --- /dev/null +++ b/spec/integration/recipes/property_validation_spec.rb @@ -0,0 +1,16 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource::.property validation" do + include IntegrationSupport + + # Bare types + # is + # - Class, Regex, Symbol, nil, PropertyType, RSpec::Matcher + # equal_to + # kind_of + # regex + # callbacks + # respond_to + # cannot_be + # required +end diff --git a/spec/unit/resource_property_spec.rb b/spec/unit/resource_property_spec.rb new file mode 100644 index 0000000000..516ab533f2 --- /dev/null +++ b/spec/unit/resource_property_spec.rb @@ -0,0 +1,744 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource.property" do + include IntegrationSupport + + class Namer + @i = 0 + def self.next_resource_name + "chef_resource_property_spec_#{@i += 1}" + end + def self.reset_index + @current_index = 0 + end + def self.current_index + @current_index + end + def self.next_index + @current_index += 1 + end + end + + def lazy(&block) + Chef::DelayedEvaluator.new(&block) + end + + before do + Namer.reset_index + end + + def self.new_resource_name + Namer.next_resource_name + end + + let(:resource_class) do + new_resource_name = self.class.new_resource_name + Class.new(Chef::Resource) do + resource_name new_resource_name + def next_index + Namer.next_index + end + end + end + + let(:resource) do + resource_class.new("blah") + end + + def self.english_join(values) + return '<nothing>' if values.size == 0 + return values[0].inspect if values.size == 1 + "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}" + end + + def self.with_property(*properties, &block) + tags_index = properties.find_index { |p| !p.is_a?(String)} + if tags_index + properties, tags = properties[0..tags_index-1], properties[tags_index..-1] + else + tags = [] + end + properties = properties.map { |property| "property #{property}" } + context "With properties #{english_join(properties)}", *tags do + before do + properties.each do |property_str| + resource_class.class_eval(property_str, __FILE__, __LINE__) + end + end + instance_eval(&block) + end + end + + # Basic properties + with_property ":bare_property" do + it "can be set" 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 + 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 eq 10 + expect(resource.bare_property).to eq 10 + end + it "can be updated" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property 20).to eq 20 + expect(resource.bare_property).to eq 20 + end + it "can be set with =" 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 updated with =" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property = 20).to eq 20 + expect(resource.bare_property).to eq 20 + end + end + + with_property ":x, Integer" do + context "and subclass" 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 "x is inherited" 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 + end + + it "x's validation is inherited" do + expect { subresource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + end + + context "with property :y on the subclass" do + before do + subresource_class.class_eval do + property :y + 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 + 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 + 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 + end + end + + context "with property :x on the subclass" do + before do + subresource_class.class_eval do + property :x + 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's validation is overwritten" do + expect(subresource.x 'ohno').to eq 'ohno' + expect(subresource.x).to eq 'ohno' + end + + it "the superclass's validation for x is still there" do + expect { resource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + context "with property :x, String on the subclass" do + before do + subresource_class.class_eval do + property :x, String + 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's validation is overwritten" do + expect { subresource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + expect(subresource.x 'ohno').to eq 'ohno' + expect(subresource.x).to eq 'ohno' + end + + it "the superclass's validation for x is still there" do + expect { resource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + expect(resource.x 10).to eq 10 + expect(resource.x).to eq 10 + end + end + end + end + + context "Chef::Resource::PropertyType#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 + + with_property ":x" do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set to a lazy value, property_is_set?(:x) is true" 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 false" do + resource.x + expect(resource.property_is_set?(:x)).to be_falsey + end + end + + with_property ":x, default: 10" do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set to a lazy value, property_is_set?(:x) is true" 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 + resource.x + expect(resource.property_is_set?(:x)).to be_truthy + end + end + + with_property ":x, default: nil" do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set to a lazy value, property_is_set?(:x) is true" 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 + resource.x + expect(resource.property_is_set?(:x)).to be_truthy + end + end + + with_property ":x, default: lazy { 10 }" do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" 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 + resource.x + expect(resource.property_is_set?(:x)).to be_truthy + end + end + end + + context "Chef::Resource::PropertyType#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 + expect(resource.property_is_set?(:x)).to be_truthy + expect(resource.x).to eq 20 + end + it "when x is not set, it returns 10" 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({}) + end + + context "With a subclass" 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) { subresource_class.new('blah') } + it "The default is inherited" do + expect(subresource.x).to eq 10 + end + end + 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({}) + end + end + + with_property ":x, default: nil" do + it "when x is not set, it returns nil" do + expect(resource.x).to be_nil + end + end + + with_property ":x" do + it "when x is not set, it returns nil" do + expect(resource.x).to be_nil + end + end + + context "hash default" do + with_property ":x, default: {}" do + it "when x is not set, it returns {}" do + expect(resource.x).to eq({}) + end + it "The same exact value is returned multiple times in a row" do + value = resource.x + expect(value).to eq({}) + expect(resource.x.object_id).to eq(value.object_id) + end + it "Multiple instances of x receive the exact same value" do + # TODO this isn't really great behavior, but it's noted here so we find out + # if it changed. + expect(resource.x.object_id).to eq(resource_class.new('blah2').x.object_id) + end + end + + with_property ":x, default: lazy { {} }" do + it "when x is not set, it returns {}" do + expect(resource.x).to eq({}) + end + it "The same exact value is returned multiple times in a row" do + value = resource.x + expect(value).to eq({}) + expect(resource.x.object_id).to eq(value.object_id) + end + it "Multiple instances of x receive different values" do + expect(resource.x.object_id).not_to eq(resource_class.new('blah2').x.object_id) + end + end + end + + context "with a class with 'blah' as both class and instance methods" do + before do + resource_class.class_eval do + def self.blah + 'class' + end + def blah + "instance#{next_index}" + end + end + end + with_property ":x, default: lazy { blah }" do + it "x is run in context of the instance" do + expect(resource.x).to eq "instance1" + end + it "x is run in the context of each instance it is run in" do + expect(resource.x).to eq "instance1" + expect(resource_class.new('blah2').x).to eq "instance2" + expect(resource.x).to eq "instance1" + end + end + end + + context "validation of defaults" do + with_property ":x, String, default: 10" do + it "when the resource is created, no error is raised" do + resource + end + it "when x is set, no error is raised" do + expect(resource.x 'hi').to eq 'hi' + expect(resource.x).to eq 'hi' + end + it "when x is retrieved, a validation error is raised" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + with_property ":x, String, default: lazy { Namer.next_index }" do + it "when the resource is created, no error is raised" do + resource + end + it "when x is set, no error is raised" do + expect(resource.x 'hi').to eq 'hi' + expect(resource.x).to eq 'hi' + end + it "when x is retrieved, a validation error is raised" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + expect(Namer.current_index).to eq 1 + end + end + + with_property ":x, default: lazy { Namer.next_index }, is: proc { |v| Namer.next_index; true }" do + it "when x is retrieved, validation is run no more than once" 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 + end + + context "coercion of defaults" do + 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 + 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, no more than once" do + expect(resource.x).to eq '101' + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + end + end + + 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 + 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 + end + + with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }, is: proc { |v| Namer.next_index; true }' do + it "when x is retrieved, coercion is run, no more than once" do + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 2 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 2 + end + end + + context "validation and coercion of defaults" do + with_property ':x, String, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do + it "when x is retrieved, it is coerced before validating and passes" do + expect(resource.x).to eq '101' + end + end + with_property ':x, Integer, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do + it "when x is retrieved, it is coerced before validating and fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + end + with_property ':x, String, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "when x is retrieved, it is coerced before validating and passes" do + expect(resource.x).to eq '101' + end + end + with_property ':x, Integer, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "when x is retrieved, it is coerced before validating and fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + end + with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }, is: proc { |v| Namer.next_index; true }' do + it "when x is retrieved, coercion and validation is run exactly once per instance" do + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 2 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 2 + end + end + end + end + end + + context "Chef::Resource#lazy" do + with_property ':x' do + it "setting x to a lazy value does not run it immediately" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "you can set x to a lazy value in the instance" do + resource.instance_eval do + x lazy { Namer.next_index } + end + expect(resource.x).to eq 1 + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 1 + end + it "retrieving a lazy value pops it open" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 1 + end + it "retrieving a lazy value twice does not run it a second time" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq 1 + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 1 + end + it "setting the same lazy value on two different instances will run it twice" do + resource2 = resource_class.new("blah2") + l = lazy { Namer.next_index } + resource.x l + resource2.x l + expect(resource2.x).to eq 1 + expect(resource.x).to eq 2 + expect(resource2.x).to eq 1 + end + + context "when the class has a class and instance method named blah" do + before do + resource_class.class_eval do + def self.blah + "class" + end + def blah + "instance#{Namer.next_index}" + end + end + end + it "retrieving lazy { blah } gets the instance variable" do + resource.x lazy { blah } + expect(resource.x).to eq "instance1" + end + it "retrieving lazy { blah } from two different instances gets two different instance variables" do + resource2 = resource_class.new("blah2") + l = lazy { blah } + resource2.x l + resource.x l + expect(resource2.x).to eq "instance1" + expect(resource.x).to eq "instance2" + expect(resource2.x).to eq "instance1" + end + end + end + + with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "lazy values are not coerced on set" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "lazy values are coerced on get" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 2 + end + it "lazy values are coerced exactly once" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 2 + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 2 + end + end + + with_property ':x, String' do + it "lazy values are not validated on set" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "lazy values are validated on get" do + resource.x lazy { Namer.next_index } + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + expect(Namer.current_index).to eq 1 + end + end + + with_property ':x, is: proc { |v| Namer.next_index; true }' do + it "lazy values are validated exactly once" do + resource.x lazy { Namer.next_index } + 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, Integer, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "lazy values are not validated or coerced on set" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "lazy values are coerced before being validated, which fails" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + expect(Namer.current_index).to eq 2 + end + end + + with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }, is: proc { |v| Namer.next_index; true }' do + it "lazy values are coerced and validated exactly once" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 3 + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 3 + end + end + + with_property ':x, String, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "lazy values are coerced before being validated, which succeeds" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 2 + end + end + end + + context "Chef::Resource::PropertyType#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" + expect(Namer.current_index).to eq 1 + end + it "coercion sets the value (and coercion does not run on get)" do + expect(resource.x 10).to eq "101" + expect(resource.x).to eq "101" + expect(Namer.current_index).to eq 1 + end + it "coercion runs each time set happens" do + expect(resource.x 10).to eq "101" + expect(Namer.current_index).to eq 1 + expect(resource.x 10).to eq "102" + expect(Namer.current_index).to eq 2 + end + end + with_property ':x, coerce: proc { |x| puts "hi"; Namer.next_index; raise "hi" if x == 10; x }, is: proc { |x| Namer.next_index; x != 10 }' do + it "failed coercion fails to set the value" do + resource.x 20 + expect(resource.x).to eq 20 + expect(Namer.current_index).to eq 2 + expect { resource.x 10 }.to raise_error 'hi' + expect(resource.x).to eq 20 + expect(Namer.current_index).to eq 3 + end + it "validation does not run if coercion fails" do + expect { resource.x 10 }.to raise_error 'hi' + expect(Namer.current_index).to eq 1 + end + end + end + + context "Chef::Resource::PropertyType 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 + expect(Namer.current_index).to eq 1 + end + it "validation sets the value (and validation does not run on get)" do + expect(resource.x 10).to eq 10 + expect(resource.x).to eq 10 + expect(Namer.current_index).to eq 1 + end + it "validation runs each time set happens" do + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 1 + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 2 + end + it "failed validation fails to set the value" do + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 1 + expect { resource.x 'blah' }.to raise_error Chef::Exceptions::ValidationFailed + expect(resource.x).to eq 10 + expect(Namer.current_index).to eq 2 + end + end + end + + context "Chef::Resource::PropertyType#name_property" do + with_property ':x, name_property: true' do + it "defaults x to resource.name" do + expect(resource.x).to eq 'blah' + end + it "does not pick up resource.name if set" do + expect(resource.x 10).to eq 10 + expect(resource.x).to eq 10 + end + it "binds to the latest resource.name when run" do + resource.name = 'foo' + expect(resource.x).to eq 'foo' + end + it "does not pick up later instances of resource.name" do + # TODO honestly not sure this is right, but let's at least test what we + # currently do so that if it changes, we know about it. + expect(resource.x).to eq 'blah' + resource.name = 'foo' + expect(resource.x).to eq 'blah' + end + end + with_property ':x, name_property: true, default: 10' do + it "chooses default over name_property" do + expect(resource.x).to eq 10 + end + end + end + + # TODO "is" on a PropertyType: inheritance of coercion, defaults, name_property, identity, lazy values, and validation +end diff --git a/spec/unit/resource_property_state_spec.rb b/spec/unit/resource_property_state_spec.rb new file mode 100644 index 0000000000..c5117c53e3 --- /dev/null +++ b/spec/unit/resource_property_state_spec.rb @@ -0,0 +1,492 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource#identity and #state" do + include IntegrationSupport + + class NewResourceNamer + @i = 0 + def self.next + "chef_resource_property_spec_#{@i += 1}" + end + end + + def self.new_resource_name + NewResourceNamer.next + end + + let(:resource_class) do + new_resource_name = self.class.new_resource_name + Class.new(Chef::Resource) do + resource_name new_resource_name + end + end + + let(:resource) do + resource_class.new("blah") + end + + def self.english_join(values) + return '<nothing>' if values.size == 0 + return values[0].inspect if values.size == 1 + "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}" + end + + def self.with_property(*properties, &block) + tags_index = properties.find_index { |p| !p.is_a?(String)} + if tags_index + properties, tags = properties[0..tags_index-1], properties[tags_index..-1] + else + tags = [] + end + properties = properties.map { |property| "property #{property}" } + context "With properties #{english_join(properties)}", *tags do + before do + properties.each do |property_str| + resource_class.class_eval(property_str, __FILE__, __LINE__) + end + end + instance_eval(&block) + end + end + + # identity + context "Chef::Resource#identity_attr" 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 "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 + + expect(resource.x 'woo').to eq 'woo' + expect(resource.x).to eq 'woo' + + expect(resource.name).to eq 'blah' + 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 + + context "With a subclass" 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('sub') + 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.name).to eq 'sub' + expect(subresource.identity).to eq 'sub' + end + + context "With identity_attr :x on the superclass" do + before do + resource_class.class_eval do + identity_attr :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 + + 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 + + with_property ":y, String" do + context "With identity_attr :y on the subclass" do + before do + subresource_class.class_eval do + identity_attr :y + end + end + it "y is part of state" do + expect(subresource_class.state_attrs).to eq [ :x, :y ] + subresource.x 'foo' + subresource.y 'bar' + expect(subresource.state).to eq({ x: 'foo', y: 'bar' }) + end + it "y is the identity" do + expect(subresource_class.identity_attr).to eq :y + subresource.x 'foo' + subresource.y 'bar' + expect(subresource.identity).to eq 'bar' + end + it "y still has validation" do + expect { subresource.y 12 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + end + end + 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 + + context "With custom property custom_property defined only as methods, using different variables for storage" do + before do + resource_class.class_eval do + def custom_property + @blarghle*3 + end + def custom_property=(x) + @blarghle = x*2 + end + end + + context "And identity_attr :custom_property" do + before do + resource_class.class_eval do + identity_attr :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 + end + it "custom_property becomes part of desired_state" do + 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 + expect(resource.custom_property = 1).to eq 2 + expect(resource.custom_property).to eq 6 + end + it "custom_property is returned as the identity" do + expect(resource_class.identity_attr).to + expect(resource.identity).to be_nil + resource.custom_property = 1 + expect(resource.identity).to eq 6 + end + it "custom_property is part of desired state" do + resource.custom_property = 1 + expect(resource.state).to eq({ custom_property: 6 }) + end + it "property_is_set?(:custom_property) returns true even if it hasn't been set" do + expect(resource.property_is_set?(:custom_property)).to be_truthy + end + end + end + 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({}) + 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 + context "With a class with a normal getter and setter" do + before do + resource_class.class_eval do + def x + @blah*3 + end + def x=(value) + @blah = value*2 + end + end + end + it "state_attrs(:x) causes the value to be included in properties" do + resource_class.state_attrs(:x) + resource.x = 1 + + expect(resource.x).to eq 6 + expect(resource.state).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 + end + +end diff --git a/spec/unit/resource_property_validation_spec.rb b/spec/unit/resource_property_validation_spec.rb new file mode 100644 index 0000000000..42df2ea003 --- /dev/null +++ b/spec/unit/resource_property_validation_spec.rb @@ -0,0 +1,333 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource.property validation" do + include IntegrationSupport + + class Namer + @i = 0 + def self.next_resource_name + "chef_resource_property_spec_#{@i += 1}" + end + def self.reset_index + @current_index = 0 + end + def self.current_index + @current_index + end + def self.next_index + @current_index += 1 + end + end + + def lazy(&block) + Chef::DelayedEvaluator.new(&block) + end + + before do + Namer.reset_index + end + + def self.new_resource_name + Namer.next_resource_name + end + + let(:resource_class) do + new_resource_name = self.class.new_resource_name + Class.new(Chef::Resource) do + resource_name new_resource_name + def blah + Namer.next_index + end + def self.blah + "class#{Namer.next_index}" + end + end + end + + let(:resource) do + resource_class.new("blah") + end + + def self.english_join(values) + return '<nothing>' if values.size == 0 + return values[0].inspect if values.size == 1 + "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}" + end + + def self.with_property(*properties, &block) + tags_index = properties.find_index { |p| !p.is_a?(String)} + if tags_index + properties, tags = properties[0..tags_index-1], properties[tags_index..-1] + else + tags = [] + end + properties = properties.map { |property| "property #{property}" } + context "With properties #{english_join(properties)}", *tags do + before do + properties.each do |property_str| + resource_class.class_eval(property_str, __FILE__, __LINE__) + end + end + instance_eval(&block) + end + end + + def self.validation_test(validation, success_values, failure_values) + with_property ":x, #{validation}" do + success_values.each do |v| + it "value #{v.inspect} is valid" do + expect(resource.x v).to eq v + end + end + failure_values.each do |v| + if v.nil? + it "setting value to #{v.inspect} does not change the value" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x success_values.first + expect(resource.x v).to eq success_values.first + expect(resource.x).to eq success_values.first + end + else + it "value #{v.inspect} is invalid" do + expect { resource.x v }.to raise_error Chef::Exceptions::ValidationFailed + end + end + end + end + end + + # Bare types + context "bare types" do + validation_test 'String', + [ 'hi' ], + [ 10, nil ] + + validation_test ':a', + [ :a ], + [ :b, nil ] + + validation_test ':a, is: :b', + [ :a, :b ], + [ :c, nil ] + + validation_test ':a, is: [ :b, :c ]', + [ :a, :b, :c ], + [ :d, nil ] + + validation_test '[ :a, :b ], is: :c', + [ :a, :b, :c ], + [ :d, nil ] + + validation_test '[ :a, :b ], is: [ :c, :d ]', + [ :a, :b, :c, :d ], + [ :e, nil ] + + validation_test 'nil', + [ nil ], + [ :a ] + + validation_test '[ nil ]', + [ nil ], + [ :a ] + + validation_test '[]', + [ :a ], + [] + end + + # is + context "is" do + # Class + validation_test 'is: String', + [ 'a', '' ], + [ nil, :a, 1 ] + + # Value + validation_test 'is: :a', + [ :a ], + [ :b, nil ] + + validation_test 'is: [ :a, :b ]', + [ :a, :b ], + [ [ :a, :b ], nil ] + + validation_test 'is: [ [ :a, :b ] ]', + [ [ :a, :b ] ], + [ :a, :b, nil ] + + # Regex + validation_test 'is: /abc/', + [ 'abc', 'wowabcwow' ], + [ '', 'abac', nil ] + + # PropertyType + validation_test 'is: PropertyType.new(is: :a)', + [ :a ], + [ :b, nil ] + + # RSpec Matcher + class Globalses + extend RSpec::Matchers + end + + validation_test "is: Globalses.eq(10)", + [ 10 ], + [ 1, nil ] + + # Proc + validation_test 'is: proc { |x| x }', + [ true, 1 ], + [ false, nil ] + + validation_test 'is: proc { |x| x > blah }', + [ 10 ], + [ -1 ] + + validation_test 'is: nil', + [ nil ], + [ 'a' ] + + validation_test 'is: [ String, nil ]', + [ 'a', nil ], + [ :b ] + end + + # Combination + context "combination" do + validation_test 'is: String, equal_to: "a"', + [ 'a' ], + [ 'b', nil ] + end + + # equal_to + context "equal_to" do + # Value + validation_test 'equal_to: :a', + [ :a ], + [ :b, nil ] + + validation_test 'equal_to: [ :a, :b ]', + [ :a, :b ], + [ [ :a, :b ], nil ] + + validation_test 'equal_to: [ [ :a, :b ] ]', + [ [ :a, :b ] ], + [ :a, :b, nil ] + + validation_test 'equal_to: nil', + [ nil ], + [ 'a' ] + + validation_test 'equal_to: [ "a", nil ]', + [ 'a', nil ], + [ 'b' ] + + validation_test 'equal_to: [ nil, "a" ]', + [ 'a', nil ], + [ 'b' ] + end + + # kind_of + context "kind_of" do + validation_test 'kind_of: String', + [ 'a' ], + [ :b, nil ] + + validation_test 'kind_of: [ String, Symbol ]', + [ 'a', :b ], + [ 1, nil ] + + validation_test 'kind_of: [ Symbol, String ]', + [ 'a', :b ], + [ 1, nil ] + + validation_test 'kind_of: NilClass', + [ nil ], + [ 'a' ] + + validation_test 'kind_of: [ NilClass, String ]', + [ nil, 'a' ], + [ :a ] + end + + # regex + context "regex" do + validation_test 'regex: /abc/', + [ 'xabcy' ], + [ 'gbh', 123, nil ] + + validation_test 'regex: [ /abc/, /z/ ]', + [ 'xabcy', 'aza' ], + [ 'gbh', 123, nil ] + + validation_test 'regex: [ /z/, /abc/ ]', + [ 'xabcy', 'aza' ], + [ 'gbh', 123, nil ] + end + + # callbacks + context "callbacks" do + validation_test 'callbacks: { "a" => proc { |x| x > 10 }, "b" => proc { |x| x%2 == 0 } }', + [ 12 ], + [ 11, 4 ] + + validation_test 'callbacks: { "a" => proc { |x| x%2 == 0 }, "b" => proc { |x| x > 10 } }', + [ 12 ], + [ 11, 4 ] + + validation_test 'callbacks: { "a" => proc { |x| x.nil? } }', + [ nil ], + [ 'a' ] + end + + # respond_to + context "respond_to" do + validation_test 'respond_to: :split', + [ 'hi' ], + [ 1, nil ] + + validation_test 'respond_to: "split"', + [ 'hi' ], + [ 1, nil ] + + validation_test 'respond_to: [ :split, :to_s ]', + [ 'hi' ], + [ 1, nil ] + + validation_test 'respond_to: %w(split to_s)', + [ 'hi' ], + [ 1, nil ] + + validation_test 'respond_to: [ :to_s, :split ]', + [ 'hi' ], + [ 1, nil ] + end + + context "cannot_be" do + validation_test 'cannot_be: :empty', + [ nil, 1, [1,2], { a: 10 } ], + [ [] ] + + validation_test 'cannot_be: "empty"', + [ nil, 1, [1,2], { a: 10 } ], + [ [] ] + + validation_test 'cannot_be: [ :empty, :nil ]', + [ 1, [1,2], { a: 10 } ], + [ [], nil ] + + validation_test 'cannot_be: [ "empty", "nil" ]', + [ 1, [1,2], { a: 10 } ], + [ [], nil ] + + validation_test 'cannot_be: [ :nil, :empty ]', + [ 1, [1,2], { a: 10 } ], + [ [], nil ] + + validation_test 'cannot_be: [ :empty, :nil, :blahblah ]', + [ 1, [1,2], { a: 10 } ], + [ [], nil ] + end + + # TODO required +end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index fefe78fbda..10a45f13c4 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 "identity attribute is name by default" do + expect(Chef::Resource.identity_attr).to eq :name end it "sets an identity attribute" do |