diff options
-rw-r--r-- | lib/chef/mixin/params_validate.rb | 21 | ||||
-rw-r--r-- | lib/chef/resource.rb | 58 | ||||
-rw-r--r-- | lib/chef/resource/lwrp_base.rb | 8 | ||||
-rw-r--r-- | spec/unit/property/state_spec.rb | 498 | ||||
-rw-r--r-- | spec/unit/property/validation_spec.rb | 460 | ||||
-rw-r--r-- | spec/unit/property_spec.rb | 794 |
6 files changed, 1823 insertions, 16 deletions
diff --git a/lib/chef/mixin/params_validate.rb b/lib/chef/mixin/params_validate.rb index 78d72dc801..ccc313db32 100644 --- a/lib/chef/mixin/params_validate.rb +++ b/lib/chef/mixin/params_validate.rb @@ -136,7 +136,8 @@ class Chef value = _pv_opts_lookup(opts, key) unless value.nil? passes = false - Array(to_be).each do |tb| + to_be = Array(to_be) + to_be.each do |tb| passes = true if value == tb end unless passes @@ -150,7 +151,8 @@ class Chef value = _pv_opts_lookup(opts, key) unless value.nil? passes = false - Array(to_be).each do |tb| + to_be = Array(to_be) + to_be.each do |tb| passes = true if value.kind_of?(tb) end unless passes @@ -177,14 +179,16 @@ class Chef # # Note, this will *PASS* if the object doesn't respond to the method. # So, to make sure a value is not nil and not blank, you need to do - # both :cannot_be => :blank *and* :cannot_be => :nil (or :required => true) + # both :cannot_be => [ :blank, :nil ] def _pv_cannot_be(opts, key, predicate_method_base_name) value = _pv_opts_lookup(opts, key) - predicate_method = (predicate_method_base_name.to_s + "?").to_sym + Array(predicate_method_base_name).each do |method_name| + predicate_method = :"#{method_name}?" - if value.respond_to?(predicate_method) - if value.send(predicate_method) - raise Exceptions::ValidationFailed, "Option #{key} cannot be #{predicate_method_base_name}" + if value.respond_to?(predicate_method) + if value.send(predicate_method) + raise Exceptions::ValidationFailed, "Option #{key} cannot be #{predicate_method_base_name}" + end end end end @@ -202,7 +206,7 @@ class Chef value = _pv_opts_lookup(opts, key) if value != nil passes = false - [ regex ].flatten.each do |r| + Array(regex).each do |r| if value != nil if r.match(value.to_s) passes = true @@ -239,4 +243,3 @@ class Chef end end end - diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index bae7608f5b..40a6e911a6 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -712,6 +712,64 @@ class Chef provider(arg) 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 options [Hash<Symbol,Object>] Validation options. + # @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). + # + # @return [Chef::Resource::PropertyType] The property type. + # + # @example With nothing + # property :x + # + # @example With options + # property :x, default: 'hi' + # + def self.property(name, **options) + define_method(name) do |arg=nil| + set_or_return(name.to_sym, arg, options) + end + 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 diff --git a/lib/chef/resource/lwrp_base.rb b/lib/chef/resource/lwrp_base.rb index ef3c2b5bba..443e0ed819 100644 --- a/lib/chef/resource/lwrp_base.rb +++ b/lib/chef/resource/lwrp_base.rb @@ -74,13 +74,7 @@ class Chef resource_class end - # 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 # Adds +action_names+ to the list of valid actions for this resource. # Does not include superclass's action list when appending. diff --git a/spec/unit/property/state_spec.rb b/spec/unit/property/state_spec.rb new file mode 100644 index 0000000000..5a7d19af11 --- /dev/null +++ b/spec/unit/property/state_spec.rb @@ -0,0 +1,498 @@ +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 + with_property ":y, kind_of: 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 + # subresource.x 'foo' + # subresource.y 'bar' + # expect(subresource.state).to eq({ x: 'foo', y: 'bar' }) + # expect(subresource_class.state_attrs).to eq [ :x, :y ] + # end + it "y is 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/property/validation_spec.rb b/spec/unit/property/validation_spec.rb new file mode 100644 index 0000000000..a88ebfd0cf --- /dev/null +++ b/spec/unit/property/validation_spec.rb @@ -0,0 +1,460 @@ +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 'kind_of: 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 + + context "required" do + with_property ':x, required: true' do + it "if x is not specified, retrieval fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + it "value nil is invalid" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + expect { resource.x nil }.to raise_error Chef::Exceptions::ValidationFailed + end + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + end + + # with_property ':x, [String, nil], required: true' do + with_property ':x, kind_of: [String, NilClass], required: true' do + it "if x is not specified, retrieval fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + # it "value nil is valid" do + # expect(resource.x nil).to be_nil + # expect(resource.x).to be_nil + # end + it "value '1' is valid" do + expect(resource.x '1').to eq '1' + expect(resource.x).to eq '1' + end + it "value 1 is invalid" do + expect { resource.x 1 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + # with_property ':x, name_property: true, required: true' do + with_property ':x, required: true, name_attribute: true' do + it "if x is not specified, retrieval fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + # it "value nil is invalid" do + # expect { resource.x nil }.to raise_error Chef::Exceptions::ValidationFailed + # end + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + end + + # with_property ':x, default: 10, required: true' do + with_property ':x, required: true, default: 10' do + it "if x is not specified, retrieval fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + # it "value nil is invalid" do + # expect { resource.x nil }.to raise_error Chef::Exceptions::ValidationFailed + # end + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + end + end + + context "custom validators (def _pv_blarghle)" do + before do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end + + with_property ':x, blarghle: 1' do + context "and a class that implements _pv_blarghle" do + before do + resource_class.class_eval do + def _pv_blarghle(opts, key, value) + if _pv_opts_lookup(opts, key) != value + raise Chef::Exceptions::ValidationFailed, "ouch" + end + end + end + end + + # it "getting the value causes a deprecation warning" do + # Chef::Config[:treat_deprecation_warnings_as_errors] = true + # expect { resource.x }.to raise_error Chef::Exceptions::DeprecatedFeatureError + # end + + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + + it "value '1' is invalid" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + expect { resource.x '1' }.to raise_error Chef::Exceptions::ValidationFailed + end + + # it "value nil is invalid" do + # Chef::Config[:treat_deprecation_warnings_as_errors] = false + # expect { resource.x nil }.to raise_error Chef::Exceptions::ValidationFailed + # end + end + end + + with_property ':x, blarghle: 1' do + context "and a class that implements _pv_blarghle" do + before do + resource_class.class_eval do + def _pv_blarghle(opts, key, value) + if _pv_opts_lookup(opts, key) != value + raise Chef::Exceptions::ValidationFailed, "ouch" + end + end + end + end + + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + + it "value '1' is invalid" do + expect { resource.x '1' }.to raise_error Chef::Exceptions::ValidationFailed + end + + # it "value nil is invalid" do + # expect { resource.x nil }.to raise_error Chef::Exceptions::ValidationFailed + # end + end + end + end +end diff --git a/spec/unit/property_spec.rb b/spec/unit/property_spec.rb new file mode 100644 index 0000000000..b9c0d27da8 --- /dev/null +++ b/spec/unit/property_spec.rb @@ -0,0 +1,794 @@ +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" + with_property ':x, kind_of: 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 + context "with property :x, kind_of: String on the subclass" do + before do + subresource_class.class_eval do + # property :x, String + property :x, kind_of: 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 + with_property ':x, default: Chef::DelayedEvaluator.new { {} }' do + it "when x is not set, it returns {}" do + expect(resource.x).to eq({}) + end + # it "The value is different each time it is called" do + # value = resource.x + # expect(value).to eq({}) + # expect(resource.x.object_id).not_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 + "#{name}#{next_index}" + end + end + end + + # with_property ':x, default: lazy { blah }' do + with_property ':x, default: Chef::DelayedEvaluator.new { blah }' do + # it "x is run in context of the instance" do + # expect(resource.x).to eq "blah1" + # end + # it "x is run in the context of each instance it is run in" do + # expect(resource.x).to eq "blah1" + # expect(resource_class.new('another').x).to eq "another2" + # expect(resource.x).to eq "blah3" + # end + it "x is run outside the instance" do + expect(resource.x).to eq "class" + end + end + + # with_property ':x, default: lazy { |x| "#{blah}#{x.blah}" }' do + with_property ':x, default: Chef::DelayedEvaluator.new { |x| "#{blah}#{x.blah}" }' do + it "x is run in context of the class (where it was defined) and passed the instance" do + expect(resource.x).to eq "classblah1" + end + it "x is passed the value of each instance it is run in" do + expect(resource.x).to eq "classblah1" + expect(resource_class.new('another').x).to eq "classanother2" + # expect(resource.x).to eq "classblah3" + end + end + end + + context "validation of defaults" do + #with_property ':x, String, default: 10' do + with_property ':x, default: 10, kind_of: String' 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 + with_property ":x, default: Chef::DelayedEvaluator.new { Namer.next_index }, kind_of: String" 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 each time" do + # expect(resource.x).to eq 1 + # expect(Namer.current_index).to eq 2 + # expect(resource.x).to eq 3 + # expect(Namer.current_index).to eq 4 + # 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 each time" do + # expect(resource.x).to eq '101' + # expect(Namer.current_index).to eq 2 + # expect(resource.x).to eq '103' + # expect(Namer.current_index).to eq 4 + # 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 on each access" do + # expect(resource.x).to eq '101' + # expect(Namer.current_index).to eq 2 + # expect(resource.x).to eq '103' + # expect(Namer.current_index).to eq 4 + # 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(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 evaluates it twice" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq 1 + expect(resource.x).to eq 2 + expect(Namer.current_index).to eq 2 + end + it "setting the same lazy value on two different instances runs it on each instancee" 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 3 + 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 + "#{name}#{Namer.next_index}" + end + end + end + def blah + "example" + end + it "retrieving lazy { blah } gets the caller's blah" do + resource.x lazy { blah } + expect(resource.x).to eq "example" + end + # it "retrieving lazy { blah } gets the instance variable" do + # resource.x lazy { blah } + # expect(resource.x).to eq "blah1" + # end + # it "retrieving lazy { blah } from two different instances gets two different instance variables" do + # resource2 = resource_class.new("another") + # l = lazy { blah } + # resource2.x l + # resource.x l + # expect(resource2.x).to eq "another1" + # expect(resource.x).to eq "blah2" + # expect(resource2.x).to eq "another3" + # end + # it 'retrieving lazy { |x| "#{blah}#{x.blah}" } gets the example and instance variables' do + # resource.x lazy { |x| "#{blah}#{x.blah}" } + # expect(resource.x).to eq "exampleblah1" + # end + # it 'retrieving lazy { |x| "#{blah}#{x.blah}" } from two different instances gets two different instance variables' do + # resource2 = resource_class.new("another") + # l = lazy { |x| "#{blah}#{x.blah}" } + # resource2.x l + # resource.x l + # expect(resource2.x).to eq "exampleanother1" + # expect(resource.x).to eq "exampleblah2" + # expect(resource2.x).to eq "exampleanother3" + # 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 on each access" 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 "34" + # expect(Namer.current_index).to eq 4 + # end + # end + + # with_property ':x, String' do + with_property ':x, kind_of: 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 + with_property ':x, callbacks: { "a" => proc { |v| Namer.next_index; true } }' do + it "lazy values are validated on each access" 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 3 + expect(Namer.current_index).to eq 4 + 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 "45" + # expect(Namer.current_index).to eq 6 + # 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| 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 + with_property ':x, callbacks: { "a" => 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 + + # [ 'name_attribute', 'name_property' ].each do |name| + name = 'name_attribute' + context "Chef::Resource::PropertyType##{name}" do + with_property ":x, #{name}: 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 "caches resource.name" do + expect(resource.x).to eq 'blah' + resource.name 'foo' + expect(resource.x).to eq 'blah' + end + end + with_property ":x, default: 10, #{name}: true" do + it "chooses default over #{name}" do + expect(resource.x).to eq 10 + end + end + # with_property ":x, #{name}: true, default: 10" do + # it "chooses default over #{name}" do + # # expect(resource.x).to eq 10 + # expect(resource.x).to eq 10 + # end + # end + end + # end +end |