require 'spec_helper' require 'puppet_spec/language' ['Function', 'EPP'].each do |call_type| describe "#{call_type} parameter default expressions" do let! (:func_bodies) { [ '{}', '{ notice("\$a == ${a}") }', '{ notice("\$a == ${a}") notice("\$b == ${b}") }', '{ notice("\$a == ${a}") notice("\$b == ${b}") notice("\$c == ${c}") }' ] } let! (:epp_bodies) { [ '', '<% notice("\$a == ${a}") %>', '<% notice("\$a == ${a}") notice("\$b == ${b}") %>', '<% notice("\$a == ${a}") notice("\$b == ${b}") notice("\$c == ${c}") %>' ] } let! (:param_names) { ('a'..'c').to_a } let (:call_type) { call_type } let (:compiler) { Puppet::Parser::Compiler.new(Puppet::Node.new('specification')) } def collect_notices(code) logs = [] Puppet[:code] = code Puppet::Util::Log.with_destination(Puppet::Test::LogCollector.new(logs)) do compiler.compile yield end logs.select { |log| log.level == :notice }.map { |log| log.message } end # @param arg_list [String] comma separated parameter declarations. Not enclosed in parenthesis # @param body [String,Integer] verbatim body or index of an entry in func_bodies # @param call_params [Array[#to_s]] array of call parameters # @return [Array] array of notice output entries # def eval_collect_notices(arg_list, body, call_params) call = call_params.is_a?(String) ? call_params : "example(#{call_params.map {|p| p.nil? ? 'undef' : (p.is_a?(String) ? "'#{p}'" : p )}.join(',')})" body = func_bodies[body] if body.is_a?(Integer) evaluator = Puppet::Pops::Parser::EvaluatingParser.new() collect_notices("function example(#{arg_list}) #{body}") do evaluator.evaluate_string(compiler.topscope, call) end end # @param arg_list [String] comma separated parameter declarations. Not enclosed in parenthesis # @param body [String,Integer] verbatim body or index of an entry in bodies # @param call_params [Array] array of call parameters # @param code [String] code to evaluate # @return [Array] array of notice output entries # def epp_eval_collect_notices(arg_list, body, call_params, code, inline_epp) body = body.is_a?(Integer) ? epp_bodies[body] : body.strip source = "<%| #{arg_list} |%>#{body}" named_params = call_params.reduce({}) {|h, v| h[param_names[h.size]] = v; h } collect_notices(code) do if inline_epp Puppet::Pops::Evaluator::EppEvaluator.inline_epp(compiler.topscope, source, named_params) else file = Tempfile.new(['epp-script', '.epp']) begin file.write(source) file.close Puppet::Pops::Evaluator::EppEvaluator.epp(compiler.topscope, file.path, 'test', named_params) ensure file.unlink end end end end # evaluates a function or EPP call, collects notice output in a log and compares log to expected result # # @param decl [Array[]] two element array with argument list declaration and body. Body can a verbatim string # or an integer index of an entry in bodies # @param call_params [Array[#to_s]] array of call parameters # @param code [String] code to evaluate. Only applicable when call_type == 'EPP' # @param inline_epp [Boolean] true for inline_epp, false for file based epp, Only applicable when call_type == 'EPP' # @return [Array] array of notice output entries # def expect_log(decl, call_params, result, code = 'undef', inline_epp = true) if call_type == 'Function' expect(eval_collect_notices(decl[0], decl[1], call_params)).to include(*result) else expect(epp_eval_collect_notices(decl[0], decl[1], call_params, code, inline_epp)).to include(*result) end end # evaluates a function or EPP call and expects a failure # # @param decl [Array[]] two element array with argument list declaration and body. Body can a verbatim string # or an integer index of an entry in bodies # @param call_params [Array[#to_s]] array of call parameters # @param text [String,Regexp] expected error message def expect_fail(decl, call, text, inline_epp = true) if call_type == 'Function' expect{eval_collect_notices(decl[0], decl[1], call) }.to raise_error(StandardError, text) else expect{epp_eval_collect_notices(decl[0], decl[1], call, 'undef', inline_epp) }.to raise_error(StandardError, text) end end context 'that references a parameter to the left that has no default' do let!(:params) { [ <<-SOURCE, 2 ] $a, $b = $a SOURCE } it 'fails when no value is provided for required first parameter', :if => call_type == 'Function' do expect_fail(params, [], /expects between 1 and 2 arguments, got none/) end it 'fails when no value is provided for required first parameter', :if => call_type == 'EPP' do expect_fail(params, [], /default expression for \$b tries to illegally access not yet evaluated \$a/) end it "will use the referenced parameter's given value" do expect_log(params, [2], ['$a == 2', '$b == 2']) end it 'will not be evaluated when a value is given' do expect_log(params, [2, 5], ['$a == 2', '$b == 5']) end end context 'that references a parameter to the left that has a default' do let!(:params) { [ <<-SOURCE, 2 ] $a = 10, $b = $a SOURCE } it "will use the referenced parameter's default value when no value is given for the referenced parameter" do expect_log(params, [], ['$a == 10', '$b == 10']) end it "will use the referenced parameter's given value" do expect_log(params, [2], ['$a == 2', '$b == 2']) end it 'will not be evaluated when a value is given' do expect_log(params, [2, 5], ['$a == 2', '$b == 5']) end end context 'that references a variable to the right' do let!(:params) { [ <<-SOURCE, 3 ] $a = 10, $b = $c, $c = 20 SOURCE } it 'fails when the reference is evaluated' do expect_fail(params, [1], /default expression for \$b tries to illegally access not yet evaluated \$c/) end it 'does not fail when a value is given for the culprit parameter' do expect_log(params, [1,2], ['$a == 1', '$b == 2', '$c == 20']) end it 'does not fail when all values are given' do expect_log(params, [1,2,3], ['$a == 1', '$b == 2', '$c == 3']) end end context 'with regular expressions' do it "evaluates unset match scope parameter's to undef" do expect_log([<<-SOURCE, 2], [], ['$a == ', '$b == ']) $a = $0, $b = $1 SOURCE end it 'does not leak match variables from one expression to the next' do expect_log([<<-SOURCE, 2], [], ['$a == [true, h, ello]', '$b == ']) $a = ['hello' =~ /(h)(.*)/, $1, $2], $b = $1 SOURCE end it 'can evaluate expressions in separate match scopes' do expect_log([<<-SOURCE, 3], [], ['$a == [true, h, ell, o]', '$b == [true, h, i, ]', '$c == ']) $a = ['hello' =~ /(h)(.*)(o)/, $1, $2, $3], $b = ['hi' =~ /(h)(.*)/, $1, $2, $3], $c = $1 SOURCE end it 'can have nested match expressions' do expect_log([<<-SOURCE, 2], [], ['$a == [true, h, oo, h, i]', '$b == '] ) $a = ['hi' =~ /(h)(.*)/, $1, if'foo' =~ /f(oo)/ { $1 }, $1, $2], $b = $0 SOURCE end it 'can not see match scope from calling scope', :if => call_type == 'Function' do expect_log([<<-SOURCE, <<-BODY], <<-CALL, ['$a == ']) $a = $0 SOURCE { notice("\\$a == ${a}") } function caller() { example() } BODY $tmp = 'foo' =~ /(f)(o)(o)/ caller() CALL end context 'matches in calling scope', :if => call_type == 'EPP' do it 'are available when using inlined epp' do # Note that CODE is evaluated before the EPP is evaluated # expect_log([<<-SOURCE, <<-BODY], [], ['$ax == true', '$bx == foo'], <<-CODE, true) $a = $tmp, $b = $0 SOURCE <% called_from_template($a, $b) %> BODY function called_from_template($ax, $bx) { notice("\\$ax == $ax") notice("\\$bx == $bx") } $tmp = 'foo' =~ /(f)(o)(o)/ CODE end it 'are not available when using epp file' do # Note that CODE is evaluated before the EPP is evaluated # expect_log([<<-SOURCE, <<-BODY], [], ['$ax == true', '$bx == '], <<-CODE, false) $a = $tmp, $b = $0 SOURCE <% called_from_template($a, $b) %> BODY function called_from_template($ax, $bx) { notice("\\$ax == $ax") notice("\\$bx == $bx") } $tmp = 'foo' =~ /(f)(o)(o)/ CODE end end it 'will allow nested lambdas to access enclosing match scope' do expect_log([<<-SOURCE, 1], [], ['$a == [1-ello, 2-ello, 3-ello]']) $a = case "hello" { /(h)(.*)/ : { [1,2,3].map |$x| { "$x-$2" } } } SOURCE end it "will not make match scope available to #{call_type} body" do expect_log([<<-SOURCE, call_type == 'Function' ? <<-BODY : <<-EPP_BODY], [], ['Yes']) $a = "hello" =~ /.*/ SOURCE { notice("Y${0}es") } BODY <% notice("Y${0}es") %> EPP_BODY end it 'can access earlier match results when produced using the match function' do expect_log([<<-SOURCE, 3], [], ['$a == [hello, h, ello]', '$b == hello', '$c == h']) $a = 'hello'.match(/(h)(.*)/), $b = $a[0], $c = $a[1] SOURCE end end context 'will not permit assignments' do it 'at top level' do expect_fail([<<-SOURCE, 0], [], /Syntax error at '='/) $a = $x = $0 SOURCE end it 'in arrays' do expect_fail([<<-SOURCE, 0], [], /Assignment not allowed here/) $a = [$x = 3] SOURCE end it 'of variable with the same name as a subsequently declared parameter' do expect_fail([<<-SOURCE, 0], [], /Assignment not allowed here/) $a = ($b = 3), $b = 5 SOURCE end it 'of variable with the same name as a previously declared parameter' do expect_fail([<<-SOURCE, 0], [], /Assignment not allowed here/) $a = 10, $b = ($a = 10) SOURCE end end it 'will permit assignments in nested scope' do expect_log([<<-SOURCE, 3], [], ['$a == [1, 2, 3]', '$b == 0', '$c == [6, 12, 18]']) $a = [1,2,3], $b = 0, $c = $a.map |$x| { $b = $x; $b * $a.reduce |$x, $y| {$x + $y} } SOURCE end it 'will not permit duplicate parameter names' do expect_fail([<<-SOURCE, 0], [], /The parameter 'a' is declared more than once/ ) $a = 2, $a = 5 SOURCE end it 'will permit undef for optional parameters' do expect_log([<<-SOURCE, 1], [nil], ['$a == ']) Optional[Integer] $a SOURCE end it 'undef will override parameter default', :if => call_type == 'Function' do expect_log([<<-SOURCE, 1], [nil], ['$a == ']) Optional[Integer] $a = 4 SOURCE end it 'undef will not override parameter default', :unless => call_type == 'Function' do expect_log([<<-SOURCE, 1], [nil], ['$a == 4']) Optional[Integer] $a = 4 SOURCE end end end