diff --git a/lib/puppet/parser/ast/comparison_operator.rb b/lib/puppet/parser/ast/comparison_operator.rb index c8694bbff..039c81df8 100644 --- a/lib/puppet/parser/ast/comparison_operator.rb +++ b/lib/puppet/parser/ast/comparison_operator.rb @@ -1,39 +1,38 @@ require 'puppet' require 'puppet/parser/ast/branch' class Puppet::Parser::AST class ComparisonOperator < AST::Branch attr_accessor :operator, :lval, :rval # Iterate across all of our children. def each [@lval,@rval,@operator].each { |child| yield child } end # Returns a boolean which is the result of the boolean operation # of lval and rval operands def evaluate(scope) # evaluate the operands, should return a boolean value lval = @lval.safeevaluate(scope) - rval = @rval.safeevaluate(scope) - # convert to number if operands are number - lval = Puppet::Parser::Scope.number?(lval) || lval - rval = Puppet::Parser::Scope.number?(rval) || rval + case @operator + when "==","!=" + @rval.evaluate_match(lval, scope) ? @operator == '==' : @operator == '!=' + else + rval = @rval.safeevaluate(scope) + rval = Puppet::Parser::Scope.number?(rval) || rval + lval = Puppet::Parser::Scope.number?(lval) || lval - # return result - unless @operator == '!=' lval.send(@operator,rval) - else - lval != rval end end def initialize(hash) super raise ArgumentError, "Invalid comparison operator #{@operator}" unless %w{== != < > <= >=}.include?(@operator) end end end diff --git a/lib/puppet/parser/ast/leaf.rb b/lib/puppet/parser/ast/leaf.rb index 3b9163d9c..49f430278 100644 --- a/lib/puppet/parser/ast/leaf.rb +++ b/lib/puppet/parser/ast/leaf.rb @@ -1,224 +1,227 @@ class Puppet::Parser::AST # The base class for all of the leaves of the parse trees. These # basically just have types and values. Both of these parameters # are simple values, not AST objects. class Leaf < AST attr_accessor :value, :type # Return our value. def evaluate(scope) @value end # evaluate ourselves, and match def evaluate_match(value, scope) obj = self.safeevaluate(scope) obj = obj.downcase if obj.respond_to?(:downcase) value = value.downcase if value.respond_to?(:downcase) + obj = Puppet::Parser::Scope.number?(obj) || obj + value = Puppet::Parser::Scope.number?(value) || value + # "" == undef for case/selector/if obj == value or (obj == "" and value == :undef) end def match(value) @value == value end def to_s @value.to_s unless @value.nil? end end # The boolean class. True or false. Converts the string it receives # to a Ruby boolean. class Boolean < AST::Leaf # Use the parent method, but then convert to a real boolean. def initialize(hash) super unless @value == true or @value == false raise Puppet::DevError, "'#{@value}' is not a boolean" end @value end def to_s @value ? "true" : "false" end end # The base string class. class String < AST::Leaf def evaluate(scope) @value end def to_s "\"#{@value}\"" end end # An uninterpreted string. class FlatString < AST::Leaf def evaluate(scope) @value end def to_s "\"#{@value}\"" end end class Concat < AST::Leaf def evaluate(scope) @value.collect { |x| x.evaluate(scope) }.join end def to_s "concat(#{@value.join(',')})" end end # The 'default' option on case statements and selectors. class Default < AST::Leaf; end # Capitalized words; used mostly for type-defaults, but also # get returned by the lexer any other time an unquoted capitalized # word is found. class Type < AST::Leaf; end # Lower-case words. class Name < AST::Leaf; end # double-colon separated class names class ClassName < AST::Leaf; end # undef values; equiv to nil class Undef < AST::Leaf; end # Host names, either fully qualified or just the short name, or even a regex class HostName < AST::Leaf def initialize(hash) super # Note that this is an AST::Regex, not a Regexp @value = @value.to_s.downcase unless @value.is_a?(Regex) if @value =~ /[^-\w.]/ raise Puppet::DevError, "'#{@value}' is not a valid hostname" end end # implementing eql? and hash so that when an HostName is stored # in a hash it has the same hashing properties as the underlying value def eql?(value) value = value.value if value.is_a?(HostName) @value.eql?(value) end def hash @value.hash end def to_s @value.to_s end end # A simple variable. This object is only used during interpolation; # the VarDef class is used for assignment. class Variable < Name # Looks up the value of the object in the scope tree (does # not include syntactical constructs, like '$' and '{}'). def evaluate(scope) parsewrap do if (var = scope.lookupvar(@value, false)) == :undefined var = :undef end var end end def to_s "\$#{value}" end end class HashOrArrayAccess < AST::Leaf attr_accessor :variable, :key def evaluate_container(scope) container = variable.respond_to?(:evaluate) ? variable.safeevaluate(scope) : variable (container.is_a?(Hash) or container.is_a?(Array)) ? container : scope.lookupvar(container) end def evaluate_key(scope) key.respond_to?(:evaluate) ? key.safeevaluate(scope) : key end def evaluate(scope) object = evaluate_container(scope) raise Puppet::ParseError, "#{variable} is not an hash or array when accessing it with #{accesskey}" unless object.is_a?(Hash) or object.is_a?(Array) object[evaluate_key(scope)] end # Assign value to this hashkey or array index def assign(scope, value) object = evaluate_container(scope) accesskey = evaluate_key(scope) if object.is_a?(Hash) and object.include?(accesskey) raise Puppet::ParseError, "Assigning to the hash '#{variable}' with an existing key '#{accesskey}' is forbidden" end # assign to hash or array object[accesskey] = value end def to_s "\$#{variable.to_s}[#{key.to_s}]" end end class Regex < AST::Leaf def initialize(hash) super @value = Regexp.new(@value) unless @value.is_a?(Regexp) end # we're returning self here to wrap the regexp and to be used in places # where a string would have been used, without modifying any client code. # For instance, in many places we have the following code snippet: # val = @val.safeevaluate(@scope) # if val.match(otherval) # ... # end # this way, we don't have to modify this test specifically for handling # regexes. def evaluate(scope) self end def evaluate_match(value, scope, options = {}) value = value.is_a?(String) ? value : value.to_s if matched = @value.match(value) scope.ephemeral_from(matched, options[:file], options[:line]) end matched end def match(value) @value.match(value) end def to_s "/#{@value.source}/" end end end diff --git a/spec/unit/parser/ast/comparison_operator_spec.rb b/spec/unit/parser/ast/comparison_operator_spec.rb index 724b6c6f7..931f936df 100755 --- a/spec/unit/parser/ast/comparison_operator_spec.rb +++ b/spec/unit/parser/ast/comparison_operator_spec.rb @@ -1,92 +1,116 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' describe Puppet::Parser::AST::ComparisonOperator do before :each do @scope = Puppet::Parser::Scope.new - @one = stub 'one', :safeevaluate => "1" - @two = stub 'two', :safeevaluate => "2" + @one = Puppet::Parser::AST::Leaf.new(:value => "1") + @two = Puppet::Parser::AST::Leaf.new(:value => "2") + + @lval = Puppet::Parser::AST::Leaf.new(:value => "one") + @rval = Puppet::Parser::AST::Leaf.new(:value => "two") end - it "should evaluate both branches" do - lval = stub "lval" - lval.expects(:safeevaluate).with(@scope) - rval = stub "rval" - rval.expects(:safeevaluate).with(@scope) + it "should evaluate both values" do + @lval.expects(:safeevaluate).with(@scope) + @rval.expects(:safeevaluate).with(@scope) - operator = Puppet::Parser::AST::ComparisonOperator.new :lval => lval, :operator => "==", :rval => rval + operator = Puppet::Parser::AST::ComparisonOperator.new :lval => @lval, :operator => "==", :rval => @rval operator.evaluate(@scope) end - it "should convert arguments strings to numbers if they are" do + it "should convert the arguments to numbers if they are numbers in string" do Puppet::Parser::Scope.expects(:number?).with("1").returns(1) Puppet::Parser::Scope.expects(:number?).with("2").returns(2) operator = Puppet::Parser::AST::ComparisonOperator.new :lval => @one, :operator => "==", :rval => @two operator.evaluate(@scope) end - %w{< > <= >= ==}.each do |oper| + %w{< > <= >=}.each do |oper| it "should use string comparison #{oper} if operands are strings" do - lval = stub 'one', :safeevaluate => "one" - rval = stub 'two', :safeevaluate => "two" - Puppet::Parser::Scope.stubs(:number?).with("one").returns(nil) - Puppet::Parser::Scope.stubs(:number?).with("two").returns(nil) - - operator = Puppet::Parser::AST::ComparisonOperator.new :lval => lval, :operator => oper, :rval => rval + operator = Puppet::Parser::AST::ComparisonOperator.new :lval => @lval, :operator => oper, :rval => @rval operator.evaluate(@scope).should == "one".send(oper,"two") end end - it "should fail with arguments of different types" do - lval = stub 'one', :safeevaluate => "one" - rval = stub 'two', :safeevaluate => "2" - Puppet::Parser::Scope.stubs(:number?).with("one").returns(nil) - Puppet::Parser::Scope.stubs(:number?).with("2").returns(2) + describe "with string comparison" do + it "should use matching" do + @rval.expects(:evaluate_match).with("one", @scope) - operator = Puppet::Parser::AST::ComparisonOperator.new :lval => lval, :operator => ">", :rval => rval + operator = Puppet::Parser::AST::ComparisonOperator.new :lval => @lval, :operator => "==", :rval => @rval + operator.evaluate(@scope) + end + + it "should return true for :undef to '' equality" do + astundef = Puppet::Parser::AST::Leaf.new(:value => :undef) + empty = Puppet::Parser::AST::Leaf.new(:value => '') + + operator = Puppet::Parser::AST::ComparisonOperator.new :lval => astundef, :operator => "==", :rval => empty + operator.evaluate(@scope).should be_true + end + + [true, false].each do |result| + it "should return #{(result).inspect} with '==' when matching return #{result.inspect}" do + @rval.expects(:evaluate_match).with("one", @scope).returns result + + operator = Puppet::Parser::AST::ComparisonOperator.new :lval => @lval, :operator => "==", :rval => @rval + operator.evaluate(@scope).should == result + end + + it "should return #{(!result).inspect} with '!=' when matching return #{result.inspect}" do + @rval.expects(:evaluate_match).with("one", @scope).returns result + + operator = Puppet::Parser::AST::ComparisonOperator.new :lval => @lval, :operator => "!=", :rval => @rval + operator.evaluate(@scope).should == !result + end + end + end + + it "should fail with arguments of different types" do + operator = Puppet::Parser::AST::ComparisonOperator.new :lval => @one, :operator => ">", :rval => @rval lambda { operator.evaluate(@scope) }.should raise_error(ArgumentError) end it "should fail for an unknown operator" do lambda { operator = Puppet::Parser::AST::ComparisonOperator.new :lval => @one, :operator => "or", :rval => @two }.should raise_error end %w{< > <= >= ==}.each do |oper| it "should return the result of using '#{oper}' to compare the left and right sides" do operator = Puppet::Parser::AST::ComparisonOperator.new :lval => @one, :operator => oper, :rval => @two operator.evaluate(@scope).should == 1.send(oper,2) end end it "should return the result of using '!=' to compare the left and right sides" do operator = Puppet::Parser::AST::ComparisonOperator.new :lval => @one, :operator => '!=', :rval => @two operator.evaluate(@scope).should == true end it "should work for variables too" do one = Puppet::Parser::AST::Variable.new( :value => "one" ) two = Puppet::Parser::AST::Variable.new( :value => "two" ) @scope.expects(:lookupvar).with("one", false).returns(1) @scope.expects(:lookupvar).with("two", false).returns(2) operator = Puppet::Parser::AST::ComparisonOperator.new :lval => one, :operator => "<", :rval => two operator.evaluate(@scope).should == true end # see ticket #1759 %w{< > <= >=}.each do |oper| it "should return the correct result of using '#{oper}' to compare 10 and 9" do - ten = stub 'one', :safeevaluate => "10" - nine = stub 'two', :safeevaluate => "9" + ten = Puppet::Parser::AST::Leaf.new(:value => "10") + nine = Puppet::Parser::AST::Leaf.new(:value => "9") operator = Puppet::Parser::AST::ComparisonOperator.new :lval => ten, :operator => oper, :rval => nine operator.evaluate(@scope).should == 10.send(oper,9) end end end diff --git a/spec/unit/parser/ast/leaf_spec.rb b/spec/unit/parser/ast/leaf_spec.rb index 379cbfde7..d21cbf573 100755 --- a/spec/unit/parser/ast/leaf_spec.rb +++ b/spec/unit/parser/ast/leaf_spec.rb @@ -1,367 +1,388 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' describe Puppet::Parser::AST::Leaf do before :each do @scope = stub 'scope' @value = stub 'value' @leaf = Puppet::Parser::AST::Leaf.new(:value => @value) end it "should have a evaluate_match method" do Puppet::Parser::AST::Leaf.new(:value => "value").should respond_to(:evaluate_match) end describe "when evaluate_match is called" do it "should evaluate itself" do @leaf.expects(:safeevaluate).with(@scope) @leaf.evaluate_match("value", @scope) end it "should match values by equality" do @value.stubs(:==).returns(false) @leaf.stubs(:safeevaluate).with(@scope).returns(@value) @value.expects(:==).with("value") @leaf.evaluate_match("value", @scope) end it "should downcase the evaluated value if wanted" do @leaf.stubs(:safeevaluate).with(@scope).returns(@value) @value.expects(:downcase).returns("value") @leaf.evaluate_match("value", @scope) end + it "should convert values to number" do + @leaf.stubs(:safeevaluate).with(@scope).returns(@value) + Puppet::Parser::Scope.expects(:number?).with(@value).returns(2) + Puppet::Parser::Scope.expects(:number?).with("23").returns(23) + + @leaf.evaluate_match("23", @scope) + end + + it "should compare 'numberized' values" do + @leaf.stubs(:safeevaluate).with(@scope).returns(@value) + two = stub_everything 'two' + one = stub_everything 'one' + + Puppet::Parser::Scope.stubs(:number?).with(@value).returns(one) + Puppet::Parser::Scope.stubs(:number?).with("2").returns(two) + + one.expects(:==).with(two) + + @leaf.evaluate_match("2", @scope) + end + it "should match undef if value is an empty string" do @leaf.stubs(:safeevaluate).with(@scope).returns("") @leaf.evaluate_match(:undef, @scope).should be_true end it "should downcase the parameter value if wanted" do parameter = stub 'parameter' parameter.expects(:downcase).returns("value") @leaf.evaluate_match(parameter, @scope) end end describe "when converting to string" do it "should transform its value to string" do value = stub 'value', :is_a? => true value.expects(:to_s) Puppet::Parser::AST::Leaf.new( :value => value ).to_s end end it "should have a match method" do @leaf.should respond_to(:match) end it "should delegate match to ==" do @value.expects(:==).with("value") @leaf.match("value") end end describe Puppet::Parser::AST::FlatString do describe "when converting to string" do it "should transform its value to a quoted string" do value = stub 'value', :is_a? => true, :to_s => "ab" Puppet::Parser::AST::FlatString.new( :value => value ).to_s.should == "\"ab\"" end end end describe Puppet::Parser::AST::String do describe "when converting to string" do it "should transform its value to a quoted string" do value = stub 'value', :is_a? => true, :to_s => "ab" Puppet::Parser::AST::String.new( :value => value ).to_s.should == "\"ab\"" end end end describe Puppet::Parser::AST::Undef do before :each do @scope = stub 'scope' @undef = Puppet::Parser::AST::Undef.new(:value => :undef) end it "should match undef with undef" do @undef.evaluate_match(:undef, @scope).should be_true end it "should not match undef with an empty string" do @undef.evaluate_match("", @scope).should be_false end end describe Puppet::Parser::AST::HashOrArrayAccess do before :each do @scope = stub 'scope' end describe "when evaluating" do it "should evaluate the variable part if necessary" do @scope.stubs(:lookupvar).with("a").returns(["b"]) variable = stub 'variable', :evaluate => "a" access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => variable, :key => 0 ) variable.expects(:safeevaluate).with(@scope).returns("a") access.evaluate(@scope).should == "b" end it "should evaluate the access key part if necessary" do @scope.stubs(:lookupvar).with("a").returns(["b"]) index = stub 'index', :evaluate => 0 access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => index ) index.expects(:safeevaluate).with(@scope).returns(0) access.evaluate(@scope).should == "b" end it "should be able to return an array member" do @scope.stubs(:lookupvar).with("a").returns(["val1", "val2", "val3"]) access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => 1 ) access.evaluate(@scope).should == "val2" end it "should be able to return an hash value" do @scope.stubs(:lookupvar).with("a").returns({ "key1" => "val1", "key2" => "val2", "key3" => "val3" }) access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" ) access.evaluate(@scope).should == "val2" end it "should raise an error if the variable lookup didn't return an hash or an array" do @scope.stubs(:lookupvar).with("a").returns("I'm a string") access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" ) lambda { access.evaluate(@scope) }.should raise_error end it "should raise an error if the variable wasn't in the scope" do @scope.stubs(:lookupvar).with("a").returns(nil) access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" ) lambda { access.evaluate(@scope) }.should raise_error end it "should return a correct string representation" do access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key2" ) access.to_s.should == '$a[key2]' end it "should work with recursive hash access" do @scope.stubs(:lookupvar).with("a").returns({ "key" => { "subkey" => "b" }}) access1 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key") access2 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => access1, :key => "subkey") access2.evaluate(@scope).should == 'b' end it "should work with interleaved array and hash access" do @scope.stubs(:lookupvar).with("a").returns({ "key" => [ "a" , "b" ]}) access1 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key") access2 = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => access1, :key => 1) access2.evaluate(@scope).should == 'b' end end describe "when assigning" do it "should add a new key and value" do scope = Puppet::Parser::Scope.new scope.setvar("a", { 'a' => 'b' }) access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "b") access.assign(scope, "c" ) scope.lookupvar("a").should be_include("b") end it "should raise an error when trying to overwrite an hash value" do @scope.stubs(:lookupvar).with("a").returns({ "key" => [ "a" , "b" ]}) access = Puppet::Parser::AST::HashOrArrayAccess.new(:variable => "a", :key => "key") lambda { access.assign(@scope, "test") }.should raise_error end end end describe Puppet::Parser::AST::Regex do before :each do @scope = stub 'scope' end describe "when initializing" do it "should create a Regexp with its content when value is not a Regexp" do Regexp.expects(:new).with("/ab/") Puppet::Parser::AST::Regex.new :value => "/ab/" end it "should not create a Regexp with its content when value is a Regexp" do value = Regexp.new("/ab/") Regexp.expects(:new).with("/ab/").never Puppet::Parser::AST::Regex.new :value => value end end describe "when evaluating" do it "should return self" do val = Puppet::Parser::AST::Regex.new :value => "/ab/" val.evaluate(@scope).should === val end end describe "when evaluate_match" do before :each do @value = stub 'regex' @value.stubs(:match).with("value").returns(true) Regexp.stubs(:new).returns(@value) @regex = Puppet::Parser::AST::Regex.new :value => "/ab/" end it "should issue the regexp match" do @value.expects(:match).with("value") @regex.evaluate_match("value", @scope) end it "should not downcase the paramater value" do @value.expects(:match).with("VaLuE") @regex.evaluate_match("VaLuE", @scope) end it "should set ephemeral scope vars if there is a match" do @scope.expects(:ephemeral_from).with(true, nil, nil) @regex.evaluate_match("value", @scope) end it "should return the match to the caller" do @value.stubs(:match).with("value").returns(:match) @scope.stubs(:ephemeral_from) @regex.evaluate_match("value", @scope) end end it "should return the regex source with to_s" do regex = stub 'regex' Regexp.stubs(:new).returns(regex) val = Puppet::Parser::AST::Regex.new :value => "/ab/" regex.expects(:source) val.to_s end it "should delegate match to the underlying regexp match method" do regex = Regexp.new("/ab/") val = Puppet::Parser::AST::Regex.new :value => regex regex.expects(:match).with("value") val.match("value") end end describe Puppet::Parser::AST::Variable do before :each do @scope = stub 'scope' @var = Puppet::Parser::AST::Variable.new(:value => "myvar") end it "should lookup the variable in scope" do @scope.expects(:lookupvar).with("myvar", false).returns(:myvalue) @var.safeevaluate(@scope).should == :myvalue end it "should return undef if the variable wasn't set" do @scope.expects(:lookupvar).with("myvar", false).returns(:undefined) @var.safeevaluate(@scope).should == :undef end describe "when converting to string" do it "should transform its value to a variable" do value = stub 'value', :is_a? => true, :to_s => "myvar" Puppet::Parser::AST::Variable.new( :value => value ).to_s.should == "\$myvar" end end end describe Puppet::Parser::AST::HostName do before :each do @scope = stub 'scope' @value = stub 'value', :=~ => false @value.stubs(:to_s).returns(@value) @value.stubs(:downcase).returns(@value) @host = Puppet::Parser::AST::HostName.new( :value => @value) end it "should raise an error if hostname is not valid" do lambda { Puppet::Parser::AST::HostName.new( :value => "not an hostname!" ) }.should raise_error end it "should not raise an error if hostname is a regex" do lambda { Puppet::Parser::AST::HostName.new( :value => Puppet::Parser::AST::Regex.new(:value => "/test/") ) }.should_not raise_error end it "should stringify the value" do value = stub 'value', :=~ => false value.expects(:to_s).returns("test") Puppet::Parser::AST::HostName.new(:value => value) end it "should downcase the value" do value = stub 'value', :=~ => false value.stubs(:to_s).returns("UPCASED") host = Puppet::Parser::AST::HostName.new(:value => value) host.value == "upcased" end it "should evaluate to its value" do @host.evaluate(@scope).should == @value end it "should delegate eql? to the underlying value if it is an HostName" do @value.expects(:eql?).with("value") @host.eql?("value") end it "should delegate eql? to the underlying value if it is not an HostName" do value = stub 'compared', :is_a? => true, :value => "value" @value.expects(:eql?).with("value") @host.eql?(value) end it "should delegate hash to the underlying value" do @value.expects(:hash) @host.hash end end