Remove step stuff. Failed experiment for now. Too radical for point release.
This commit is contained in:
parent
b5ae4672b7
commit
5bfcbcba66
|
@ -1,186 +0,0 @@
|
|||
module Vagrant
|
||||
class Action
|
||||
class MultiStep < Step
|
||||
# This class is used as a placeholder to represent a parameter that
|
||||
# needs to be replaced at runtime. For example: A step might take a
|
||||
# parameter B that is outputted as A from a previous step. An instance
|
||||
# of this class can represent that the previous A should be remapped
|
||||
# to this new B.
|
||||
class Param
|
||||
attr_reader :name
|
||||
attr_reader :variable
|
||||
|
||||
def initialize(name, variable)
|
||||
@name = name
|
||||
@variable = variable
|
||||
end
|
||||
end
|
||||
|
||||
# Represents a remapping that comes from the group inputs.
|
||||
class GroupInput < Param; end
|
||||
|
||||
# Represents a remapping that comes from the output of a previous step.
|
||||
class StepOutput < Param; end
|
||||
|
||||
def initialize
|
||||
@step_names = []
|
||||
@steps = {}
|
||||
end
|
||||
|
||||
# This returns a custom object that represents an input parameter
|
||||
# given to this group.
|
||||
#
|
||||
# @param [Symbol] key Parameter name of the input to use from the group.
|
||||
# @return [GroupInput] A `param` type that can be used for remappings.
|
||||
def input(key)
|
||||
return GroupInput.new(:global, key)
|
||||
end
|
||||
|
||||
# This returns a custom object that represents an output parameter
|
||||
# from another step in this group.
|
||||
#
|
||||
# @param [Object] name Name of the step. This is either an explicit name
|
||||
# like a symbol or the class for the step if it is unique.
|
||||
# @param [Symbol] output The output variable name from the step.
|
||||
# @return [StepOutput] A `param` type that can be used for remappings.
|
||||
def output(name, output)
|
||||
return StepOutput.new(name, output)
|
||||
end
|
||||
|
||||
def step(step_class, *extra_inputs)
|
||||
# Determine the name for this step.
|
||||
step_name = nil
|
||||
if step_class.is_a?(Symbol)
|
||||
step_name = step_class
|
||||
step_class = extra_inputs.shift
|
||||
else
|
||||
step_name = step_class
|
||||
end
|
||||
|
||||
if @steps.has_key?(step_name)
|
||||
raise NameError, "Step with name #{step_name} already exists."
|
||||
elsif !step_class.is_a?(Class)
|
||||
raise ArgumentError, "Step class must be a class."
|
||||
end
|
||||
|
||||
# Get the options hash and set the defaults
|
||||
maps = {}
|
||||
maps = extra_inputs.pop if extra_inputs.last.kind_of?(Hash)
|
||||
|
||||
# Go over each extra input and handle the mapping. This is typically
|
||||
# syntactic sugar.
|
||||
extra_inputs.each do |direct|
|
||||
if direct.is_a?(Symbol)
|
||||
# Symbols are assumed to be inputs to this group
|
||||
direct = input(direct)
|
||||
end
|
||||
|
||||
maps[direct] = direct.variable
|
||||
end
|
||||
|
||||
# Take the missing inputs and map them together. For example
|
||||
# the input of ':a' maps directly to the previous output of ':a'
|
||||
(step_class.inputs - maps.values).each do |input|
|
||||
maps[input] = input
|
||||
end
|
||||
|
||||
# Turn the pure symbols into mapping from last steps
|
||||
maps.keys.each do |from|
|
||||
if from.kind_of?(Symbol)
|
||||
new_from = nil
|
||||
if @step_names.last.nil?
|
||||
new_from = input(from)
|
||||
else
|
||||
new_from = output(@step_names.last, from)
|
||||
end
|
||||
|
||||
maps[new_from] = maps.delete(from)
|
||||
end
|
||||
end
|
||||
|
||||
# Verify that all the mappings are correctly satisfied.
|
||||
maps.each do |from, to|
|
||||
if !mapping_satisfied?(step_class, from, to)
|
||||
raise ArgumentError, "Mapping from #{from.variable} to #{to} fails."
|
||||
end
|
||||
end
|
||||
|
||||
# Append the step
|
||||
@step_names << step_name
|
||||
@steps[step_name] = [step_class, maps]
|
||||
end
|
||||
|
||||
def call(params=nil)
|
||||
params ||= {}
|
||||
|
||||
# Instantiate all the steps
|
||||
steps = @step_names.map do |name|
|
||||
step_class, maps = @steps[name]
|
||||
[name, step_class.new, maps]
|
||||
end
|
||||
|
||||
# For each step, call it with proper inputs, using the output
|
||||
# of that call as inputs to the next.
|
||||
entered_steps = []
|
||||
step_outputs = {}
|
||||
result = steps.inject(params) do |inputs, data|
|
||||
name, step, mappings = data
|
||||
|
||||
# If we have inputs to remap, remap them.
|
||||
mappings.each do |from, to|
|
||||
if from.kind_of?(GroupInput)
|
||||
# Group inputs get their data from the initial parameters given
|
||||
# to this group.
|
||||
inputs[to] = params[from.variable]
|
||||
elsif from.kind_of?(StepOutput)
|
||||
# Step outputs get their data from a previous step's output.
|
||||
inputs[to] = step_outputs[from.name][from.variable]
|
||||
else
|
||||
raise ArgumentError, "Unknown type of remapping."
|
||||
end
|
||||
end
|
||||
|
||||
# Call the actual step, using the results for the next
|
||||
# iteration.
|
||||
entered_steps << step
|
||||
begin
|
||||
step_outputs[name] = step.call_enter(inputs)
|
||||
rescue Exception => e
|
||||
entered_steps.reverse.each do |s|
|
||||
s.call_exit(e)
|
||||
end
|
||||
|
||||
raise
|
||||
end
|
||||
|
||||
# Return a shallow dup of the results
|
||||
step_outputs[name].dup
|
||||
end
|
||||
|
||||
entered_steps.reverse.each do |s|
|
||||
s.call_exit(nil)
|
||||
end
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def mapping_satisfied?(step, from, to)
|
||||
# If the step inputs don't contain to then we fail
|
||||
return false if !step.inputs.include?(to)
|
||||
|
||||
if from.kind_of?(GroupInput)
|
||||
# We assume that inputs from the group are satisfied since
|
||||
# it has to come in.
|
||||
return true
|
||||
elsif from.kind_of?(StepOutput)
|
||||
# Verify that the outputs of the step exist to map.
|
||||
return @steps[from.name][0].outputs.include?(from.variable)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,133 +0,0 @@
|
|||
module Vagrant
|
||||
class Action
|
||||
# A step is a callable action that Vagrant uses to build up
|
||||
# more complex actions sequences.
|
||||
#
|
||||
# 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)
|
||||
inputs.concat(keys)
|
||||
end
|
||||
|
||||
# The values that this step provides. Each key should be a symbol.
|
||||
def self.output(*keys)
|
||||
outputs.concat(keys)
|
||||
end
|
||||
|
||||
# This is the array of required keys.
|
||||
def self.inputs
|
||||
@inputs ||= []
|
||||
end
|
||||
|
||||
# The array of keys that are provided by this Step.
|
||||
def self.outputs
|
||||
@outputs ||= []
|
||||
end
|
||||
|
||||
# Validates that the output matches the specification provided, and
|
||||
# raises a RuntimeError if it does not.
|
||||
def self.process_output(value)
|
||||
# The return value must be a Hash, so we just coerce it to that.
|
||||
value = {} if !value.kind_of?(Hash)
|
||||
|
||||
# Verify that we have all the outputs
|
||||
missing = outputs - value.keys
|
||||
raise RuntimeError, "Missing output keys: #{missing}" if !missing.empty?
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
# This calls the step with the given parameters, and returns a hash
|
||||
# of the outputs.
|
||||
#
|
||||
# Additional options may be provided via the options hash at the end.
|
||||
#
|
||||
# @param [Hash] params Parameters for the step.
|
||||
# @param [Hash] options Options hash
|
||||
# @option options [Symbol] :method Method to execute.
|
||||
# @option options [Boolean] :validate_output Whether to validate the
|
||||
# output or not.
|
||||
# @return [Hash] Output
|
||||
def call(params={})
|
||||
# Call the actual implementation
|
||||
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
|
||||
|
||||
# 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.
|
||||
#
|
||||
# This will raise an exception if all the `requires` are not
|
||||
# properly met. Otherwise, the parameters are set as instance variables
|
||||
# on this instance.
|
||||
def set_inputs(params)
|
||||
inputs = self.class.inputs
|
||||
remaining = inputs - params.keys
|
||||
raise UnsatisfiedRequirementsError, "Missing parameters: #{remaining}" if !remaining.empty?
|
||||
|
||||
inputs.each do |key|
|
||||
instance_variable_set("@#{key}", params[key])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,233 +0,0 @@
|
|||
require File.expand_path("../../../base", __FILE__)
|
||||
|
||||
describe Vagrant::Action::MultiStep do
|
||||
it "should compose a series of steps" do
|
||||
step_A = Class.new(Vagrant::Action::Step) do
|
||||
input :obj
|
||||
output :obj
|
||||
|
||||
def enter
|
||||
@obj << "A"
|
||||
return :obj => @obj
|
||||
end
|
||||
end
|
||||
|
||||
step_B = Class.new(Vagrant::Action::Step) do
|
||||
input :obj
|
||||
output :result
|
||||
|
||||
def enter
|
||||
return :result => (@obj << "B")
|
||||
end
|
||||
end
|
||||
|
||||
obj = []
|
||||
|
||||
ms = described_class.new
|
||||
ms.step step_A
|
||||
ms.step step_B
|
||||
ms.call(:obj => obj).should == { :result => ["A", "B"] }
|
||||
end
|
||||
|
||||
it "should allow for custom inputs to pass to specific steps" do
|
||||
step_A = Class.new(Vagrant::Action::Step) do
|
||||
def enter
|
||||
# Do nothing.
|
||||
end
|
||||
end
|
||||
|
||||
step_B = Class.new(Vagrant::Action::Step) do
|
||||
input :obj
|
||||
|
||||
def enter
|
||||
@obj << "B"
|
||||
end
|
||||
end
|
||||
|
||||
obj = []
|
||||
|
||||
ms = described_class.new
|
||||
ms.step step_A
|
||||
ms.step step_B, :obj
|
||||
ms.call(:obj => obj)
|
||||
|
||||
obj.should == ["B"]
|
||||
end
|
||||
|
||||
it "should be able to remap input names" do
|
||||
step_A = Class.new(Vagrant::Action::Step) do
|
||||
output :foo
|
||||
|
||||
def enter
|
||||
return :foo => "A"
|
||||
end
|
||||
end
|
||||
|
||||
step_B = Class.new(Vagrant::Action::Step) do
|
||||
input :from
|
||||
output :value
|
||||
|
||||
def enter
|
||||
return :value => @from
|
||||
end
|
||||
end
|
||||
|
||||
obj = []
|
||||
|
||||
ms = described_class.new
|
||||
ms.step step_A
|
||||
ms.step step_B, :foo => :from
|
||||
ms.call.should == { :value => "A" }
|
||||
end
|
||||
|
||||
it "should be able to reference variables from steps before other steps" do
|
||||
step_A = Class.new(Vagrant::Action::Step) do
|
||||
output :foo
|
||||
|
||||
def enter
|
||||
return :foo => 10
|
||||
end
|
||||
end
|
||||
|
||||
step_B = Class.new(Vagrant::Action::Step) do
|
||||
input :from
|
||||
output :value
|
||||
|
||||
def enter
|
||||
return :value => @from
|
||||
end
|
||||
end
|
||||
|
||||
step_C = Class.new(Vagrant::Action::Step) do
|
||||
input :number
|
||||
output :value
|
||||
|
||||
def enter
|
||||
return :value => @number * 2
|
||||
end
|
||||
end
|
||||
|
||||
obj = []
|
||||
|
||||
g = described_class.new
|
||||
g.step step_A
|
||||
g.step step_B, :foo => :from
|
||||
g.step step_C, g.output(step_A, :foo) => :number
|
||||
g.call.should == { :value => 20 }
|
||||
end
|
||||
|
||||
it "should error if multiple steps of the same class are given without explicit names" do
|
||||
step_A = Class.new(Vagrant::Action::Step)
|
||||
|
||||
g = described_class.new
|
||||
g.step step_A
|
||||
expect { g.step step_A }.to raise_error(NameError)
|
||||
end
|
||||
|
||||
it "should not error if multiple steps of the same class are given with custom names" do
|
||||
step_A = Class.new(Vagrant::Action::Step)
|
||||
|
||||
g = described_class.new
|
||||
g.step step_A
|
||||
expect { g.step :another, step_A }.to_not raise_error
|
||||
end
|
||||
|
||||
it "should error if a step is not a class" do
|
||||
g = described_class.new
|
||||
expect { g.step :foo }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it "should not allow a step that doesn't have all inputs satisfied" do
|
||||
step_A = Class.new(Vagrant::Action::Step) do
|
||||
output :output_A
|
||||
end
|
||||
|
||||
step_B = Class.new(Vagrant::Action::Step) do
|
||||
input :input_B
|
||||
end
|
||||
|
||||
g = described_class.new
|
||||
g.step step_A
|
||||
expect { g.step step_B }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it "should not allow remapping from outputs that don't exist" do
|
||||
step_A = Class.new(Vagrant::Action::Step) do
|
||||
output :output_A
|
||||
end
|
||||
|
||||
step_B = Class.new(Vagrant::Action::Step) do
|
||||
input :input_B
|
||||
end
|
||||
|
||||
g = described_class.new
|
||||
g.step step_A
|
||||
expect { g.step step_B, :output_B => :input_B }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it "should not allow remapping to inputs that don't exist" do
|
||||
step_A = Class.new(Vagrant::Action::Step) do
|
||||
input :input_A
|
||||
end
|
||||
|
||||
g = described_class.new
|
||||
expect { g.step step_A, g.input(:foo) => :input_B }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it "should call the enter methods in order, and the exit in reverse order" do
|
||||
step = Class.new(Vagrant::Action::Step) do
|
||||
input :key
|
||||
input :data
|
||||
output :data
|
||||
|
||||
def enter
|
||||
@data << @key
|
||||
return :data => @data
|
||||
end
|
||||
|
||||
def exit(error)
|
||||
@data << @key
|
||||
end
|
||||
end
|
||||
|
||||
g = described_class.new
|
||||
g.step step
|
||||
g.step :two, step, g.input(:key2) => :key
|
||||
g.step :three, step, g.input(:key3) => :key
|
||||
result = g.call(:data => [], :key => "1", :key2 => "2", :key3 => "3")
|
||||
result[:data].should == %W[1 2 3 3 2 1]
|
||||
end
|
||||
|
||||
it "should halt the steps and call exit with the error if an error occurs" do
|
||||
step = Class.new(Vagrant::Action::Step) do
|
||||
input :key
|
||||
input :data
|
||||
output :data
|
||||
|
||||
def enter
|
||||
@data << @key
|
||||
raise Exception, "E" if @data.last == "2"
|
||||
return :data => @data
|
||||
end
|
||||
|
||||
def exit(error)
|
||||
prefix = error ? error.message : ""
|
||||
@data << "#{prefix}#{@key}"
|
||||
end
|
||||
end
|
||||
|
||||
g = described_class.new
|
||||
g.step step
|
||||
g.step :two, step, g.input(:key2) => :key
|
||||
g.step :three, step, g.input(:key3) => :key
|
||||
|
||||
# Run the actual steps
|
||||
data = []
|
||||
expect do
|
||||
result = g.call(:data => data, :key => "1", :key2 => "2", :key3 => "3")
|
||||
end.to raise_error(Exception)
|
||||
|
||||
# Verify the result hit the methods in the proper order
|
||||
data.should == %W[1 2 E2 E1]
|
||||
end
|
||||
end
|
|
@ -1,112 +0,0 @@
|
|||
require File.expand_path("../../../base", __FILE__)
|
||||
|
||||
describe Vagrant::Action::Step do
|
||||
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 enter
|
||||
return :out => @in * 2
|
||||
end
|
||||
end
|
||||
|
||||
step_class.new.call_enter(:in => 12).should == { :out => 24 }
|
||||
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_enter }.to raise_error(Vagrant::Action::Step::UnsatisfiedRequirementsError)
|
||||
end
|
||||
|
||||
it "return an empty hash if no outputs are specified" do
|
||||
step_class = Class.new(described_class) do
|
||||
def enter
|
||||
return 12
|
||||
end
|
||||
end
|
||||
|
||||
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 enter
|
||||
return :bar => 12
|
||||
end
|
||||
end
|
||||
|
||||
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 "calls exit with nil if no exception occurred" do
|
||||
step_class = Class.new(described_class) do
|
||||
input :obj
|
||||
|
||||
def exit(error)
|
||||
@obj << error
|
||||
end
|
||||
end
|
||||
|
||||
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
|
Loading…
Reference in New Issue