diff --git a/features/recipe.feature b/features/recipe.feature index b8e85e6..56e4b61 100644 --- a/features/recipe.feature +++ b/features/recipe.feature @@ -9,6 +9,21 @@ Feature: recipe evaluation Then the exit status must be 0 And the output must contain "hello from recipe" + Scenario: reports errors when evaluating an invalid recipe + Given a recipe with: + """ + puts 'OK' + + invalid_keyword + """ + When I execute the recipe + Then the exit status must be 70 + And the output must match: + """ + \AOK + recipe.rb:3:.+invalid recipe keyword `invalid_keyword' + """ + Scenario: source keyword, requires a recipe file Given a recipe with: """ diff --git a/lib/producer/core/cli.rb b/lib/producer/core/cli.rb index 2ac4fde..d59c907 100644 --- a/lib/producer/core/cli.rb +++ b/lib/producer/core/cli.rb @@ -12,17 +12,24 @@ module Producer def run! check_arguments! - evaluate_recipe_file(@arguments[1]) + evaluate_recipe_file end def check_arguments! print_usage_and_exit(64) unless @arguments.length == 2 end - def evaluate_recipe_file(filepath) + def evaluate_recipe_file recipe = Recipe.from_file(@arguments[1]) env = Env.new(recipe) - recipe.evaluate env + begin + recipe.evaluate env + rescue Recipe::RecipeEvaluationError => e + @stdout.puts [e.backtrace.shift, e.message].join ': ' + @stdout.puts e.backtrace + + exit 70 + end end diff --git a/lib/producer/core/recipe.rb b/lib/producer/core/recipe.rb index 6fed222..ab0264c 100644 --- a/lib/producer/core/recipe.rb +++ b/lib/producer/core/recipe.rb @@ -1,6 +1,8 @@ module Producer module Core class Recipe + RecipeEvaluationError = Class.new(StandardError) + attr_reader :code, :filepath def self.from_file(filepath) @@ -29,11 +31,15 @@ module Producer def evaluate(env) if @code - instance_eval @code + instance_eval @code, env.current_recipe.filepath else instance_eval &@block end self + rescue NameError => e + err = RecipeEvaluationError.new("invalid recipe keyword `#{e.name}'") + err.set_backtrace e.backtrace.reject { |l| l =~ /\/producer-core\// } + raise err end diff --git a/spec/fixtures/recipes/invalid.rb b/spec/fixtures/recipes/invalid.rb new file mode 100644 index 0000000..f454281 --- /dev/null +++ b/spec/fixtures/recipes/invalid.rb @@ -0,0 +1,4 @@ +# this recipe will raise a NameError on line 4 when evaluated by the recipe +# DSL. + +invalid_keyword diff --git a/spec/producer/core/cli_spec.rb b/spec/producer/core/cli_spec.rb index 47e7a63..cf4f9f5 100644 --- a/spec/producer/core/cli_spec.rb +++ b/spec/producer/core/cli_spec.rb @@ -20,7 +20,7 @@ module Producer::Core end it 'evaluates the recipe' do - expect(cli).to receive(:evaluate_recipe_file).with(arguments[1]) + expect(cli).to receive(:evaluate_recipe_file) cli.run! end end @@ -50,14 +50,14 @@ module Producer::Core describe '#evaluate_recipe_file' do it 'builds a recipe' do expect(Recipe).to receive(:from_file).with(arguments[1]).and_call_original - cli.evaluate_recipe_file(arguments[1]) + cli.evaluate_recipe_file end it 'builds an environment with the current recipe' do recipe = double('recipe').as_null_object allow(Recipe).to receive(:from_file).and_return(recipe) expect(Env).to receive(:new).with(recipe).and_call_original - cli.evaluate_recipe_file(arguments[1]) + cli.evaluate_recipe_file end it 'evaluates the recipe with the environment' do @@ -66,7 +66,33 @@ module Producer::Core env = double('env') allow(Env).to receive(:new).and_return(env) expect(recipe).to receive(:evaluate).with(env) - cli.evaluate_recipe_file(arguments[1]) + cli.evaluate_recipe_file + end + + context 'error during recipe evaluation' do + let(:arguments) { ['host', fixture_path_for('recipes/invalid.rb')] } + let(:stdout) { StringIO.new } + subject(:cli) { CLI.new(arguments, stdout) } + + it 'exits with a return status of 70' do + expect { cli.evaluate_recipe_file } + .to raise_error(SystemExit) { |e| + expect(e.status).to eq 70 + } + end + + it 'prints the error' do + begin + cli.evaluate_recipe_file + rescue SystemExit + end + expect(stdout.string).to match(/ + \A + #{arguments[1]}:4: + .+ + invalid\srecipe\skeyword\s`invalid_keyword' + /x) + end end end end diff --git a/spec/producer/core/recipe_spec.rb b/spec/producer/core/recipe_spec.rb index 8f45a2c..f5ccdca 100644 --- a/spec/producer/core/recipe_spec.rb +++ b/spec/producer/core/recipe_spec.rb @@ -5,7 +5,7 @@ module Producer::Core include FixturesHelpers let(:code) { 'nil' } - let(:env) { double('env') } + let(:env) { double('env').as_null_object } subject(:recipe) { Recipe.new(code) } describe '.from_file' do @@ -88,6 +88,24 @@ module Producer::Core it 'returns itself' do expect(dsl.evaluate(env)).to eq dsl end + + context 'invalid recipe' do + let(:filepath) { fixture_path_for 'recipes/error.rb' } + let(:recipe) { Recipe.from_file(filepath) } + subject(:dsl) { Recipe::DSL.new File.read(filepath) } + + it 'reports the recipe file path in the error' do + allow(env).to receive(:current_recipe) { recipe } + expect { dsl.evaluate(env) }.to raise_error(RuntimeError) { |e| + expect(e.backtrace.first).to match /\A#{filepath}/ + } + end + + it 'raises a RecipeEvaluationError on NameError' do + dsl = Recipe::DSL.new { incorrect_keyword } + expect { dsl.evaluate(env) }.to raise_error(Recipe::RecipeEvaluationError) + end + end end context 'DSL specific methods' do