Remove step stuff. Failed experiment for now. Too radical for point release.

This commit is contained in:
Mitchell Hashimoto 2011-12-09 13:40:49 -08:00
parent b5ae4672b7
commit 5bfcbcba66
4 changed files with 0 additions and 664 deletions

View File

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

View File

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

View File

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

View File

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