diff --git a/lib/puppet/parser/ast/selector.rb b/lib/puppet/parser/ast/selector.rb index d6a4ea436..04f0b1873 100644 --- a/lib/puppet/parser/ast/selector.rb +++ b/lib/puppet/parser/ast/selector.rb @@ -1,44 +1,50 @@ require 'puppet/parser/ast/branch' class Puppet::Parser::AST # The inline conditional operator. Unlike CaseStatement, which executes # code, we just return a value. class Selector < AST::Branch attr_accessor :param, :values def each [@param,@values].each { |child| yield child } end # Find the value that corresponds with the test. def evaluate(scope) level = scope.ephemeral_level # Get our parameter. paramvalue = @param.safeevaluate(scope) default = nil @values = [@values] unless @values.instance_of? AST::ASTArray or @values.instance_of? Array # Then look for a match in the options. @values.each do |obj| # short circuit asap if we have a match return obj.value.safeevaluate(scope) if obj.param.evaluate_match(paramvalue, scope) # Store the default, in case it's necessary. default = obj if obj.param.is_a?(Default) end # Unless we found something, look for the default. return default.value.safeevaluate(scope) if default self.fail Puppet::ParseError, "No matching value for selector param '#{paramvalue}'" ensure scope.unset_ephemeral_var(level) end def to_s - param.to_s + " ? { " + values.collect { |v| v.to_s }.join(', ') + " }" + if @values.instance_of? AST::ASTArray or @values.instance_of? Array + v = @values + else + v = [@values] + end + + param.to_s + " ? { " + v.collect { |v| v.to_s }.join(', ') + " }" end end end diff --git a/spec/unit/parser/ast/selector_spec.rb b/spec/unit/parser/ast/selector_spec.rb index 1bf5f6757..771b5df6c 100755 --- a/spec/unit/parser/ast/selector_spec.rb +++ b/spec/unit/parser/ast/selector_spec.rb @@ -1,138 +1,86 @@ #!/usr/bin/env rspec require 'spec_helper' describe Puppet::Parser::AST::Selector do - before :each do - @scope = Puppet::Parser::Scope.new + let :scope do Puppet::Parser::Scope.new end + + # Take a code expression containing a selector, and return that portion of + # the AST. This does the magic required to make that legal and all. + def parse(selector) + Puppet::Parser::Parser.new(scope.environment). + parse("$foo = #{selector}"). + code[0].value # extract only the selector end describe "when evaluating" do - - before :each do - @param = stub 'param' - @param.stubs(:safeevaluate).with(@scope).returns("value") - - @value1 = stub 'value1' - @param1 = stub_everything 'param1' - @param1.stubs(:safeevaluate).with(@scope).returns(@param1) - @param1.stubs(:respond_to?).with(:downcase).returns(false) - @value1.stubs(:param).returns(@param1) - @value1.stubs(:value).returns(@value1) - - @value2 = stub 'value2' - @param2 = stub_everything 'param2' - @param2.stubs(:safeevaluate).with(@scope).returns(@param2) - @param2.stubs(:respond_to?).with(:downcase).returns(false) - @value2.stubs(:param).returns(@param2) - @value2.stubs(:value).returns(@value2) - - @values = stub 'values', :instance_of? => true - @values.stubs(:each).multiple_yields(@value1, @value2) - - @selector = Puppet::Parser::AST::Selector.new :param => @param, :values => @values - @selector.stubs(:fail) - end - it "should evaluate param" do - @param.expects(:safeevaluate).with(@scope) - - @selector.evaluate(@scope) + selector = parse 'value ? { default => result }' + selector.param.expects(:safeevaluate) + selector.evaluate(scope) end - it "should scan each option" do - @values.expects(:each).multiple_yields(@value1, @value2) + it "should try to match each option in sequence" do + selector = parse '"a" ? { "a" => "a", "b" => "b", default => "default" }' - @selector.evaluate(@scope) - end - - describe "when scanning values" do - it "should evaluate first matching option" do - @param2.stubs(:evaluate_match).with { |*arg| arg[0] == "value" }.returns(true) - @value2.expects(:safeevaluate).with(@scope) - - @selector.evaluate(@scope) + order = sequence('evaluation of matching options') + selector.values.each do |slot| + slot.param.expects(:evaluate_match).in_sequence(order).returns(false) end - it "should return the first matching evaluated option" do - @param2.stubs(:evaluate_match).with { |*arg| arg[0] == "value" }.returns(true) - @value2.stubs(:safeevaluate).with(@scope).returns(:result) + selector.evaluate(scope) + end - @selector.evaluate(@scope).should == :result + describe "when scanning values" do + it "should evaluate and return first matching option" do + selector = parse '"b" ? { "a" => "=a", "b" => "=b", "c" => "=c" }' + selector.evaluate(scope).should == '=b' end it "should evaluate the default option if none matched" do - @param1.stubs(:is_a?).with(Puppet::Parser::AST::Default).returns(true) - @value1.expects(:safeevaluate).with(@scope).returns(@param1) - - @selector.evaluate(@scope) + selector = parse '"a" ? { "b" => "=b", default => "=default" }' + selector.evaluate(scope).should == "=default" end - it "should return the default evaluated option if none matched" do - result = stub 'result' - @param1.stubs(:is_a?).with(Puppet::Parser::AST::Default).returns(true) - @value1.stubs(:safeevaluate).returns(result) - - @selector.evaluate(@scope).should == result + it "should return the default even if that isn't the last option" do + selector = parse '"a" ? { "b" => "=b", default => "=default", "c" => "=c" }' + selector.evaluate(scope).should == "=default" end - it "should return nil if nothing matched" do - @selector.evaluate(@scope).should be_nil - end - - it "should delegate matching to evaluate_match" do - @param1.expects(:evaluate_match).with { |*arg| arg[0] == "value" and arg[1] == @scope } - - @selector.evaluate(@scope) - end - - it "should evaluate the matching param" do - @param1.stubs(:evaluate_match).with { |*arg| arg[0] == "value" and arg[1] == @scope }.returns(true) - - @value1.expects(:safeevaluate).with(@scope) - - @selector.evaluate(@scope) - end - - it "should return this evaluated option if it matches" do - @param1.stubs(:evaluate_match).with { |*arg| arg[0] == "value" and arg[1] == @scope }.returns(true) - @value1.stubs(:safeevaluate).with(@scope).returns(:result) - - @selector.evaluate(@scope).should == :result + it "should raise ParseError if nothing matched, and no default" do + selector = parse '"a" ? { "b" => "=b" }' + msg = /No matching value for selector param/ + expect { selector.evaluate(scope) }.to raise_error Puppet::ParseError, msg end it "should unset scope ephemeral variables after option evaluation" do - @scope.stubs(:ephemeral_level).returns(:level) - @param1.stubs(:evaluate_match).with { |*arg| arg[0] == "value" and arg[1] == @scope }.returns(true) - @value1.stubs(:safeevaluate).with(@scope).returns(:result) - - @scope.expects(:unset_ephemeral_var).with(:level) - - @selector.evaluate(@scope) + selector = parse '"a" ? { "a" => "=a" }' + scope.expects(:unset_ephemeral_var).with(scope.ephemeral_level) + selector.evaluate(scope) end it "should not leak ephemeral variables even if evaluation fails" do - @scope.stubs(:ephemeral_level).returns(:level) - @param1.stubs(:evaluate_match).with { |*arg| arg[0] == "value" and arg[1] == @scope }.returns(true) - @value1.stubs(:safeevaluate).with(@scope).raises - - @scope.expects(:unset_ephemeral_var).with(:level) - - lambda { @selector.evaluate(@scope) }.should raise_error - end - - it "should fail if there is no default" do - @selector.expects(:fail) - - @selector.evaluate(@scope) + selector = parse '"a" ? { "b" => "=b" }' + scope.expects(:unset_ephemeral_var).with(scope.ephemeral_level) + expect { selector.evaluate(scope) }.to raise_error end end end + describe "when converting to string" do - it "should produce a string version of this selector" do - values = Puppet::Parser::AST::ASTArray.new :children => [ Puppet::Parser::AST::ResourceParam.new(:param => "type", :value => "value", :add => false) ] - param = Puppet::Parser::AST::Variable.new :value => "myvar" - selector = Puppet::Parser::AST::Selector.new :param => param, :values => values - selector.to_s.should == "$myvar ? { type => value }" + it "should work with a single match" do + parse('$input ? { "a" => "a+" }').to_s.should == '$input ? { "a" => "a+" }' + end + + it "should work with multiple matches" do + parse('$input ? { "a" => "a+", "b" => "b+" }').to_s. + should == '$input ? { "a" => "a+", "b" => "b+" }' + end + + it "should preserve order of inputs" do + match = ('a' .. 'z').map {|x| "#{x} => #{x}" }.join(', ') + selector = parse "$input ? { #{match} }" + + selector.to_s.should == "$input ? { #{match} }" end end end