diff --git a/lib/puppet/property.rb b/lib/puppet/property.rb
index 9d2d98b0e..f4914fad9 100644
--- a/lib/puppet/property.rb
+++ b/lib/puppet/property.rb
@@ -1,357 +1,357 @@
# The virtual base class for properties, which are the self-contained building
# blocks for actually doing work on the system.
require 'puppet'
require 'puppet/parameter'
class Puppet::Property < Puppet::Parameter
require 'puppet/property/ensure'
# Because 'should' uses an array, we have a special method for handling
# it. We also want to keep copies of the original values, so that
# they can be retrieved and compared later when merging.
attr_reader :shouldorig
attr_writer :noop
class << self
attr_accessor :unmanaged
attr_reader :name
# Return array matching info, defaulting to just matching
# the first value.
def array_matching
@array_matching ||= :first
end
# Set whether properties should match all values or just the first one.
def array_matching=(value)
value = value.intern if value.is_a?(String)
raise ArgumentError, "Supported values for Property#array_matching are 'first' and 'all'" unless [:first, :all].include?(value)
@array_matching = value
end
end
# Look up a value's name, so we can find options and such.
def self.value_name(name)
if value = value_collection.match?(name)
value.name
end
end
# Retrieve an option set when a value was defined.
def self.value_option(name, option)
if value = value_collection.value(name)
value.send(option)
end
end
# Define a new valid value for a property. You must provide the value itself,
# usually as a symbol, or a regex to match the value.
#
# The first argument to the method is either the value itself or a regex.
# The second argument is an option hash; valid options are:
# * :method: The name of the method to define. Defaults to 'set_'.
# * :required_features: A list of features this value requires.
# * :event: The event that should be returned when this value is set.
# * :call: When to call any associated block. The default value
# is `instead`, which means to call the value instead of calling the
# provider. You can also specify `before` or `after`, which will
# call both the block and the provider, according to the order you specify
# (the `first` refers to when the block is called, not the provider).
def self.newvalue(name, options = {}, &block)
value = value_collection.newvalue(name, options, &block)
define_method(value.method, &value.block) if value.method and value.block
value
end
# Call the provider method.
def call_provider(value)
provider.send(self.class.name.to_s + "=", value)
rescue NoMethodError
self.fail "The #{provider.class.name} provider can not handle attribute #{self.class.name}"
end
# Call the dynamically-created method associated with our value, if
# there is one.
def call_valuemethod(name, value)
if method = self.class.value_option(name, :method) and self.respond_to?(method)
begin
event = self.send(method)
rescue Puppet::Error
raise
rescue => detail
puts detail.backtrace if Puppet[:trace]
error = Puppet::Error.new("Could not set '#{value} on #{self.class.name}: #{detail}", @resource.line, @resource.file)
error.set_backtrace detail.backtrace
raise error
end
elsif block = self.class.value_option(name, :block)
# FIXME It'd be better here to define a method, so that
# the blocks could return values.
self.instance_eval(&block)
else
devfail "Could not find method for value '#{name}'"
end
end
# How should a property change be printed as a string?
def change_to_s(current_value, newvalue)
begin
if current_value == :absent
return "defined '#{name}' as #{self.class.format_value_for_display should_to_s(newvalue)}"
elsif newvalue == :absent or newvalue == [:absent]
return "undefined '#{name}' from #{self.class.format_value_for_display is_to_s(current_value)}"
else
return "#{name} changed #{self.class.format_value_for_display is_to_s(current_value)} to #{self.class.format_value_for_display should_to_s(newvalue)}"
end
rescue Puppet::Error, Puppet::DevError
raise
rescue => detail
puts detail.backtrace if Puppet[:trace]
raise Puppet::DevError, "Could not convert change '#{name}' to string: #{detail}"
end
end
# Figure out which event to return.
def event_name
value = self.should
event_name = self.class.value_option(value, :event) and return event_name
name == :ensure or return (name.to_s + "_changed").to_sym
return (resource.type.to_s + case value
when :present; "_created"
when :absent; "_removed"
else
"_changed"
end).to_sym
end
# Return a modified form of the resource event.
def event
resource.event :name => event_name, :desired_value => should, :property => self, :source_description => path
end
attr_reader :shadow
# initialize our property
def initialize(hash = {})
super
if ! self.metaparam? and klass = Puppet::Type.metaparamclass(self.class.name)
setup_shadow(klass)
end
end
# Determine whether the property is in-sync or not. If @should is
# not defined or is set to a non-true value, then we do not have
# a valid value for it and thus consider the property to be in-sync
# since we cannot fix it. Otherwise, we expect our should value
# to be an array, and if @is matches any of those values, then
# we consider it to be in-sync.
#
# Don't override this method.
def safe_insync?(is)
# If there is no @should value, consider the property to be in sync.
return true unless @should
# Otherwise delegate to the (possibly derived) insync? method.
insync?(is)
end
def self.method_added(sym)
raise "Puppet::Property#safe_insync? shouldn't be overridden; please override insync? instead" if sym == :safe_insync?
end
# This method may be overridden by derived classes if necessary
# to provide extra logic to determine whether the property is in
# sync. In most cases, however, only `property_matches?` needs to be
# overridden to give the correct outcome - without reproducing all the array
# matching logic, etc, found here.
def insync?(is)
self.devfail "#{self.class.name}'s should is not array" unless @should.is_a?(Array)
# an empty array is analogous to no should values
return true if @should.empty?
# Look for a matching value, either for all the @should values, or any of
# them, depending on the configuration of this property.
if match_all? then
- old = (is == @should or is == @should.collect { |v| v.to_s })
- new = Array(is).zip(@should).all? {|is, want| property_matches?(is, want) }
+ # Emulate Array#== using our own comparison function.
+ # A non-array was not equal to an array, which @should always is.
+ return false unless is.is_a? Array
- puts "old and new mismatch!" unless old == new
- fail "old and new mismatch!" unless old == new
+ # If they were different lengths, they are not equal.
+ return false unless is.length == @should.length
- # We need to pairwise compare the entries; this preserves the old
- # behaviour while using the new pair comparison code.
- return Array(is).zip(@should).all? {|is, want| property_matches?(is, want) }
+ # Finally, are all the elements equal?
+ return is.zip(@should).all? {|a, b| property_matches?(a, b) }
else
return @should.any? {|want| property_matches?(is, want) }
end
end
# Compare the current and desired value of a property in a property-specific
# way. Invoked by `insync?`; this should be overridden if your property
# has a different comparison type but does not actually differentiate the
# overall insync? logic.
def property_matches?(current, desired)
# This preserves the older Puppet behaviour of doing raw and string
# equality comparisons for all equality. I am not clear this is globally
# desirable, but at least it is not a breaking change. --daniel 2011-11-11
current == desired or current == desired.to_s
end
# because the @should and @is vars might be in weird formats,
# we need to set up a mechanism for pretty printing of the values
# default to just the values, but this way individual properties can
# override these methods
def is_to_s(currentvalue)
currentvalue
end
# Send a log message.
def log(msg)
Puppet::Util::Log.create(
:level => resource[:loglevel],
:message => msg,
:source => self
)
end
# Should we match all values, or just the first?
def match_all?
self.class.array_matching == :all
end
# Execute our shadow's munge code, too, if we have one.
def munge(value)
self.shadow.munge(value) if self.shadow
super
end
# each property class must define the name method, and property instances
# do not change that name
# this implicitly means that a given object can only have one property
# instance of a given property class
def name
self.class.name
end
# for testing whether we should actually do anything
def noop
# This is only here to make testing easier.
if @resource.respond_to?(:noop?)
@resource.noop?
else
if defined?(@noop)
@noop
else
Puppet[:noop]
end
end
end
# By default, call the method associated with the property name on our
# provider. In other words, if the property name is 'gid', we'll call
# 'provider.gid' to retrieve the current value.
def retrieve
provider.send(self.class.name)
end
# Set our value, using the provider, an associated block, or both.
def set(value)
# Set a name for looking up associated options like the event.
name = self.class.value_name(value)
call = self.class.value_option(name, :call) || :none
if call == :instead
call_valuemethod(name, value)
elsif call == :none
# They haven't provided a block, and our parent does not have
# a provider, so we have no idea how to handle this.
self.fail "#{self.class.name} cannot handle values of type #{value.inspect}" unless @resource.provider
call_provider(value)
else
# LAK:NOTE 20081031 This is a change in behaviour -- you could
# previously specify :call => [;before|:after], which would call
# the setter *in addition to* the block. I'm convinced this
# was never used, and it makes things unecessarily complicated.
# If you want to specify a block and still call the setter, then
# do so in the block.
devfail "Cannot use obsolete :call value '#{call}' for property '#{self.class.name}'"
end
end
# If there's a shadowing metaparam, instantiate it now.
# This allows us to create a property or parameter with the
# same name as a metaparameter, and the metaparam will only be
# stored as a shadow.
def setup_shadow(klass)
@shadow = klass.new(:resource => self.resource)
end
# Only return the first value
def should
return nil unless defined?(@should)
self.devfail "should for #{self.class.name} on #{resource.name} is not an array" unless @should.is_a?(Array)
if match_all?
return @should.collect { |val| self.unmunge(val) }
else
return self.unmunge(@should[0])
end
end
# Set the should value.
def should=(values)
values = [values] unless values.is_a?(Array)
@shouldorig = values
values.each { |val| validate(val) }
@should = values.collect { |val| self.munge(val) }
end
def should_to_s(newvalue)
[newvalue].flatten.join(" ")
end
def sync
devfail "Got a nil value for should" unless should
set(should)
end
# Verify that the passed value is valid.
# If the developer uses a 'validate' hook, this method will get overridden.
def unsafe_validate(value)
super
validate_features_per_value(value)
end
# Make sure that we've got all of the required features for a given value.
def validate_features_per_value(value)
if features = self.class.value_option(self.class.value_name(value), :required_features)
features = Array(features)
needed_features = features.collect { |f| f.to_s }.join(", ")
raise ArgumentError, "Provider must have features '#{needed_features}' to set '#{self.class.name}' to '#{value}'" unless provider.satisfies?(features)
end
end
# Just return any should value we might have.
def value
self.should
end
# Match the Parameter interface, but we really just use 'should' internally.
# Note that the should= method does all of the validation and such.
def value=(value)
self.should = value
end
end
diff --git a/spec/unit/property_spec.rb b/spec/unit/property_spec.rb
index 59de829e4..3f25b743b 100755
--- a/spec/unit/property_spec.rb
+++ b/spec/unit/property_spec.rb
@@ -1,494 +1,510 @@
#!/usr/bin/env rspec
require 'spec_helper'
require 'puppet/property'
describe Puppet::Property do
let :resource do Puppet::Type.type(:host).new :name => "foo" end
let :subclass do
# We need a completely fresh subclass every time, because we modify both
# class and instance level things inside the tests.
subclass = Class.new(Puppet::Property) do @name = :foo end
subclass.initvars
subclass
end
let :property do subclass.new :resource => resource end
it "should be able to look up the modified name for a given value" do
subclass.newvalue(:foo)
subclass.value_name("foo").should == :foo
end
it "should be able to look up the modified name for a given value matching a regex" do
subclass.newvalue(%r{.})
subclass.value_name("foo").should == %r{.}
end
it "should be able to look up a given value option" do
subclass.newvalue(:foo, :event => :whatever)
subclass.value_option(:foo, :event).should == :whatever
end
it "should be able to specify required features" do
subclass.should respond_to(:required_features=)
end
{"one" => [:one],:one => [:one],%w{a} => [:a],[:b] => [:b],%w{one two} => [:one,:two],[:a,:b] => [:a,:b]}.each { |in_value,out_value|
it "should always convert required features into an array of symbols (e.g. #{in_value.inspect} --> #{out_value.inspect})" do
subclass.required_features = in_value
subclass.required_features.should == out_value
end
}
it "should return its name as a string when converted to a string" do
property.to_s.should == property.name.to_s
end
it "should be able to shadow metaparameters" do
property.must respond_to(:shadow)
end
describe "when returning the default event name" do
it "should use the current 'should' value to pick the event name" do
property.expects(:should).returns "myvalue"
subclass.expects(:value_option).with('myvalue', :event).returns :event_name
property.event_name
end
it "should return any event defined with the specified value" do
property.expects(:should).returns :myval
subclass.expects(:value_option).with(:myval, :event).returns :event_name
property.event_name.should == :event_name
end
describe "and the property is 'ensure'" do
before :each do
property.stubs(:name).returns :ensure
resource.expects(:type).returns :mytype
end
it "should use _created if the 'should' value is 'present'" do
property.expects(:should).returns :present
property.event_name.should == :mytype_created
end
it "should use _removed if the 'should' value is 'absent'" do
property.expects(:should).returns :absent
property.event_name.should == :mytype_removed
end
it "should use _changed if the 'should' value is not 'absent' or 'present'" do
property.expects(:should).returns :foo
property.event_name.should == :mytype_changed
end
it "should use _changed if the 'should value is nil" do
property.expects(:should).returns nil
property.event_name.should == :mytype_changed
end
end
it "should use _changed if the property is not 'ensure'" do
property.stubs(:name).returns :myparam
property.expects(:should).returns :foo
property.event_name.should == :myparam_changed
end
it "should use _changed if no 'should' value is set" do
property.stubs(:name).returns :myparam
property.expects(:should).returns nil
property.event_name.should == :myparam_changed
end
end
describe "when creating an event" do
before :each do
property.stubs(:should).returns "myval"
end
it "should use an event from the resource as the base event" do
event = Puppet::Transaction::Event.new
resource.expects(:event).returns event
property.event.should equal(event)
end
it "should have the default event name" do
property.expects(:event_name).returns :my_event
property.event.name.should == :my_event
end
it "should have the property's name" do
property.event.property.should == property.name.to_s
end
it "should have the 'should' value set" do
property.stubs(:should).returns "foo"
property.event.desired_value.should == "foo"
end
it "should provide its path as the source description" do
property.stubs(:path).returns "/my/param"
property.event.source_description.should == "/my/param"
end
end
describe "when shadowing metaparameters" do
let :shadow_class do
shadow_class = Class.new(Puppet::Property) do
@name = :alias
end
shadow_class.initvars
shadow_class
end
it "should create an instance of the metaparameter at initialization" do
Puppet::Type.metaparamclass(:alias).expects(:new).with(:resource => resource)
shadow_class.new :resource => resource
end
it "should munge values using the shadow's munge method" do
shadow = mock 'shadow'
Puppet::Type.metaparamclass(:alias).expects(:new).returns shadow
shadow.expects(:munge).with "foo"
property = shadow_class.new :resource => resource
property.munge("foo")
end
end
describe "when defining new values" do
it "should define a method for each value created with a block that's not a regex" do
subclass.newvalue(:foo) { }
property.must respond_to(:set_foo)
end
end
describe "when assigning the value" do
it "should just set the 'should' value" do
property.value = "foo"
property.should.must == "foo"
end
it "should validate each value separately" do
property.expects(:validate).with("one")
property.expects(:validate).with("two")
property.value = %w{one two}
end
it "should munge each value separately and use any result as the actual value" do
property.expects(:munge).with("one").returns :one
property.expects(:munge).with("two").returns :two
# Do this so we get the whole array back.
subclass.array_matching = :all
property.value = %w{one two}
property.should.must == [:one, :two]
end
it "should return any set value" do
(property.value = :one).should == :one
end
end
describe "when returning the value" do
it "should return nil if no value is set" do
property.should.must be_nil
end
it "should return the first set 'should' value if :array_matching is set to :first" do
subclass.array_matching = :first
property.should = %w{one two}
property.should.must == "one"
end
it "should return all set 'should' values as an array if :array_matching is set to :all" do
subclass.array_matching = :all
property.should = %w{one two}
property.should.must == %w{one two}
end
it "should default to :first array_matching" do
subclass.array_matching.should == :first
end
it "should unmunge the returned value if :array_matching is set to :first" do
property.class.unmunge do |v| v.to_sym end
subclass.array_matching = :first
property.should = %w{one two}
property.should.must == :one
end
it "should unmunge all the returned values if :array_matching is set to :all" do
property.class.unmunge do |v| v.to_sym end
subclass.array_matching = :all
property.should = %w{one two}
property.should.must == [:one, :two]
end
end
describe "when validating values" do
it "should do nothing if no values or regexes have been defined" do
lambda { property.should = "foo" }.should_not raise_error
end
it "should fail if the value is not a defined value or alias and does not match a regex" do
subclass.newvalue(:foo)
lambda { property.should = "bar" }.should raise_error
end
it "should succeeed if the value is one of the defined values" do
subclass.newvalue(:foo)
lambda { property.should = :foo }.should_not raise_error
end
it "should succeeed if the value is one of the defined values even if the definition uses a symbol and the validation uses a string" do
subclass.newvalue(:foo)
lambda { property.should = "foo" }.should_not raise_error
end
it "should succeeed if the value is one of the defined values even if the definition uses a string and the validation uses a symbol" do
subclass.newvalue("foo")
lambda { property.should = :foo }.should_not raise_error
end
it "should succeed if the value is one of the defined aliases" do
subclass.newvalue("foo")
subclass.aliasvalue("bar", "foo")
lambda { property.should = :bar }.should_not raise_error
end
it "should succeed if the value matches one of the regexes" do
subclass.newvalue(/./)
lambda { property.should = "bar" }.should_not raise_error
end
it "should validate that all required features are present" do
subclass.newvalue(:foo, :required_features => [:a, :b])
resource.provider.expects(:satisfies?).with([:a, :b]).returns true
property.should = :foo
end
it "should fail if required features are missing" do
subclass.newvalue(:foo, :required_features => [:a, :b])
resource.provider.expects(:satisfies?).with([:a, :b]).returns false
lambda { property.should = :foo }.should raise_error(Puppet::Error)
end
it "should internally raise an ArgumentError if required features are missing" do
subclass.newvalue(:foo, :required_features => [:a, :b])
resource.provider.expects(:satisfies?).with([:a, :b]).returns false
lambda { property.validate_features_per_value :foo }.should raise_error(ArgumentError)
end
it "should validate that all required features are present for regexes" do
value = subclass.newvalue(/./, :required_features => [:a, :b])
resource.provider.expects(:satisfies?).with([:a, :b]).returns true
property.should = "foo"
end
it "should support specifying an individual required feature" do
value = subclass.newvalue(/./, :required_features => :a)
resource.provider.expects(:satisfies?).returns true
property.should = "foo"
end
end
describe "when munging values" do
it "should do nothing if no values or regexes have been defined" do
property.munge("foo").should == "foo"
end
it "should return return any matching defined values" do
subclass.newvalue(:foo)
property.munge("foo").should == :foo
end
it "should return any matching aliases" do
subclass.newvalue(:foo)
subclass.aliasvalue(:bar, :foo)
property.munge("bar").should == :foo
end
it "should return the value if it matches a regex" do
subclass.newvalue(/./)
property.munge("bar").should == "bar"
end
it "should return the value if no other option is matched" do
subclass.newvalue(:foo)
property.munge("bar").should == "bar"
end
end
describe "when syncing the 'should' value" do
it "should set the value" do
subclass.newvalue(:foo)
property.should = :foo
property.expects(:set).with(:foo)
property.sync
end
end
describe "when setting a value" do
it "should catch exceptions and raise Puppet::Error" do
subclass.newvalue(:foo) { raise "eh" }
lambda { property.set(:foo) }.should raise_error(Puppet::Error)
end
describe "that was defined without a block" do
it "should call the settor on the provider" do
subclass.newvalue(:bar)
resource.provider.expects(:foo=).with :bar
property.set(:bar)
end
end
describe "that was defined with a block" do
it "should call the method created for the value if the value is not a regex" do
subclass.newvalue(:bar) {}
property.expects(:set_bar)
property.set(:bar)
end
it "should call the provided block if the value is a regex" do
subclass.newvalue(/./) { self.test }
property.expects(:test)
property.set("foo")
end
end
end
describe "when producing a change log" do
it "should say 'defined' when the current value is 'absent'" do
property.change_to_s(:absent, "foo").should =~ /^defined/
end
it "should say 'undefined' when the new value is 'absent'" do
property.change_to_s("foo", :absent).should =~ /^undefined/
end
it "should say 'changed' when neither value is 'absent'" do
property.change_to_s("foo", "bar").should =~ /changed/
end
end
shared_examples_for "#insync?" do
# We share a lot of behaviour between the all and first matching, so we
# use a shared behaviour set to emulate that. The outside world makes
# sure the class, etc, point to the right content.
[[], [12], [12, 13]].each do |input|
it "should return true if should is empty with is => #{input.inspect}" do
property.should = []
property.must be_insync(input)
end
end
end
describe "#insync?" do
context "array_matching :all" do
# `@should` is an array of scalar values, and `is` is an array of scalar values.
before :each do
property.class.array_matching = :all
end
it_should_behave_like "#insync?"
context "if the should value is an array" do
before :each do property.should = [1, 2] end
it "should match if is exactly matches" do
property.must be_insync [1, 2]
end
it "should match if it matches, but all stringified" do
property.must be_insync ["1", "2"]
end
it "should not match if some-but-not-all values are stringified" do
property.must_not be_insync ["1", 2]
property.must_not be_insync [1, "2"]
end
it "should not match if order is different but content the same" do
property.must_not be_insync [2, 1]
end
it "should not match if there are more items in should than is" do
property.must_not be_insync [1]
end
it "should not match if there are less items in should than is" do
property.must_not be_insync [1, 2, 3]
end
it "should not match if `is` is empty but `should` isn't" do
property.must_not be_insync []
end
end
end
context "array_matching :first" do
# `@should` is an array of scalar values, and `is` is a scalar value.
before :each do
property.class.array_matching = :first
end
it_should_behave_like "#insync?"
[[1], # only the value
[1, 2], # matching value first
[2, 1], # matching value last
[0, 1, 2], # matching value in the middle
].each do |input|
it "should by true if one unmodified should value of #{input.inspect} matches what is" do
property.should = input
property.must be_insync 1
end
it "should be true if one stringified should value of #{input.inspect} matches what is" do
property.should = input
property.must be_insync "1"
end
end
it "should not match if we expect a string but get the non-stringified value" do
property.should = ["1"]
property.must_not be_insync 1
end
[[0], [0, 2]].each do |input|
it "should not match if no should values match what is" do
property.should = input
property.must_not be_insync 1
property.must_not be_insync "1" # shouldn't match either.
end
end
end
end
+
+ describe "#property_matches?" do
+ [1, "1", [1], :one].each do |input|
+ it "should treat two equal objects as equal (#{input.inspect})" do
+ property.property_matches?(input, input).should be_true
+ end
+ end
+
+ it "should treat two objects as equal if the first argument is the stringified version of the second" do
+ property.property_matches?("1", 1).should be_true
+ end
+
+ it "should NOT treat two objects as equal if the first argument is not a string, and the second argument is a string, even if it stringifies to the first" do
+ property.property_matches?(1, "1").should be_false
+ end
+ end
end