diff --git a/lib/puppet/pops/evaluator/epp_evaluator.rb b/lib/puppet/pops/evaluator/epp_evaluator.rb index f8b341868..e5e26efb6 100644 --- a/lib/puppet/pops/evaluator/epp_evaluator.rb +++ b/lib/puppet/pops/evaluator/epp_evaluator.rb @@ -1,88 +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 raise ArgumentError, "inline_epp(): the first argument must be a String with the epp source text, got a #{file.class}" end # Parse and validate the source parser = Puppet::Pops::Parser::EvaluatingParser::EvaluatingEppParser.new begin 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 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}" - require 'debugger'; debugger 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) 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 ArgumentException, "#{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 diff --git a/lib/puppet/pops/parser/evaluating_parser.rb b/lib/puppet/pops/parser/evaluating_parser.rb index ace5b8be0..22fe53720 100644 --- a/lib/puppet/pops/parser/evaluating_parser.rb +++ b/lib/puppet/pops/parser/evaluating_parser.rb @@ -1,196 +1,200 @@ # Does not support "import" and parsing ruby files # class Puppet::Pops::Parser::EvaluatingParser attr_reader :parser def initialize() @parser = Puppet::Pops::Parser::Parser.new() end def parse_string(s, file_source = 'unknown') @file_source = file_source clear() # Handling of syntax error can be much improved (in general), now it bails out of the parser # and does not have as rich information (when parsing a string), need to update it with the file source # (ideally, a syntax error should be entered as an issue, and not just thrown - but that is a general problem # and an improvement that can be made in the eparser (rather than here). # Also a possible improvement (if the YAML parser returns positions) is to provide correct output of position. # begin assert_and_report(parser.parse_string(s)) rescue Puppet::ParseError => e # TODO: This is not quite right, why does not the exception have the correct file? e.file = @file_source unless e.file.is_a?(String) && !e.file.empty? raise e end end def parse_file(file) @file_source = file clear() assert_and_report(parser.parse_file(file)) end def evaluate_string(scope, s, file_source='unknown') evaluate(scope, parse_string(s, file_source)) end def evaluate_file(file) evaluate(parse_file(file)) end def clear() @acceptor = nil end def evaluate(scope, model) return nil unless model ast = Puppet::Pops::Model::AstTransformer.new(@file_source, nil).transform(model) return nil unless ast ast.safeevaluate(scope) end def validate(parse_result) resulting_acceptor = acceptor() validator(resulting_acceptor).validate(parse_result) resulting_acceptor end def acceptor() Puppet::Pops::Validation::Acceptor.new end def validator(acceptor) Puppet::Pops::Validation::ValidatorFactory_3_1.new().validator(acceptor) end def assert_and_report(parse_result) return nil unless parse_result if parse_result.source_ref.nil? or parse_result.source_ref == '' parse_result.source_ref = @file_source end validation_result = validate(parse_result) max_errors = Puppet[:max_errors] max_warnings = Puppet[:max_warnings] + 1 max_deprecations = Puppet[:max_deprecations] + 1 # If there are warnings output them warnings = validation_result.warnings if warnings.size > 0 formatter = Puppet::Pops::Validation::DiagnosticFormatterPuppetStyle.new emitted_w = 0 emitted_dw = 0 validation_result.warnings.each {|w| if w.severity == :deprecation # Do *not* call Puppet.deprecation_warning it is for internal deprecation, not # deprecation of constructs in manifests! (It is not designed for that purpose even if # used throughout the code base). # Puppet.warning(formatter.format(w)) if emitted_dw < max_deprecations emitted_dw += 1 else Puppet.warning(formatter.format(w)) if emitted_w < max_warnings emitted_w += 1 end break if emitted_w > max_warnings && emitted_dw > max_deprecations # but only then } end # If there were errors, report the first found. Use a puppet style formatter. errors = validation_result.errors if errors.size > 0 formatter = Puppet::Pops::Validation::DiagnosticFormatterPuppetStyle.new if errors.size == 1 || max_errors <= 1 # raise immediately raise Puppet::ParseError.new(formatter.format(errors[0])) end emitted = 0 errors.each do |e| Puppet.err(formatter.format(e)) emitted += 1 break if emitted >= max_errors end warnings_message = warnings.size > 0 ? ", and #{warnings.size} warnings" : "" giving_up_message = "Found #{errors.size} errors#{warnings_message}. Giving up" exception = Puppet::ParseError.new(giving_up_message) exception.file = errors[0].file raise exception end parse_result end def quote(x) self.class.quote(x) end # Translates an already parsed string that contains control characters, quotes # and backslashes into a quoted string where all such constructs have been escaped. # Parsing the return value of this method using the puppet parser should yield # exactly the same string as the argument passed to this method # # The method makes an exception for the two character sequences \$ and \s. They # will not be escaped since they have a special meaning in puppet syntax. # # TODO: Handle \uXXXX characters ?? # # @param x [String] The string to quote and "unparse" # @return [String] The quoted string # def self.quote(x) escaped = '"' p = nil x.each_char do |c| case p when nil # do nothing when "\t" escaped << '\\t' when "\n" escaped << '\\n' when "\f" escaped << '\\f' # TODO: \cx is a range of characters - skip for now # when "\c" # escaped << '\\c' when '"' escaped << '\\"' when '\\' escaped << if c == '$' || c == 's'; p; else '\\\\'; end # don't escape \ when followed by s or $ else escaped << p end p = c end escaped << p unless p.nil? escaped << '"' end # This is a temporary solution to making it possible to use the new evaluator. The main class # will eventually have this behavior instead of using transformation to Puppet 3.x AST class Transitional < Puppet::Pops::Parser::EvaluatingParser + def evaluator + @@evaluator ||= Puppet::Pops::Evaluator::EvaluatorImpl.new() + @@evaluator + end + def evaluate(scope, model) return nil unless model - @@evaluator ||= Puppet::Pops::Evaluator::EvaluatorImpl.new() - @@evaluator.evaluate(model, scope) + evaluator.evaluate(model, scope) end def validator(acceptor) Puppet::Pops::Validation::ValidatorFactory_4_0.new().validator(acceptor) end # Create a closure that can be called in the given scope def closure(model, scope) - Puppet::Pops::Evaluator::Closure.new(@@evaluator, model, scope) + Puppet::Pops::Evaluator::Closure.new(evaluator, model, scope) end end class EvaluatingEppParser < Transitional def initialize() @parser = Puppet::Pops::Parser::EppParser.new() end end end diff --git a/spec/unit/parser/functions/epp_spec.rb b/spec/unit/parser/functions/epp_spec.rb new file mode 100644 index 000000000..0f664b08f --- /dev/null +++ b/spec/unit/parser/functions/epp_spec.rb @@ -0,0 +1,62 @@ + +require 'spec_helper' + +describe "the template 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 + 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 + + + def eval_template_with_args(content, args_hash) + File.stubs(:read).with("template.epp").returns(content) + FileTest.stubs(:exist?).with("template.epp").returns(true) + Puppet::Parser::Files.stubs(:find_template).returns("template.epp") + 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 diff --git a/spec/unit/pops/parser/epp_parser_spec.rb b/spec/unit/pops/parser/epp_parser_spec.rb index a91148ed1..951293fd3 100644 --- a/spec/unit/pops/parser/epp_parser_spec.rb +++ b/spec/unit/pops/parser/epp_parser_spec.rb @@ -1,77 +1,78 @@ require 'spec_helper' require 'puppet/pops' require File.join(File.dirname(__FILE__), '/../factory_rspec_helper') module EppParserRspecHelper include FactoryRspecHelper def parse(code) parser = Puppet::Pops::Parser::EppParser.new() parser.parse_string(code) end end describe "epp parser" do include EppParserRspecHelper it "should instantiate an epp parser" do parser = Puppet::Pops::Parser::EppParser.new() parser.class.should == Puppet::Pops::Parser::EppParser end it "should parse a code string and return a program with epp" do parser = Puppet::Pops::Parser::EppParser.new() model = parser.parse_string("Nothing to see here, move along...").current model.class.should == Puppet::Pops::Model::Program - model.body.class.should == Puppet::Pops::Model::EppExpression + model.body.class.should == Puppet::Pops::Model::LambdaExpression + model.body.body.class.should == Puppet::Pops::Model::EppExpression end context "when facing bad input it reports" do it "unbalanced tags" do expect { dump(parse("<% missing end tag")) }.to raise_error(/Unbalanced/) end it "abrupt end" do expect { dump(parse("dum di dum di dum <%")) }.to raise_error(/Unbalanced/) end it "nested epp tags" do expect { dump(parse("<% $a = 10 <% $b = 20 %>%>")) }.to raise_error(/Syntax error/) end it "nested epp expression tags" do expect { dump(parse("<%= 1+1 <%= 2+2 %>%>")) }.to raise_error(/Syntax error/) end end context "handles parsing of" do it "text (and nothing else)" do - dump(parse("Hello World")).should == "(epp (block (render-s 'Hello World')))" + dump(parse("Hello World")).should == "(lambda (epp (block (render-s 'Hello World'))))" end it "template parameters" do - dump(parse("<%($x)%>Hello World")).should == "(epp (parameters x) (block (render-s 'Hello World')))" + dump(parse("<%($x)%>Hello World")).should == "(lambda (parameters x) (epp (block (render-s 'Hello World'))))" end it "template parameters with default" do - dump(parse("<%($x='cigar')%>Hello World")).should == "(epp (parameters (= x 'cigar')) (block (render-s 'Hello World')))" + dump(parse("<%($x='cigar')%>Hello World")).should == "(lambda (parameters (= x 'cigar')) (epp (block (render-s 'Hello World'))))" end it "template parameters with and without default" do - dump(parse("<%($x='cigar', $y)%>Hello World")).should == "(epp (parameters (= x 'cigar') y) (block (render-s 'Hello World')))" + dump(parse("<%($x='cigar', $y)%>Hello World")).should == "(lambda (parameters (= x 'cigar') y) (epp (block (render-s 'Hello World'))))" end it "comments" do - dump(parse("<%#($x='cigar', $y)%>Hello World")).should == "(epp (block (render-s 'Hello World')))" + dump(parse("<%#($x='cigar', $y)%>Hello World")).should == "(lambda (epp (block (render-s 'Hello World'))))" end it "verbatim epp tags" do - dump(parse("<%% contemplating %%>Hello World")).should == "(epp (block (render-s '<% contemplating %>Hello World')))" + dump(parse("<%% contemplating %%>Hello World")).should == "(lambda (epp (block (render-s '<% contemplating %>Hello World'))))" end it "expressions" do dump(parse("We all live in <%= 3.14 - 2.14 %> world")).should == - "(epp (block (render-s 'We all live in ') (render (- 3.14 2.14)) (render-s ' world')))" + "(lambda (epp (block (render-s 'We all live in ') (render (- 3.14 2.14)) (render-s ' world'))))" end end end