summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordanielsdeleo <dan@opscode.com>2014-02-06 13:11:30 -0800
committerdanielsdeleo <dan@opscode.com>2014-02-06 13:11:30 -0800
commitcfca4a26fde79b209542930405026aef1a5a16bb (patch)
tree0428862db64cedc7727ae795080997af3eaa541b
parent96543437ec30d19f2780dc2de1c6520dbe8d3d5d (diff)
parentb8f52adb281cd6a71bedfca285a16f7a0e295aef (diff)
downloadchef-cfca4a26fde79b209542930405026aef1a5a16bb.tar.gz
Merge branch 'inline-syntax-check'
* fixes https://tickets.opscode.com/browse/CHEF-4986 * Improves syntax check speed for Ruby 1.9+, especially when using bundler.
-rw-r--r--lib/chef/cookbook/syntax_check.rb113
-rw-r--r--spec/unit/cookbook/syntax_check_spec.rb1
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) }