diff options
-rw-r--r-- | lib/chef/cookbook/syntax_check.rb | 113 | ||||
-rw-r--r-- | spec/unit/cookbook/syntax_check_spec.rb | 1 |
2 files changed, 107 insertions, 7 deletions
diff --git a/lib/chef/cookbook/syntax_check.rb b/lib/chef/cookbook/syntax_check.rb index 59888e2ba3..4f8dcff6dc 100644 --- a/lib/chef/cookbook/syntax_check.rb +++ b/lib/chef/cookbook/syntax_check.rb @@ -17,6 +17,8 @@ # require 'pathname' +require 'stringio' +require 'erubis' require 'chef/mixin/shell_out' require 'chef/mixin/checksum' @@ -161,28 +163,127 @@ class Chef def validate_template(erb_file) Chef::Log.debug("Testing template #{erb_file} for syntax errors...") - result = shell_out("erubis -x #{erb_file} | ruby -c") + if validate_inline? + validate_erb_file_inline(erb_file) + else + validate_erb_via_subcommand(erb_file) + end + end + + def validate_ruby_file(ruby_file) + Chef::Log.debug("Testing #{ruby_file} for syntax errors...") + if validate_inline? + validate_ruby_file_inline(ruby_file) + else + validate_ruby_by_subcommand(ruby_file) + end + end + + # Whether or not we're running on a version of ruby that can support + # inline validation. Inline validation relies on the `RubyVM` features + # introduced with ruby 1.9, so 1.8 cannot be supported. + def validate_inline? + defined?(RubyVM::InstructionSequence) + end + + # Validate the ruby code in an erb template. Uses RubyVM to do syntax + # checking, so callers should check #validate_inline? before calling. + def validate_erb_file_inline(erb_file) + old_stderr = $stderr + + engine = Erubis::Eruby.new + engine.convert!(IO.read(erb_file)) + + ruby_code = engine.src + + # Even when we're compiling the code w/ RubyVM, syntax errors just + # print to $stderr. We want to capture this and handle the printing + # ourselves, so we must temporarily swap $stderr to capture the output. + tmp_stderr = $stderr = StringIO.new + + abs_path = File.expand_path(erb_file) + RubyVM::InstructionSequence.new(ruby_code, erb_file, abs_path, 0) + + true + rescue SyntaxError + $stderr = old_stderr + invalid_erb_file(erb_file, tmp_stderr.string) + false + ensure + # be paranoid about setting stderr back to the old value. + $stderr = old_stderr if defined?(old_stderr) && old_stderr + end + + # Validate the ruby code in an erb template. Pipes the output of `erubis + # -x` to `ruby -c`, so it works with any ruby version, but is much slower + # than the inline version. + # -- + # TODO: This can be removed when ruby 1.8 support is dropped. + def validate_erb_via_subcommand(erb_file) + result = shell_out("erubis -x #{erb_file} | #{ruby} -c") result.error! true rescue Mixlib::ShellOut::ShellCommandFailed + invalid_erb_file(erb_file, result.stderr) + false + end + + # Debug a syntax error in a template. + def invalid_erb_file(erb_file, error_message) file_relative_path = erb_file[/^#{Regexp.escape(cookbook_path+File::Separator)}(.*)/, 1] Chef::Log.fatal("Erb template #{file_relative_path} has a syntax error:") - result.stderr.each_line { |l| Chef::Log.fatal(l.chomp) } + error_message.each_line { |l| Chef::Log.fatal(l.chomp) } + nil + end + + # Validate the syntax of a ruby file. Uses (Ruby 1.9+ only) RubyVM to + # compile the code without evaluating it or spawning a new process. + # Callers should check #validate_inline? before calling. + def validate_ruby_file_inline(ruby_file) + # Even when we're compiling the code w/ RubyVM, syntax errors just + # print to $stderr. We want to capture this and handle the printing + # ourselves, so we must temporarily swap $stderr to capture the output. + old_stderr = $stderr + tmp_stderr = $stderr = StringIO.new + abs_path = File.expand_path(ruby_file) + file_content = IO.read(abs_path) + RubyVM::InstructionSequence.new(file_content, ruby_file, abs_path, 0) + true + rescue SyntaxError + $stderr = old_stderr + invalid_ruby_file(ruby_file, tmp_stderr.string) false + ensure + # be paranoid about setting stderr back to the old value. + $stderr = old_stderr if defined?(old_stderr) && old_stderr end - def validate_ruby_file(ruby_file) - Chef::Log.debug("Testing #{ruby_file} for syntax errors...") - result = shell_out("ruby -c #{ruby_file}") + # Validate the syntax of a ruby file by shelling out to `ruby -c`. Should + # work for all ruby versions, but is slower and uses more resources than + # the inline strategy. + def validate_ruby_by_subcommand(ruby_file) + result = shell_out("#{ruby} -c #{ruby_file}") result.error! true rescue Mixlib::ShellOut::ShellCommandFailed + invalid_ruby_file(ruby_file, result.stderr) + false + end + + # Debugs ruby syntax errors by printing the path to the file and any + # diagnostic info given in +error_message+ + def invalid_ruby_file(ruby_file, error_message) file_relative_path = ruby_file[/^#{Regexp.escape(cookbook_path+File::Separator)}(.*)/, 1] Chef::Log.fatal("Cookbook file #{file_relative_path} has a ruby syntax error:") - result.stderr.each_line { |l| Chef::Log.fatal(l.chomp) } + error_message.each_line { |l| Chef::Log.fatal(l.chomp) } false end + # Returns the full path to the running ruby. + def ruby + Gem.ruby + end + end end end diff --git a/spec/unit/cookbook/syntax_check_spec.rb b/spec/unit/cookbook/syntax_check_spec.rb index 85d6950a45..40a89c99a4 100644 --- a/spec/unit/cookbook/syntax_check_spec.rb +++ b/spec/unit/cookbook/syntax_check_spec.rb @@ -28,7 +28,6 @@ describe Chef::Cookbook::SyntaxCheck do Chef::Log.logger = Logger.new(StringIO.new) Chef::Log.level = :warn # suppress "Syntax OK" messages - @attr_files = %w{default.rb smokey.rb}.map { |f| File.join(cookbook_path, 'attributes', f) } @defn_files = %w{client.rb server.rb}.map { |f| File.join(cookbook_path, 'definitions', f)} @recipes = %w{default.rb gigantor.rb one.rb}.map { |f| File.join(cookbook_path, 'recipes', f) } |