diff --git a/lib/puppet/pops/evaluator/closure.rb b/lib/puppet/pops/evaluator/closure.rb index f8b8c6425..d68690b12 100644 --- a/lib/puppet/pops/evaluator/closure.rb +++ b/lib/puppet/pops/evaluator/closure.rb @@ -1,213 +1,218 @@ # A Closure represents logic bound to a particular scope. # As long as the runtime (basically the scope implementation) has the behaviour of Puppet 3x it is not # safe to use this closure when the scope given to it when initialized goes "out of scope". # # Note that the implementation is backwards compatible in that the call method accepts a scope, but this # scope is not used. # # Note that this class is a CallableSignature, and the methods defined there should be used # as the API for obtaining information in a callable implementation agnostic way. # class Puppet::Pops::Evaluator::Closure < Puppet::Pops::Evaluator::CallableSignature attr_reader :evaluator attr_reader :model attr_reader :enclosing_scope def initialize(evaluator, model, scope) @evaluator = evaluator @model = model @enclosing_scope = scope end # marker method checked with respond_to :puppet_lambda # @api private # @deprecated Use the type system to query if an object is of Callable type, then use its signatures method for info def puppet_lambda() true end # compatible with 3x AST::Lambda # @api public def call(scope, *args) - tc = Puppet::Pops::Types::TypeCalculator variable_bindings = combine_values_with_parameters(args) + tc = Puppet::Pops::Types::TypeCalculator final_args = tc.infer_set(parameters.inject([]) do |final_args, param| if param.captures_rest final_args.concat(variable_bindings[param.name]) else final_args << variable_bindings[param.name] end end) if tc.callable?(type, final_args) @evaluator.evaluate_block_with_bindings(@enclosing_scope, variable_bindings, @model.body) else raise ArgumentError, "lambda called with mis-matched arguments\n#{Puppet::Pops::Evaluator::CallableMismatchDescriber.diff_string('lambda', final_args, [self])}" end end # Call closure with argument assignment by name def call_by_name(scope, args_hash, spill_over = false) - parameters = @model.parameters || [] - if !spill_over && args_hash.size > parameters.size raise ArgumentError, "Too many arguments: #{args_hash.size} for #{parameters.size}" end # associate values with parameters scope_hash = {} parameters.each do |p| - scope_hash[p.name] = args_hash[p.name] || @evaluator.evaluate(p.value, scope) + scope_hash[p.name] = args_hash[p.name] || @evaluator.evaluate(p.value, @enclosing_scope) end - missing = scope_hash.reduce([]) {|memo, entry| memo << entry[0] if entry[1].nil?; memo } - unless missing.empty? - optional = parameters.count { |p| !p.value.nil? } - raise ArgumentError, "Too few arguments; no value given for required parameters #{missing.join(" ,")}" + + missing = scope_hash.select { |name, value| value.nil? } + if missing.any? + raise ArgumentError, "Too few arguments; no value given for required parameters #{missing.collect(&:first).join(" ,")}" end + if spill_over # all args from given hash should be used, nil entries replaced by default values should win scope_hash = args_hash.merge(scope_hash) end - @evaluator.evaluate_block_with_bindings(@enclosing_scope, scope_hash, @model.body) + tc = Puppet::Pops::Types::TypeCalculator + final_args = tc.infer_set(parameter_names.collect { |param| scope_hash[param] }) + if tc.callable?(type, final_args) + @evaluator.evaluate_block_with_bindings(@enclosing_scope, scope_hash, @model.body) + else + raise ArgumentError, "lambda called with mis-matched arguments\n#{Puppet::Pops::Evaluator::CallableMismatchDescriber.diff_string('lambda', final_args, [self])}" + end end def parameters @model.parameters end # Returns the number of parameters (required and optional) # @return [Integer] the total number of accepted parameters def parameter_count # yes, this is duplication of code, but it saves a method call @model.parameters.size end # @api public def parameter_names @model.parameters.collect(&:name) end # @api public def type @callable ||= create_callable_type end # @api public def last_captures_rest? last = @model.parameters[-1] last && last.captures_rest end # @api public def block_name # TODO: Lambda's does not support blocks yet. This is a placeholder 'unsupported_block' end private def combine_values_with_parameters(args) variable_bindings = {} parameters.each_with_index do |parameter, index| param_captures = parameter.captures_rest default_expression = parameter.value if index >= args.size if default_expression # not given, has default value = @evaluator.evaluate(default_expression, @enclosing_scope) if param_captures && !value.is_a?(Array) # correct non array default value value = [value] end else # not given, does not have default if param_captures # default for captures rest is an empty array value = [] else @evaluator.fail(Puppet::Pops::Issues::MISSING_REQUIRED_PARAMETER, parameter, { :param_name => parameter.name }) end end else given_argument = args[index] if param_captures # get excess arguments value = args[(parameter_count-1)..-1] # If the input was a single nil, or undef, and there is a default, use the default if value.size == 1 && (given_argument.nil? || given_argument == :undef) && default_expression value = @evaluator.evaluate(default_expression, scope) # and ensure it is an array value = [value] unless value.is_a?(Array) end else value = given_argument end end variable_bindings[parameter.name] = value end variable_bindings end def create_callable_type() types = [] range = [0, 0] in_optional_parameters = false parameters.each do |param| type = if param.type_expr @evaluator.evaluate(param.type_expr, @enclosing_scope) else Puppet::Pops::Types::TypeFactory.optional_object() end if param.captures_rest && type.is_a?(Puppet::Pops::Types::PArrayType) # An array on a slurp parameter is how a size range is defined for a # slurp (Array[Integer, 1, 3] *$param). However, the callable that is # created can't have the array in that position or else type checking # will require the parameters to be arrays, which isn't what is # intended. The array type contains the intended information and needs # to be unpacked. param_range = type.size_range type = type.element_type elsif param.captures_rest && !type.is_a?(Puppet::Pops::Types::PArrayType) param_range = ANY_NUMBER_RANGE elsif param.value param_range = OPTIONAL_SINGLE_RANGE else param_range = REQUIRED_SINGLE_RANGE end types << type if param_range[0] == 0 in_optional_parameters = true elsif param_range[0] != 0 && in_optional_parameters @evaluator.fail(Puppet::Pops::Issues::REQUIRED_PARAMETER_AFTER_OPTIONAL, param, { :param_name => param.name }) end range[0] += param_range[0] range[1] += param_range[1] end if range[1] == Puppet::Pops::Types::INFINITY range[1] = :default end Puppet::Pops::Types::TypeFactory.callable(*(types + range)) end # Produces information about parameters compatible with a 4x Function (which can have multiple signatures) def signatures [ self ] end ANY_NUMBER_RANGE = [0, Puppet::Pops::Types::INFINITY] OPTIONAL_SINGLE_RANGE = [0, 1] REQUIRED_SINGLE_RANGE = [1, 1] end diff --git a/lib/puppet/pops/evaluator/epp_evaluator.rb b/lib/puppet/pops/evaluator/epp_evaluator.rb index 31e59aea7..d7d9a0529 100644 --- a/lib/puppet/pops/evaluator/epp_evaluator.rb +++ b/lib/puppet/pops/evaluator/epp_evaluator.rb @@ -1,87 +1,87 @@ # Handler of Epp call/evaluation from the epp and inline_epp functions # class Puppet::Pops::Evaluator::EppEvaluator def self.inline_epp(scope, epp_source, template_args = nil) - unless epp_source.is_a? String + unless epp_source.is_a?(String) raise ArgumentError, "inline_epp(): the first argument must be a String with the epp source text, got a #{epp_source.class}" end # Parse and validate the source parser = Puppet::Pops::Parser::EvaluatingParser::EvaluatingEppParser.new begin - result = parser.parse_string(epp_source, 'inlined-epp-text') + result = parser.parse_string(epp_source, 'inlined-epp-text') rescue Puppet::ParseError => e raise ArgumentError, "inline_epp(): Invalid EPP: #{e.message}" end # Evaluate (and check template_args) evaluate(parser, 'inline_epp', scope, false, result, template_args) end def self.epp(scope, file, env_name, template_args = nil) - unless file.is_a? String + unless file.is_a?(String) raise ArgumentError, "epp(): the first argument must be a String with the filename, got a #{file.class}" end file = file + ".epp" unless file =~ /\.epp$/ scope.debug "Retrieving epp template #{file}" template_file = Puppet::Parser::Files.find_template(file, env_name) unless template_file raise Puppet::ParseError, "Could not find template '#{file}'" end # Parse and validate the source parser = Puppet::Pops::Parser::EvaluatingParser::EvaluatingEppParser.new begin - result = parser.parse_file(template_file) + result = parser.parse_file(template_file) rescue Puppet::ParseError => e raise ArgumentError, "epp(): Invalid EPP: #{e.message}" end # Evaluate (and check template_args) evaluate(parser, 'epp', scope, true, result, template_args) end private def self.evaluate(parser, func_name, scope, use_global_scope_only, parse_result, template_args) template_args, template_args_set = handle_template_args(func_name, template_args) body = parse_result.body unless body.is_a?(Puppet::Pops::Model::LambdaExpression) raise ArgumentError, "#{func_name}(): the parser did not produce a LambdaExpression, got '#{body.class}'" end unless body.body.is_a?(Puppet::Pops::Model::EppExpression) raise ArgumentError, "#{func_name}(): the parser did not produce an EppExpression, got '#{body.body.class}'" end unless parse_result.definitions.empty? raise ArgumentError, "#{func_name}(): The EPP template contains illegal expressions (definitions)" end see_scope = body.body.see_scope if see_scope && !template_args_set # no epp params and no arguments were given => inline_epp logic sees all local variables, epp all global closure_scope = use_global_scope_only ? scope.find_global_scope : scope spill_over = false else # no epp params or user provided arguments in a hash, epp logic only sees global + what was given closure_scope = scope.find_global_scope # given spill over if there are no params (e.g. replace closure scope by a new scope with the given args) spill_over = see_scope end evaluated_result = parser.closure(body, closure_scope).call_by_name(scope, template_args, spill_over) evaluated_result end def self.handle_template_args(func_name, template_args) if template_args.nil? [{}, false] else unless template_args.is_a?(Hash) raise ArgumentError, "#{func_name}(): the template_args must be a Hash, got a #{template_args.class}" end [template_args, true] end end -end \ No newline at end of file +end diff --git a/spec/unit/parser/functions/epp_spec.rb b/spec/unit/parser/functions/epp_spec.rb index 962709703..309b4ac13 100644 --- a/spec/unit/parser/functions/epp_spec.rb +++ b/spec/unit/parser/functions/epp_spec.rb @@ -1,103 +1,135 @@ require 'spec_helper' describe "the epp function" do include PuppetSpec::Files before :all do Puppet::Parser::Functions.autoloader.loadall end before :each do Puppet[:parser] = 'future' end let :node do Puppet::Node.new('localhost') end let :compiler do Puppet::Parser::Compiler.new(node) end let :scope do Puppet::Parser::Scope.new(compiler) end context "when accessing scope variables as $ variables" do it "looks up the value from the scope" do scope["what"] = "are belong" eval_template("all your base <%= $what %> to us").should == "all your base are belong to us" end it "get nil accessing a variable that does not exist" do eval_template("<%= $kryptonite == undef %>").should == "true" end it "get nil accessing a variable that is undef" do scope['undef_var'] = :undef eval_template("<%= $undef_var == undef %>").should == "true" end it "gets shadowed variable if args are given" do scope['phantom'] = 'of the opera' eval_template_with_args("<%= $phantom == dragos %>", 'phantom' => 'dragos').should == "true" end + it "can use values from the enclosing scope for defaults" do + scope['phantom'] = 'of the opera' + eval_template("<%- |$phantom = $phantom| -%><%= $phantom %>").should == "of the opera" + end + + it "uses the default value if the given value is undef/nil" do + eval_template_with_args("<%- |$phantom = 'inside your mind'| -%><%= $phantom %>", 'phantom' => nil).should == "inside your mind" + end + it "gets shadowed variable if args are given and parameters are specified" do scope['x'] = 'wrong one' eval_template_with_args("<%- |$x| -%><%= $x == correct %>", 'x' => 'correct').should == "true" end it "raises an error if required variable is not given" do scope['x'] = 'wrong one' - expect { + expect do eval_template_with_args("<%-| $x |-%><%= $x == correct %>", 'y' => 'correct') - }.to raise_error(/no value given for required parameters x/) + end.to raise_error(/no value given for required parameters x/) end it "raises an error if too many arguments are given" do scope['x'] = 'wrong one' - expect { + expect do eval_template_with_args("<%-| $x |-%><%= $x == correct %>", 'x' => 'correct', 'y' => 'surplus') - }.to raise_error(/Too many arguments: 2 for 1/) + end.to raise_error(/Too many arguments: 2 for 1/) end end + context "when using typed parameters" do + it "allows a passed value that matches the parameter's type" do + expect(eval_template_with_args("<%-|String $x|-%><%= $x == correct %>", 'x' => 'correct')).to eq("true") + end + + it "raises an error when the passed value does not match the parameter's type" do + expect do + eval_template_with_args("<%-|Integer $x|-%><%= $x %>", 'x' => 'incorrect') + end.to raise_error(/expected.*Integer.*actual.*String/m) + end + + it "raises an error when the default value does not match the parameter's type" do + expect do + eval_template("<%-|Integer $x = 'nope'|-%><%= $x %>") + end.to raise_error(/expected.*Integer.*actual.*String/m) + end + + it "allows an parameter to default to undef" do + expect(eval_template("<%-|Optional[Integer] $x = undef|-%><%= $x == undef %>")).to eq("true") + end + end + + # although never a problem with epp it "is not interfered with by having a variable named 'string' (#14093)" do scope['string'] = "this output should not be seen" eval_template("some text that is static").should == "some text that is static" end it "has access to a variable named 'string' (#14093)" do scope['string'] = "the string value" eval_template("string was: <%= $string %>").should == "string was: the string value" end describe 'when loading from modules' do include PuppetSpec::Files it 'an epp template is found' do modules_dir = dir_containing('modules', { 'testmodule' => { 'templates' => { 'the_x.epp' => 'The x is <%= $x %>' } }}) Puppet.override({:current_environment => (env = Puppet::Node::Environment.create(:testload, [ modules_dir ]))}, "test") do node.environment = env expect(scope.function_epp([ 'testmodule/the_x.epp', { 'x' => '3'} ])).to eql("The x is 3") end end end def eval_template_with_args(content, args_hash) file_path = tmpdir('epp_spec_content') filename = File.join(file_path, "template.epp") File.open(filename, "w+") { |f| f.write(content) } Puppet::Parser::Files.stubs(:find_template).returns(filename) scope.function_epp(['template', args_hash]) end def eval_template(content) file_path = tmpdir('epp_spec_content') filename = File.join(file_path, "template.epp") File.open(filename, "w+") { |f| f.write(content) } Puppet::Parser::Files.stubs(:find_template).returns(filename) scope.function_epp(['template']) end end