Revamp Step to be more like a Python with-context

This commit is contained in:
Mitchell Hashimoto 2011-12-05 21:05:36 -08:00
parent 60db1c8394
commit c5eae41fd8
2 changed files with 133 additions and 53 deletions

View File

@ -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])

View File

@ -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