vaguerent/test/unit/vagrant/action/builder_test.rb
Chris Roberts 5d70cc3bf2 Retain original trigger behavior
These updates allow the after trigger to behave the same as the
original with regards to the execution location of the trigger
within the execution stack.
2020-04-03 15:47:00 -07:00

762 lines
20 KiB
Ruby

require File.expand_path("../../../base", __FILE__)
describe Vagrant::Action::Builder do
let(:data) { { data: [] } }
# This returns a proc that can be used with the builder
# that simply appends data to an array in the env.
def appender_proc(data)
result = Proc.new { |env| env[:data] << data }
# Define a to_s on it for helpful output
result.define_singleton_method(:to_s) do
"<Appender: #{data}>"
end
result
end
def wrapper_proc(data)
Class.new do
def initialize(app, env)
@app = app
end
def self.name
"TestAction"
end
define_method(:call) do |env|
env[:data] << "#{data}_in"
@app.call(env)
env[:data] << "#{data}_out"
end
end
end
context "copying" do
it "should copy the stack" do
copy = subject.dup
expect(copy.stack.object_id).not_to eq(subject.stack.object_id)
end
end
context "build" do
it "should provide build as a shortcut for basic sequences" do
data = {}
proc = Proc.new { |env| env[:data] = true }
subject = described_class.build(proc)
subject.call(data)
expect(data[:data]).to eq(true)
end
end
context "basic `use`" do
it "should add items to the stack and make them callable" do
data = {}
proc = Proc.new { |env| env[:data] = true }
subject.use proc
subject.call(data)
expect(data[:data]).to eq(true)
end
it "should be able to add multiple items" do
data = {}
proc1 = Proc.new { |env| env[:one] = true }
proc2 = Proc.new { |env| env[:two] = true }
subject.use proc1
subject.use proc2
subject.call(data)
expect(data[:one]).to eq(true)
expect(data[:two]).to eq(true)
end
it "should be able to add another builder" do
data = {}
proc1 = Proc.new { |env| env[:one] = true }
# Build the first builder
one = described_class.new
one.use proc1
# Add it to this builder
two = described_class.new
two.use one
# Call the 2nd and verify results
two.call(data)
expect(data[:one]).to eq(true)
end
end
context "inserting" do
it "can insert at an index" do
subject.use appender_proc(1)
subject.insert(0, appender_proc(2))
subject.call(data)
expect(data[:data]).to eq([2, 1])
end
it "can insert by name" do
# Create the proc then make sure it has a name
bar_proc = appender_proc(2)
def bar_proc.name; :bar; end
subject.use appender_proc(1)
subject.use bar_proc
subject.insert_before :bar, appender_proc(3)
subject.call(data)
expect(data[:data]).to eq([1, 3, 2])
end
it "can insert next to a previous object" do
proc2 = appender_proc(2)
subject.use appender_proc(1)
subject.use proc2
subject.insert(proc2, appender_proc(3))
subject.call(data)
expect(data[:data]).to eq([1, 3, 2])
end
it "can insert before" do
subject.use appender_proc(1)
subject.insert_before 0, appender_proc(2)
subject.call(data)
expect(data[:data]).to eq([2, 1])
end
it "can insert after" do
subject.use appender_proc(1)
subject.use appender_proc(3)
subject.insert_after 0, appender_proc(2)
subject.call(data)
expect(data[:data]).to eq([1, 2, 3])
end
it "merges middleware stacks of other builders" do
wrapper_class = Proc.new do |letter|
Class.new do
def initialize(app, env)
@app = app
end
def self.name
"TestAction"
end
define_method(:call) do |env|
env[:data] << "#{letter}1"
@app.call(env)
env[:data] << "#{letter}2"
end
end
end
proc2 = appender_proc(2)
subject.use appender_proc(1)
subject.use proc2
builder = described_class.new
builder.use wrapper_class.call("A")
builder.use wrapper_class.call("B")
subject.insert(proc2, builder)
subject.call(data)
expect(data[:data]).to eq([1, "A1", "B1", 2, "B2", "A2"])
end
it "raises an exception if an invalid object given for insert" do
expect { subject.insert "object", appender_proc(1) }.
to raise_error(RuntimeError)
end
it "raises an exception if an invalid object given for insert_after" do
expect { subject.insert_after "object", appender_proc(1) }.
to raise_error(RuntimeError)
end
end
context "replace" do
it "can replace an object" do
proc1 = appender_proc(1)
proc2 = appender_proc(2)
subject.use proc1
subject.replace proc1, proc2
subject.call(data)
expect(data[:data]).to eq([2])
end
it "can replace by index" do
proc1 = appender_proc(1)
proc2 = appender_proc(2)
subject.use proc1
subject.replace 0, proc2
subject.call(data)
expect(data[:data]).to eq([2])
end
end
context "deleting" do
it "can delete by object" do
proc1 = appender_proc(1)
subject.use proc1
subject.use appender_proc(2)
subject.delete proc1
subject.call(data)
expect(data[:data]).to eq([2])
end
it "can delete by index" do
proc1 = appender_proc(1)
subject.use proc1
subject.use appender_proc(2)
subject.delete 0
subject.call(data)
expect(data[:data]).to eq([2])
end
end
describe "action hooks" do
let(:hook) { double("hook") }
let(:manager) { Vagrant.plugin("2").manager }
before do
allow(manager).to receive(:action_hooks).and_return([])
end
it "applies them properly" do
hook_proc = proc{ |h| h.append(appender_proc(2)) }
expect(manager).to receive(:action_hooks).with(:test_action).
and_return([hook_proc])
data[:action_name] = :test_action
subject.use appender_proc(1)
subject.call(data)
expect(data[:data]).to eq([1, 2])
expect(data[:action_hooks_already_ran]).to eq(true)
end
it "applies without prepend/append if it has already" do
hook_proc = proc{ |h| h.append(appender_proc(2)) }
expect(manager).to receive(:action_hooks).with(:test_action).
and_return([hook_proc])
data[:action_name] = :test_action
subject.use appender_proc(1)
subject.call(data.merge(action_hooks_already_ran: true))
expect(data[:data]).to eq([1])
subject.call(data)
end
end
describe "calling another app later" do
it "calls in the proper order" do
# We have to do this because inside the Class.new, it can't see these
# rspec methods...
described_klass = described_class
wrapper_proc = self.method(:wrapper_proc)
wrapper = Class.new do
def initialize(app, env)
@app = app
end
def self.name
"TestAction"
end
define_method(:call) do |env|
inner = described_klass.new
inner.use wrapper_proc[2]
inner.use @app
inner.call(env)
end
end
subject.use wrapper_proc(1)
subject.use wrapper
subject.use wrapper_proc(3)
subject.call(data)
expect(data[:data]).to eq([
"1_in", "2_in", "3_in", "3_out", "2_out", "1_out"])
end
end
describe "dynamic action hooks" do
class ActionOne
def initialize(app, env)
@app = app
end
def call(env)
env[:data] << 1 if env[:data]
@app.call(env)
end
def recover(env)
env[:recover] << 1
end
end
class ActionTwo
def initialize(app, env)
@app = app
end
def call(env)
env[:data] << 2 if env[:data]
@app.call(env)
end
def recover(env)
env[:recover] << 2
end
end
let(:data) { {data: []} }
let(:hook_action_name) { :action_two }
let(:plugin) do
h_name = hook_action_name
@plugin ||= Class.new(Vagrant.plugin("2")) do
name "Test Plugin"
action_hook(:before_test, h_name) do |hook|
hook.prepend(proc{ |env| env[:data] << :first })
end
end
end
before { plugin }
after do
Vagrant.plugin("2").manager.unregister(@plugin) if @plugin
@plugin = nil
end
it "should call hook before running action" do
instance = described_class.build(ActionTwo)
instance.call(data)
expect(data[:data].first).to eq(:first)
expect(data[:data].last).to eq(2)
end
context "when hook is appending to action" do
let(:plugin) do
@plugin ||= Class.new(Vagrant.plugin("2")) do
name "Test Plugin"
action_hook(:before_test, :action_two) do |hook|
hook.append(proc{ |env| env[:data] << :first })
end
end
end
it "should call hook after action when action is nested" do
instance = described_class.build(ActionTwo).use(described_class.build(ActionOne))
instance.call(data)
expect(data[:data][0]).to eq(2)
expect(data[:data][1]).to eq(:first)
expect(data[:data][2]).to eq(1)
end
end
context "when hook uses class name" do
let(:hook_action_name) { "ActionTwo" }
it "should execute the hook" do
instance = described_class.build(ActionTwo)
instance.call(data)
expect(data[:data]).to include(:first)
end
end
context "when action includes a namespace" do
module Vagrant
module Test
class ActionTest
def initialize(app, env)
@app = app
end
def call(env)
env[:data] << :test if env[:data]
@app.call(env)
end
end
end
end
let(:instance) { described_class.build(Vagrant::Test::ActionTest) }
context "when hook uses short snake case name" do
let(:hook_action_name) { :action_test }
it "should execute the hook" do
instance.call(data)
expect(data[:data]).to include(:first)
end
end
context "when hook uses partial snake case name" do
let(:hook_action_name) { :test_action_test }
it "should execute the hook" do
instance.call(data)
expect(data[:data]).to include(:first)
end
end
context "when hook uses full snake case name" do
let(:hook_action_name) { :vagrant_test_action_test }
it "should execute the hook" do
instance.call(data)
expect(data[:data]).to include(:first)
end
end
context "when hook uses short class name" do
let(:hook_action_name) { "ActionTest" }
it "should execute the hook" do
instance.call(data)
expect(data[:data]).to include(:first)
end
end
context "when hook uses partial namespace class name" do
let(:hook_action_name) { "Test::ActionTest" }
it "should execute the hook" do
instance.call(data)
expect(data[:data]).to include(:first)
end
end
context "when hook uses full namespace class name" do
let(:hook_action_name) { "Vagrant::Test::ActionTest" }
it "should execute the hook" do
instance.call(data)
expect(data[:data]).to include(:first)
end
end
end
end
describe "#apply_dynamic_updates" do
let(:env) { {triggers: triggers, machine: machine} }
let(:machine) { nil }
let(:triggers) { nil }
let(:subject) do
@subject ||= described_class.new.tap do |b|
b.use Vagrant::Action::Builtin::EnvSet
b.use Vagrant::Action::Builtin::Confirm
end
end
after { @subject = nil }
it "should not modify the builder stack by default" do
s1 = subject.stack.dup
subject.apply_dynamic_updates(env)
s2 = subject.stack.dup
expect(s1).to eq(s2)
end
context "when an action hooks is defined" do
let(:plugin) do
@plugin ||= Class.new(Vagrant.plugin("2")) do
name "Test Plugin"
action_hook(:before_action, Vagrant::Action::Builtin::Confirm) do |hook|
hook.prepend(Vagrant::Action::Builtin::Call)
end
end
end
before { plugin }
after do
Vagrant.plugin("2").manager.unregister(@plugin) if @plugin
@plugin = nil
end
it "should modify the builder stack" do
s1 = subject.stack.dup
subject.apply_dynamic_updates(env)
s2 = subject.stack.dup
expect(s1).not_to eq(s2)
end
it "should add new action to the middle of the call stack" do
subject.apply_dynamic_updates(env)
expect(subject.stack[1].first).to eq(Vagrant::Action::Builtin::Call)
end
end
context "when triggers are enabled" do
let(:triggers) { double("triggers") }
before do
allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).
with("typed_triggers").and_return(true)
allow(triggers).to receive(:find).and_return([])
end
it "should not modify the builder stack by default" do
s1 = subject.stack.dup
subject.apply_dynamic_updates(env)
s2 = subject.stack.dup
expect(s1).to eq(s2)
end
context "when triggers are found" do
let(:action) { Vagrant::Action::Builtin::EnvSet }
before { expect(triggers).to receive(:find).
with(action, timing, nil, type).and_return([true]) }
context "for action type" do
let(:type) { :action }
context "for before timing" do
let(:timing) { :before }
it "should add trigger action to start of stack" do
subject.apply_dynamic_updates(env)
expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Trigger)
end
it "should have timing and type arguments" do
subject.apply_dynamic_updates(env)
args = subject.stack[0][1]
expect(args).to include(type)
expect(args).to include(timing)
expect(args).to include(action.to_s)
end
end
context "for after timing" do
let(:timing) { :after }
it "should add trigger action to middle of stack" do
subject.apply_dynamic_updates(env)
expect(subject.stack[1].first).to eq(Vagrant::Action::Builtin::Trigger)
end
it "should have timing and type arguments" do
subject.apply_dynamic_updates(env)
args = subject.stack[1][1]
expect(args).to include(type)
expect(args).to include(timing)
expect(args).to include(action.to_s)
end
end
end
context "for hook type" do
let(:type) { :hook }
context "for before timing" do
let(:timing) { :before }
it "should add trigger action to start of stack" do
subject.apply_dynamic_updates(env)
expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Trigger)
end
it "should have timing and type arguments" do
subject.apply_dynamic_updates(env)
args = subject.stack[0][1]
expect(args).to include(type)
expect(args).to include(timing)
expect(args).to include(action.to_s)
end
end
context "for after timing" do
let(:timing) { :after }
it "should add trigger action to middle of stack" do
subject.apply_dynamic_updates(env)
expect(subject.stack[1].first).to eq(Vagrant::Action::Builtin::Trigger)
end
it "should have timing and type arguments" do
subject.apply_dynamic_updates(env)
args = subject.stack[1][1]
expect(args).to include(type)
expect(args).to include(timing)
expect(args).to include(action.to_s)
end
end
end
end
end
end
describe "#apply_action_name" do
let(:env) { {triggers: triggers, machine: machine, action_name: action_name, raw_action_name: raw_action_name} }
let(:raw_action_name) { :up }
let(:action_name) { "machine_#{raw_action_name}".to_sym }
let(:machine) { nil }
let(:triggers) { double("triggers") }
let(:subject) do
@subject ||= described_class.new.tap do |b|
b.use Vagrant::Action::Builtin::EnvSet
b.use Vagrant::Action::Builtin::Confirm
end
end
before { allow(triggers).to receive(:find).and_return([]) }
after { @subject = nil }
it "should mark action hooks applied within env" do
subject.apply_action_name(env)
expect(env[:action_hooks_already_ran]).to be_truthy
end
context "when a plugin has added an action hook" do
let(:plugin) do
@plugin ||= Class.new(Vagrant.plugin("2")) do
name "Test Plugin"
action_hook(:before_action, :machine_up) do |hook|
hook.prepend(Vagrant::Action::Builtin::Call)
end
end
end
before { plugin }
after do
Vagrant.plugin("2").manager.unregister(@plugin) if @plugin
@plugin = nil
end
it "should add new action to the call stack" do
subject.apply_action_name(env)
expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Call)
end
it "should only add new action to the call stack once" do
subject.apply_action_name(env)
subject.apply_action_name(env)
expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Call)
expect(subject.stack[1].first).not_to eq(Vagrant::Action::Builtin::Call)
end
end
context "when trigger has been defined for raw action" do
before do
expect(triggers).to receive(:find).with(raw_action_name, timing, nil, :action).
and_return([true])
end
context "when timing is before" do
let(:timing) { :before }
it "should add a trigger action to the start of the stack" do
subject.apply_action_name(env)
expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Trigger)
end
it "should include arguments to the trigger action" do
subject.apply_action_name(env)
args = subject.stack[0][1]
expect(args).to include(raw_action_name)
expect(args).to include(timing)
expect(args).to include(:action)
end
end
context "when timing is after" do
let(:timing) { :after }
it "should add a trigger action to the end of the stack" do
subject.apply_action_name(env)
expect(subject.stack.first.first).to eq(Vagrant::Action::Builtin::Delayed)
end
it "should include arguments to the trigger action" do
subject.apply_action_name(env)
builder = subject.stack.first[1]&.first
expect(builder).not_to be_nil
args = builder.stack.first[1]
expect(args).to include(raw_action_name)
expect(args).to include(timing)
expect(args).to include(:action)
end
end
end
context "when trigger has been defined for hook" do
before do
allow(Vagrant::Util::Experimental).to receive(:feature_enabled?).
with("typed_triggers").and_return(true)
expect(triggers).to receive(:find).with(action_name, timing, nil, :hook).
and_return([true])
end
context "when timing is before" do
let(:timing) { :before }
it "should add a trigger action to the start of the stack" do
subject.apply_action_name(env)
expect(subject.stack[0].first).to eq(Vagrant::Action::Builtin::Trigger)
end
it "should include arguments to the trigger action" do
subject.apply_action_name(env)
args = subject.stack[0][1]
expect(args).to include(action_name)
expect(args).to include(timing)
expect(args).to include(:hook)
end
end
context "when timing is after" do
let(:timing) { :after }
it "should add a trigger action to the end of the stack" do
subject.apply_action_name(env)
expect(subject.stack.last.first).to eq(Vagrant::Action::Builtin::Trigger)
end
it "should include arguments to the trigger action" do
subject.apply_action_name(env)
args = subject.stack.last[1]
expect(args).to include(action_name)
expect(args).to include(timing)
expect(args).to include(:hook)
end
end
end
end
end