summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Keiser <john@johnkeiser.com>2015-10-20 17:15:05 -0700
committerJohn Keiser <john@johnkeiser.com>2015-10-20 17:15:05 -0700
commita16d253bec53b8209a092d990c58bcb3e10bd3bd (patch)
treec0bfafacdb7ccabf85a635209b43aeb22db23bb5
parent4d35de670c9d54ad3b672e668672ba3256e2aef8 (diff)
parent4d11c33afc5821d4b19bba4f8431c941c6c73d39 (diff)
downloadchef-a16d253bec53b8209a092d990c58bcb3e10bd3bd.tar.gz
Merge branch 'jk/property_mixin'
-rw-r--r--lib/chef/mixin/properties.rb302
-rw-r--r--lib/chef/resource.rb339
-rw-r--r--spec/unit/mixin/properties_spec.rb97
3 files changed, 428 insertions, 310 deletions
diff --git a/lib/chef/mixin/properties.rb b/lib/chef/mixin/properties.rb
new file mode 100644
index 0000000000..85abe4427e
--- /dev/null
+++ b/lib/chef/mixin/properties.rb
@@ -0,0 +1,302 @@
+require 'chef/delayed_evaluator'
+require 'chef/mixin/params_validate'
+require 'chef/property'
+
+class Chef
+ module Mixin
+ module Properties
+ module ClassMethods
+ #
+ # The list of properties defined on this resource.
+ #
+ # Everything defined with `property` is in this list.
+ #
+ # @param include_superclass [Boolean] `true` to include properties defined
+ # on superclasses; `false` or `nil` to return the list of properties
+ # directly on this class.
+ #
+ # @return [Hash<Symbol,Property>] The list of property names and types.
+ #
+ def properties(include_superclass=true)
+ if include_superclass
+ result = {}
+ ancestors.reverse_each { |c| result.merge!(c.properties(false)) if c.respond_to?(:properties) }
+ result
+ else
+ @properties ||= {}
+ end
+ 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`.
+ #
+ # @example Bare property
+ # 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 property(name, type=NOT_PASSED, **options)
+ name = name.to_sym
+
+ options.each { |k,v| options[k.to_sym] = v if k.is_a?(String) }
+
+ options[:instance_variable_name] = :"@#{name}" if !options.has_key?(:instance_variable_name)
+ 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
+
+ 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 property_type(**options)
+ Property.derive(**options)
+ end
+
+ #
+ # Create a lazy value for assignment to a default value.
+ #
+ # @param block The block to run when the value is retrieved.
+ #
+ # @return [Chef::DelayedEvaluator] The lazy value
+ #
+ def lazy(&block)
+ DelayedEvaluator.new(&block)
+ end
+
+ #
+ # 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.
+ #
+ # 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 state_properties(*names)
+ if !names.empty?
+ names = names.map { |name| name.to_sym }.uniq
+
+ 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 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 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 identity_properties *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
+
+ def included(other)
+ other.extend ClassMethods
+ end
+ end
+
+ def self.included(other)
+ other.extend ClassMethods
+ end
+
+ include Chef::Mixin::ParamsValidate
+
+ #
+ # 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)
+ 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
+ end
+ end
+end
diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb
index 9e40fdccfd..90453bd00e 100644
--- a/lib/chef/resource.rb
+++ b/lib/chef/resource.rb
@@ -19,7 +19,6 @@
#
require 'chef/exceptions'
-require 'chef/mixin/params_validate'
require 'chef/dsl/platform_introspection'
require 'chef/dsl/data_query'
require 'chef/dsl/registry_helper'
@@ -40,6 +39,7 @@ require 'chef/resource_resolver'
require 'set'
require 'chef/mixin/deprecation'
+require 'chef/mixin/properties'
require 'chef/mixin/provides'
require 'chef/mixin/shell_out'
require 'chef/mixin/powershell_out'
@@ -61,6 +61,34 @@ class Chef
include Chef::Mixin::ShellOut
include Chef::Mixin::PowershellOut
+ # Bring in `property` and `property_type`
+ include Chef::Mixin::Properties
+
+ #
+ # 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
+
#
# The node the current Chef run is using.
#
@@ -133,30 +161,6 @@ class Chef
end
#
- # The list of properties defined on this resource.
- #
- # Everything defined with `property` is in this list.
- #
- # @param include_superclass [Boolean] `true` to include properties defined
- # on superclasses; `false` or `nil` to return the list of properties
- # directly on this class.
- #
- # @return [Hash<Symbol,Property>] The list of property names and types.
- #
- def self.properties(include_superclass=true)
- @properties ||= {}
- if include_superclass
- if superclass.respond_to?(:properties)
- superclass.properties.merge(@properties)
- else
- @properties.dup
- end
- else
- @properties
- end
- end
-
- #
# The action or actions that will be taken when this resource is run.
#
# @param arg [Array[Symbol], Symbol] A list of actions (e.g. `:create`)
@@ -681,7 +685,6 @@ class Chef
# Resource Definition Interface (for resource developers)
#
- include Chef::Mixin::ParamsValidate
include Chef::Mixin::Deprecation
#
@@ -715,240 +718,6 @@ class Chef
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`.
- #
- # @example Bare property
- # 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=NOT_PASSED, **options)
- name = name.to_sym
-
- options.each { |k,v| options[k.to_sym] = v if k.is_a?(String) }
-
- options[:instance_variable_name] = :"@#{name}" if !options.has_key?(:instance_variable_name)
- 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
-
- 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)
- 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
-
- #
- # Create a lazy value for assignment to a default value.
- #
- # @param block The block to run when the value is retrieved.
- #
- # @return [Chef::DelayedEvaluator] The lazy value
- #
- def self.lazy(&block)
- DelayedEvaluator.new(&block)
- end
-
- #
- # 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.
- #
- # 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
-
- 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 list of "state properties" implemented by the Resource
# subclass.
#
@@ -973,56 +742,6 @@ class Chef
end
#
- # 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 identity_properties *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
diff --git a/spec/unit/mixin/properties_spec.rb b/spec/unit/mixin/properties_spec.rb
new file mode 100644
index 0000000000..18178619e4
--- /dev/null
+++ b/spec/unit/mixin/properties_spec.rb
@@ -0,0 +1,97 @@
+require 'support/shared/integration/integration_helper'
+require 'chef/mixin/properties'
+
+module ChefMixinPropertiesSpec
+ describe "Chef::Resource.property" do
+ include IntegrationSupport
+
+ context "with a base class A with properties a, ab, and ac" do
+ class A
+ include Chef::Mixin::Properties
+ property :a, 'a', default: 'a'
+ property :ab, ['a', 'b'], default: 'a'
+ property :ac, ['a', 'c'], default: 'a'
+ end
+
+ context "and a module B with properties b, ab and bc" do
+ module B
+ include Chef::Mixin::Properties
+ property :b, 'b', default: 'b'
+ property :ab, default: 'b'
+ property :bc, ['b', 'c'], default: 'c'
+ end
+
+ context "and a derived class C < A with properties c, ac and bc" do
+ class C < A
+ include B
+ property :c, 'c', default: 'c'
+ property :ac, default: 'c'
+ property :bc, default: 'c'
+ end
+
+ it "A.properties has a, ab, and ac with types 'a', ['a', 'b'], and ['b', 'c']" do
+ expect(A.properties.keys).to eq [ :a, :ab, :ac ]
+ expect(A.properties[:a].validation_options[:is]).to eq 'a'
+ expect(A.properties[:ab].validation_options[:is]).to eq [ 'a', 'b' ]
+ expect(A.properties[:ac].validation_options[:is]).to eq [ 'a', 'c' ]
+ end
+ it "B.properties has b, ab, and bc with types 'b', nil and ['b', 'c']" do
+ expect(B.properties.keys).to eq [ :b, :ab, :bc ]
+ expect(B.properties[:b].validation_options[:is]).to eq 'b'
+ expect(B.properties[:ab].validation_options[:is]).to be_nil
+ expect(B.properties[:bc].validation_options[:is]).to eq [ 'b', 'c' ]
+ end
+ it "C.properties has a, b, c, ac and bc with merged types" do
+ expect(C.properties.keys).to eq [ :a, :ab, :ac, :b, :bc, :c ]
+ expect(C.properties[:a].validation_options[:is]).to eq 'a'
+ expect(C.properties[:b].validation_options[:is]).to eq 'b'
+ expect(C.properties[:c].validation_options[:is]).to eq 'c'
+ expect(C.properties[:ac].validation_options[:is]).to eq [ 'a', 'c' ]
+ expect(C.properties[:bc].validation_options[:is]).to eq [ 'b', 'c' ]
+ end
+ it "C.properties has ab with a non-merged type (from B)" do
+ expect(C.properties[:ab].validation_options[:is]).to be_nil
+ end
+
+ context "and an instance of C" do
+ let(:c) { C.new }
+
+ it "all properties can be retrieved and merged properties default to ab->b, ac->c, bc->c" do
+ expect(c.a).to eq('a')
+ expect(c.b).to eq('b')
+ expect(c.c).to eq('c')
+ expect(c.ab).to eq('b')
+ expect(c.ac).to eq('c')
+ expect(c.bc).to eq('c')
+ end
+ end
+ end
+ end
+ end
+ end
+
+ context "with an Inner module" do
+ module Inner
+ include Chef::Mixin::Properties
+ property :inner
+ end
+
+ context "and an Outer module including it" do
+ module Outer
+ include Inner
+ property :outer
+ end
+
+ context "and an Outerest class including that" do
+ class Outerest
+ include Outer
+ property :outerest
+ end
+
+ it "Outerest.properties.validation_options[:is] inner, outer, outerest" do
+ expect(Outerest.properties.keys).to eq [:inner, :outer, :outerest]
+ end
+ end
+ end
+ end
+end