diff --git a/lib/vagrant/action/step.rb b/lib/vagrant/action/step.rb index 250bf60ad..3fc606f0a 100644 --- a/lib/vagrant/action/step.rb +++ b/lib/vagrant/action/step.rb @@ -3,13 +3,23 @@ module Vagrant # A step is a callable action that Vagrant uses to build up # more complex actions sequences. # - # A step is really just a reimplementation of a "function" - # within the runtime of Ruby. A step advertises parameters - # that it requires (inputs), as well as return values (outputs). - # The `Step` class handles validating that all the required - # parameters are set on the step as well as can validate the - # return values. + # A step must specify the inputs it requires and the outputs + # it will return, and can implement two methods: `enter` and + # `exit` (both are optional, but a step is useless without + # at least one). + # + # The inputs are guaranteed to be ready only by the time + # `enter` is called and are available as instance variables. + # `enter` is called first. + # + # `exit` is called at some point after `enter` (the exact time + # is not guarateed) and is given one parameter: `error` which + # is non-nil if an exception was raised at some point during + # or after `enter` was called. The return value of `exit` does + # nothing. class Step + class UnsatisfiedRequirementsError < RuntimeError; end + # The keys given to this will be required by the step. Each key # should be a symbol. def self.input(*keys) @@ -55,26 +65,53 @@ module Vagrant # @option options [Boolean] :validate_output Whether to validate the # output or not. # @return [Hash] Output - def call(params={}, options=nil) - options = { - :method => :execute, - :validate_output => true - }.merge(options || {}) - - # Set and validate the inputs - set_inputs(params) - + def call(params={}) # Call the actual implementation - results = send(options[:method]) + results = nil + begin + results = call_enter(params) + rescue UnsatisfiedRequirementsError + # This doesn't get an `exit` call called since enter + # was never even called in this case. + raise + rescue Exception => e + call_exit(e) + raise + end - # Validate the outputs if it is enabled and the list of configured - # outputs is not empty. - results = self.class.process_output(results) if options[:validate_output] + # No exception occurred if we reach this point. Call exit. + call_exit(nil) # Return the final results results end + # This method will only call the `enter` method for the step. + # + # The parameters given here will be validated as the inputs for + # the step and used to call `enter`. The results of `enter` will + # be validated as the outputs and returned. + # + # @param [Hash] inputs + # @return [Hash] + def call_enter(inputs={}) + # Set and validate the inputs + set_inputs(inputs) + + # Call the actual enter call + results = nil + results = send(:enter) if respond_to?(:enter) + + # Validate the outputs if it is enabled and the list of configured + # outputs is not empty. + self.class.process_output(results) + end + + # This method will call `exit` with the given error. + def call_exit(error) + send(:exit, error) if respond_to?(:exit) + end + protected # Sets the parameters for the step. @@ -85,7 +122,7 @@ module Vagrant def set_inputs(params) inputs = self.class.inputs remaining = inputs - params.keys - raise ArgumentError, "Missing parameters: #{remaining}" if !remaining.empty? + raise UnsatisfiedRequirementsError, "Missing parameters: #{remaining}" if !remaining.empty? inputs.each do |key| instance_variable_set("@#{key}", params[key]) diff --git a/test/unit/vagrant/action/step_test.rb b/test/unit/vagrant/action/step_test.rb index 89c63c111..f74e21905 100644 --- a/test/unit/vagrant/action/step_test.rb +++ b/test/unit/vagrant/action/step_test.rb @@ -1,69 +1,112 @@ require File.expand_path("../../../base", __FILE__) describe Vagrant::Action::Step do - it "provides the parameters as instance variables" do - step_class = Class.new(described_class) do - input :foo + describe "call_enter" do + it "calls enter with inputs and returns the outputs" do + step_class = Class.new(described_class) do + input :in + output :out - def execute - return :value => @foo + def enter + return :out => @in * 2 + end end + + step_class.new.call_enter(:in => 12).should == { :out => 24 } end - step_class.new.call(:foo => 12).should == { :value => 12 } - end - - it "raises an exception if not all required parameters are given" do - step_class = Class.new(described_class) do - input :foo - end - - expect { step_class.new.call }.to raise_error(ArgumentError) - end - - it "calls a custom method if given" do - step_class = Class.new(described_class) do - def prepare - return :foo => 12 + it "raises an exception if not all required parameters are given" do + step_class = Class.new(described_class) do + input :foo end + + expect { step_class.new.call_enter }.to raise_error(Vagrant::Action::Step::UnsatisfiedRequirementsError) end - step_class.new.call({}, :method => :prepare).should == { :foo => 12 } - end - - describe "outputs" do it "return an empty hash if no outputs are specified" do step_class = Class.new(described_class) do - def execute + def enter return 12 end end - step_class.new.call.should == {} + step_class.new.call_enter.should == {} end it "raises an exception if missing outputs" do step_class = Class.new(described_class) do output :foo - def execute + def enter return :bar => 12 end end - expect { step_class.new.call }.to raise_error(RuntimeError) + expect { step_class.new.call_enter }.to raise_error(RuntimeError) + end + end + + describe "call_exit" do + it "should simply call the `exit` method with the given argument" do + step_class = Class.new(described_class) do + def exit(error) + raise RuntimeError, error + end + end + + expect { step_class.new.call_exit(7) }.to raise_error(RuntimeError) + end + end + + describe "calling" do + it "calls enter then exit" do + step_class = Class.new(described_class) do + input :obj + + def enter + @obj << "enter" + end + + def exit(error) + @obj << "exit" + end + end + + obj = [] + step_class.new.call(:obj => obj) + obj.should == ["enter", "exit"] end - it "does nothing if missing outputs but we disabled validating" do + it "calls exit with nil if no exception occurred" do step_class = Class.new(described_class) do - output :foo + input :obj - def execute - return :bar => 12 + def exit(error) + @obj << error end end - step_class.new.call({}, :validate_output => false).should == { :bar => 12 } + obj = [] + step_class.new.call(:obj => obj) + obj.should == [nil] + end + + it "calls exit with an exception if it occurred" do + step_class = Class.new(described_class) do + input :obj + + def enter + raise RuntimeError, "foo" + end + + def exit(error) + @obj << error + end + end + + obj = [] + expect { step_class.new.call(:obj => obj) }.to raise_error(RuntimeError) + obj[0].should be_kind_of(RuntimeError) end end end