diff --git a/lib/vagrant/action.rb b/lib/vagrant/action.rb index a646109c2..31a910c04 100644 --- a/lib/vagrant/action.rb +++ b/lib/vagrant/action.rb @@ -16,6 +16,7 @@ module Vagrant autoload :CleanupDisks, "vagrant/action/builtin/cleanup_disks" autoload :Confirm, "vagrant/action/builtin/confirm" autoload :ConfigValidate, "vagrant/action/builtin/config_validate" + autoload :Delayed, "vagrant/action/builtin/delayed" autoload :DestroyConfirm, "vagrant/action/builtin/destroy_confirm" autoload :Disk, "vagrant/action/builtin/disk" autoload :EnvSet, "vagrant/action/builtin/env_set" @@ -35,6 +36,7 @@ module Vagrant autoload :SSHRun, "vagrant/action/builtin/ssh_run" autoload :SyncedFolders, "vagrant/action/builtin/synced_folders" autoload :SyncedFolderCleanup, "vagrant/action/builtin/synced_folder_cleanup" + autoload :Trigger, "vagrant/action/builtin/trigger" autoload :WaitForCommunicator, "vagrant/action/builtin/wait_for_communicator" end diff --git a/lib/vagrant/action/builder.rb b/lib/vagrant/action/builder.rb index 5adc635f8..356e3dace 100644 --- a/lib/vagrant/action/builder.rb +++ b/lib/vagrant/action/builder.rb @@ -122,6 +122,7 @@ module Vagrant # @return [Integer] def index(object) stack.each_with_index do |item, i| + return i if item == object return i if item[0] == object return i if item[0].respond_to?(:name) && item[0].name == object end @@ -132,42 +133,162 @@ module Vagrant # Converts the builder stack to a runnable action sequence. # # @param [Hash] env The action environment hash - # @return [Object] A callable object + # @return [Warden] A callable object def to_app(env) - app_stack = nil + # Start with a duplicate of ourself which can + # be modified + builder = self.dup - # If we have action hooks, then we apply them - if env[:action_hooks] - builder = self.dup + # Apply all dynamic modifications of the stack. This + # will generate dynamic hooks for all actions within + # the stack, load any triggers for action classes, and + # apply them to the builder's stack + builder.apply_dynamic_updates(env) - # These are the options to pass into hook application. - options = {} - - # If we already ran through once and did append/prepends, - # then don't do it again. - if env[:action_hooks_already_ran] - options[:no_prepend_or_append] = true - end - - # Specify that we already ran, so in the future we don't repeat - # the prepend/append hooks. - env[:action_hooks_already_ran] = true - - # Apply all the hooks to the new builder instance - env[:action_hooks].each do |hook| - hook.apply(builder, options) - end - - # The stack is now the result of the new builder - app_stack = builder.stack.dup - end - - # If we don't have a stack then default to using our own - app_stack ||= stack.dup + # Now that the stack is fully expanded, apply any + # action hooks that may be defined so they are on + # the outermost locations of the stack + builder.apply_action_name(env) # Wrap the middleware stack with the Warden to provide a consistent # and predictable behavior upon exceptions. - Warden.new(app_stack, env) + Warden.new(builder.stack.dup, env) + end + + # Find any action hooks or triggers which have been defined + # for items within the stack. Update the stack with any + # hooks or triggers found. + # + # @param [Hash] env Call environment + # @return [Builder] self + def apply_dynamic_updates(env) + if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") + triggers = env[:triggers] + end + + # Use a Hook as a convenient interface for injecting + # any applicable trigger actions within the stack + machine_name = env[:machine].name if env[:machine] + + # Iterate over all items in the stack and apply new items + # into the hook as they are found. Must be sure to dup the + # stack here since we are modifying the stack in the loop. + stack.dup.each do |item| + hook = Hook.new + + action = item.first + next if action.is_a?(Proc) + + # Start with adding any action triggers that may be defined + if triggers && !triggers.find(action, :before, machine_name, :action).empty? + hook.prepend(Vagrant::Action::Builtin::Trigger, + action.name, triggers, :before, :action) + end + + if triggers && !triggers.find(action, :after, machine_name, :action).empty? + hook.append(Vagrant::Action::Builtin::Trigger, + action.name, triggers, :after, :action) + end + + # Next look for any hook triggers that may be defined against + # the dynamically generated action class hooks + if triggers && !triggers.find(action, :before, machine_name, :hook).empty? + hook.prepend(Vagrant::Action::Builtin::Trigger, + action.name, triggers, :before, :hook) + end + + if triggers && !triggers.find(action, :after, machine_name, :hook).empty? + hook.append(Vagrant::Action::Builtin::Trigger, + action.name, triggers, :after, :hook) + end + + # Finally load any registered hooks for dynamically generated + # action class based hooks + Vagrant.plugin("2").manager.find_action_hooks(action).each do |hook_proc| + hook_proc.call(hook) + end + + hook.apply(self, root: item) + end + + # Apply the hook to ourself to update the stack + self + end + + # If action hooks have not already been set, this method + # will perform three tasks: + # 1. Load any hook triggers defined for the action_name + # 2. Load any action_hooks defined from plugins + # 3. Load any action triggers based on machine action called (not action classes) + # + # @param [Hash] env Call environment + # @return [Builder] + def apply_action_name(env) + return self if !env[:action_name] + hook = Hook.new + machine_name = env[:machine].name if env[:machine] + + # Start with loading any hook triggers if applicable + if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") && env[:triggers] + if !env[:triggers].find(env[:action_name], :before, machine_name, :hook).empty? + hook.prepend(Vagrant::Action::Builtin::Trigger, + env[:action_name], env[:triggers], :before, :hook) + end + if !env[:triggers].find(env[:action_name], :after, machine_name, :hook).empty? + hook.append(Vagrant::Action::Builtin::Trigger, + env[:action_name], env[:triggers], :after, :hook) + end + end + + # Next we load up all the action hooks that plugins may + # have defined + action_hooks = Vagrant.plugin("2").manager.action_hooks(env[:action_name]) + action_hooks.each do |hook_proc| + hook_proc.call(hook) + end + + # Finally load any action triggers defined. The action triggers + # are the originally implemented trigger style. They run before + # and after specific provider actions (like :up, :halt, etc) and + # are different from true action triggers + if env[:triggers] + if !env[:triggers].find(env[:raw_action_name], :before, machine_name, :action).empty? + hook.prepend(Vagrant::Action::Builtin::Trigger, + env[:raw_action_name], env[:triggers], :before, :action) + end + if !env[:triggers].find(env[:raw_action_name], :after, machine_name, :action).empty? + # NOTE: These after triggers need to be delayed before running to + # allow the rest of the call stack to complete before being + # run. The delayed action is prepended to the stack (not appended) + # to ensure it is called first, which results in it properly + # waiting for everything to finish before itself completing. + builder = self.class.build(Vagrant::Action::Builtin::Trigger, + env[:raw_action_name], env[:triggers], :after, :action) + hook.prepend(Vagrant::Action::Builtin::Delayed, builder) + end + end + + # If the hooks are empty, then there was nothing to apply and + # we can just send ourself back + return self if hook.empty? + + # These are the options to pass into hook application. + options = {} + + # If we already ran through once and did append/prepends, + # then don't do it again. + if env[:action_hooks_already_ran] + options[:no_prepend_or_append] = true + end + + # Specify that we already ran, so in the future we don't repeat + # the prepend/append hooks. + env[:action_hooks_already_ran] = true + + # Apply all the hooks to the new builder instance + hook.apply(self, options) + + self end end end diff --git a/lib/vagrant/action/builtin/after_trigger.rb b/lib/vagrant/action/builtin/after_trigger.rb deleted file mode 100644 index 106d10df1..000000000 --- a/lib/vagrant/action/builtin/after_trigger.rb +++ /dev/null @@ -1,31 +0,0 @@ -module Vagrant - module Action - module Builtin - # This class is intended to be used by the Action::Warden class for executing - # action triggers before any given action. - # - # @param [Symbol] action_name - name to fire trigger on - # @param [Vagrant::Plugin::V2::Triger] triggers - trigger object - class AfterTriggerAction - # @param [Symbol] action_name - The action class name to fire trigger on - # @param [Vagrant::Plugin::V2::Triger] triggers - trigger object - def initialize(app, env, action_name, triggers) - @app = app - @env = env - @triggers = triggers - @action_name = action_name - end - - def call(env) - machine = env[:machine] - machine_name = machine.name if machine - - @triggers.fire_triggers(@action_name, :after, machine_name, :action) if Vagrant::Util::Experimental.feature_enabled?("typed_triggers"); - - # Carry on - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/builtin/before_trigger.rb b/lib/vagrant/action/builtin/before_trigger.rb deleted file mode 100644 index fad2e5799..000000000 --- a/lib/vagrant/action/builtin/before_trigger.rb +++ /dev/null @@ -1,28 +0,0 @@ -module Vagrant - module Action - module Builtin - # This class is intended to be used by the Action::Warden class for executing - # action triggers before any given action. - class BeforeTriggerAction - # @param [Symbol] action_name - The action class name to fire trigger on - # @param [Vagrant::Plugin::V2::Triger] triggers - trigger object - def initialize(app, env, action_name, triggers) - @app = app - @env = env - @triggers = triggers - @action_name = action_name - end - - def call(env) - machine = env[:machine] - machine_name = machine.name if machine - - @triggers.fire_triggers(@action_name, :before, machine_name, :action) if Vagrant::Util::Experimental.feature_enabled?("typed_triggers"); - - # Carry on - @app.call(env) - end - end - end - end -end diff --git a/lib/vagrant/action/builtin/delayed.rb b/lib/vagrant/action/builtin/delayed.rb new file mode 100644 index 000000000..d91d1eebe --- /dev/null +++ b/lib/vagrant/action/builtin/delayed.rb @@ -0,0 +1,26 @@ +module Vagrant + module Action + module Builtin + # This class is used to delay execution until the end of + # a configured stack + class Delayed + # @param [Object] callable The object to call (must respond to #call) + def initialize(app, env, callable) + if !callable.respond_to?(:call) + raise TypeError, "Callable argument is expected to respond to `#call`" + end + @app = app + @env = env + @callable = callable + end + + def call(env) + # Allow the rest of the call stack to execute + @app.call(env) + # Now call our delayed stack + @callable.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/builtin/trigger.rb b/lib/vagrant/action/builtin/trigger.rb new file mode 100644 index 000000000..0591ea7b4 --- /dev/null +++ b/lib/vagrant/action/builtin/trigger.rb @@ -0,0 +1,36 @@ +module Vagrant + module Action + module Builtin + # This class is used within the Builder class for injecting triggers into + # different parts of the call stack. + class Trigger + # @param [Class, String, Symbol] name Name of trigger to fire + # @param [Vagrant::Plugin::V2::Triger] triggers Trigger object + # @param [Symbol] timing When trigger should fire (:before/:after) + # @param [Symbol] type Type of trigger + def initialize(app, env, name, triggers, timing, type=:action) + @app = app + @env = env + @triggers = triggers + @name = name + @timing = timing + @type = type + + if ![:before, :after].include?(timing) + raise ArgumentError, + "Invalid value provided for `timing` (allowed: :before or :after)" + end + end + + def call(env) + machine = env[:machine] + machine_name = machine.name if machine + + @triggers.fire(@name, @timing, machine_name, @type) + # Carry on + @app.call(env) + end + end + end + end +end diff --git a/lib/vagrant/action/hook.rb b/lib/vagrant/action/hook.rb index 21881e8ba..3ab308085 100644 --- a/lib/vagrant/action/hook.rb +++ b/lib/vagrant/action/hook.rb @@ -65,6 +65,14 @@ module Vagrant @prepend_hooks << [new, args, block] end + # @return [Boolean] + def empty? + before_hooks.empty? && + after_hooks.empty? && + prepend_hooks.empty? && + append_hooks.empty? + end + # This applies the given hook to a builder. This should not be # called directly. # @@ -75,12 +83,22 @@ module Vagrant if !options[:no_prepend_or_append] # Prepends first @prepend_hooks.each do |klass, args, block| - builder.insert(0, klass, *args, &block) + if options[:root] + idx = builder.index(options[:root]) + else + idx = 0 + end + builder.insert(idx, klass, *args, &block) end # Appends @append_hooks.each do |klass, args, block| - builder.use(klass, *args, &block) + if options[:root] + idx = builder.index(options[:root]) + builder.insert(idx + 1, klass, *args, &block) + else + builder.use(klass, *args, &block) + end end end diff --git a/lib/vagrant/action/runner.rb b/lib/vagrant/action/runner.rb index f125617b1..dd36588e6 100644 --- a/lib/vagrant/action/runner.rb +++ b/lib/vagrant/action/runner.rb @@ -34,30 +34,19 @@ module Vagrant environment.merge!(@lazy_globals.call) if @lazy_globals environment.merge!(options || {}) - if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") - # NOTE: Triggers are initialized later in the Action::Runer because of - # how `@lazy_globals` are evaluated. Rather than trying to guess where - # the `env` is coming from, we can wait until they're merged into a single - # hash above. - env = environment[:env] - machine = environment[:machine] - machine_name = machine.name if machine + # NOTE: Triggers are initialized later in the Action::Runer because of + # how `@lazy_globals` are evaluated. Rather than trying to guess where + # the `env` is coming from, we can wait until they're merged into a single + # hash above. + env = environment[:env] + machine = environment[:machine] + environment[:triggers] = machine.triggers if machine + + if env ui = Vagrant::UI::Prefixed.new(env.ui, "vagrant") - triggers = Vagrant::Plugin::V2::Trigger.new(env, env.vagrantfile.config.trigger, machine, ui) - end - - # Setup the action hooks - hooks = Vagrant.plugin("2").manager.action_hooks(environment[:action_name]) - if !hooks.empty? - @logger.info("Preparing hooks for middleware sequence...") - environment[:action_hooks] = hooks.map do |hook_proc| - Hook.new.tap do |h| - hook_proc.call(h) - end - end - - @logger.info("#{environment[:action_hooks].length} hooks defined.") + environment[:triggers] ||= Vagrant::Plugin::V2::Trigger. + new(env, env.vagrantfile.config.trigger, machine, ui) end # Run the action chain in a busy block, marking the environment as @@ -95,14 +84,10 @@ module Vagrant action_name = environment[:action_name] - triggers.fire_triggers(action_name, :before, machine_name, :hook) if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") - # We place a process lock around every action that is called @logger.info("Running action: #{environment[:action_name]} #{callable_id}") Util::Busy.busy(int_callback) { callable.call(environment) } - triggers.fire_triggers(action_name, :after, machine_name, :hook) if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") - # Return the environment in case there are things in there that # the caller wants to use. environment diff --git a/lib/vagrant/action/warden.rb b/lib/vagrant/action/warden.rb index b50d219f1..828e317fc 100644 --- a/lib/vagrant/action/warden.rb +++ b/lib/vagrant/action/warden.rb @@ -1,7 +1,5 @@ require "log4r" require 'vagrant/util/experimental' -require 'vagrant/action/builtin/before_trigger' -require 'vagrant/action/builtin/after_trigger' module Vagrant module Action @@ -47,40 +45,9 @@ module Vagrant raise Errors::VagrantInterrupt if env[:interrupted] action = @actions.shift @logger.info("Calling IN action: #{action}") - - actions = [action] - - # If this is an action class, set name to check for any defined hooks - if !action.is_a?(Proc) - hook_name = action.class.name - @logger.debug("Performing hooks lookup on #{hook_name} (Action: #{action} - class: #{action.class})") - hooks = Vagrant.plugin("2").manager.find_action_hooks(hook_name) - - if !hooks.empty? - @logger.debug("Found #{hooks.count} hooks defined for #{hook_name} (Action: #{action})") - # Create a new builder for the current action so we can - # properly apply the hooks with the correct ordering - b = Builder.new - b.stack << action - - hooks.each do |hook_proc| - hook = Hook.new.tap { |h| hook_proc.call(h) } - hook.apply(b) - end - - actions = b.stack.map { |a| finalize_action(a, @env) } - end - end - - @logger.info("Calling BEGIN action: #{action}") - actions.each do |local_action| - @logger.info("Calling IN action: #{local_action} (root: #{action})") - @stack.unshift(local_action).first.call(env) - raise Errors::VagrantInterrupt if env[:interrupted] - @logger.info("Calling OUT action: #{local_action} (root: #{action})") - end - - @logger.info("Calling COMPLETE action: #{action}") + @stack.unshift(action).first.call(env) + raise Errors::VagrantInterrupt if env[:interrupted] + @logger.info("Calling OUT action: #{action}") rescue SystemExit, NoMemoryError # This means that an "exit" or "abort" was called, or we have run out # of memory. In these cases, we just exit immediately. @@ -132,19 +99,7 @@ module Vagrant args ||= [] if klass.is_a?(Class) - # A action klass which is to be instantiated with the - # app, env, and any arguments given - - # We wrap the action class in two Trigger method calls so that - # action triggers can fire before and after each given action in the stack. - klass_name = klass.name - [Vagrant::Action::Builtin::BeforeTriggerAction.new(self, env, - klass_name, - @triggers), - klass.new(self, env, *args, &block), - Vagrant::Action::Builtin::AfterTriggerAction.new(self, env, - klass_name, - @triggers)] + klass.new(self, env, *args, &block) elsif klass.respond_to?(:call) # Make it a lambda which calls the item then forwards # up the chain diff --git a/lib/vagrant/cli.rb b/lib/vagrant/cli.rb index 43bdafe41..9421c9c2b 100644 --- a/lib/vagrant/cli.rb +++ b/lib/vagrant/cli.rb @@ -62,9 +62,11 @@ module Vagrant # Initialize and execute the command class, returning the exit status. result = 0 begin - @triggers.fire_triggers(@sub_command.to_sym, :before, nil, :command) if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") + @triggers.fire(@sub_command, :before, nil, :command) if + Vagrant::Util::Experimental.feature_enabled?("typed_triggers") result = command_class.new(@sub_args, @env).execute - @triggers.fire_triggers(@sub_command.to_sym, :after, nil, :command) if Vagrant::Util::Experimental.feature_enabled?("typed_triggers") + @triggers.fire(@sub_command, :after, nil, :command) if + Vagrant::Util::Experimental.feature_enabled?("typed_triggers") rescue Interrupt @env.ui.info(I18n.t("vagrant.cli_interrupt")) result = 1 diff --git a/lib/vagrant/machine.rb b/lib/vagrant/machine.rb index 0b1ca1b65..f782a60f9 100644 --- a/lib/vagrant/machine.rb +++ b/lib/vagrant/machine.rb @@ -62,6 +62,11 @@ module Vagrant # @return [Hash] attr_reader :provider_options + # The triggers with machine specific configuration applied + # + # @return [Vagrant::Plugin::V2::Trigger] + attr_reader :triggers + # The UI for outputting in the scope of this machine. # # @return [UI] @@ -160,8 +165,6 @@ module Vagrant # as extra data set on the environment hash for the middleware # runner. def action(name, opts=nil) - @triggers.fire_triggers(name, :before, @name.to_s, :action) - @logger.info("Calling action: #{name} on provider #{@provider}") opts ||= {} @@ -210,8 +213,6 @@ module Vagrant ui.machine("action", name.to_s, "end") action_result end - - @triggers.fire_triggers(name, :after, @name.to_s, :action) # preserve returning environment after machine action runs return return_env rescue Errors::EnvironmentLockedError @@ -230,6 +231,7 @@ module Vagrant def action_raw(name, callable, extra_env=nil) # Run the action with the action runner on the environment env = { + raw_action_name: name, action_name: "machine_action_#{name}".to_sym, machine: self, machine_action: name, diff --git a/lib/vagrant/plugin/v2/manager.rb b/lib/vagrant/plugin/v2/manager.rb index ce21be1f4..bba37ae04 100644 --- a/lib/vagrant/plugin/v2/manager.rb +++ b/lib/vagrant/plugin/v2/manager.rb @@ -64,7 +64,11 @@ module Vagrant # @param [Class, String] key Base key for generation # @return [Array] all valid keys def generate_hook_keys(key) - key = key.is_a?(Class) ? key.name : key.to_s + if key.is_a?(Class) + key = key.name.to_s + else + key = key.to_s + end parts = key.split("::") [].tap do |keys| until parts.empty? diff --git a/lib/vagrant/plugin/v2/plugin.rb b/lib/vagrant/plugin/v2/plugin.rb index cab042437..9bc166268 100644 --- a/lib/vagrant/plugin/v2/plugin.rb +++ b/lib/vagrant/plugin/v2/plugin.rb @@ -71,6 +71,7 @@ module Vagrant # @return [Array] List of the hooks for the given action. def self.action_hook(name, hook_name=nil, &block) # The name is currently not used but we want it for the future. + hook_name = hook_name.to_s if hook_name hook_name ||= ALL_ACTIONS components.action_hooks[hook_name.to_sym] << block diff --git a/lib/vagrant/plugin/v2/trigger.rb b/lib/vagrant/plugin/v2/trigger.rb index 7ab4d73d8..0a6919d2c 100644 --- a/lib/vagrant/plugin/v2/trigger.rb +++ b/lib/vagrant/plugin/v2/trigger.rb @@ -30,58 +30,97 @@ module Vagrant @logger = Log4r::Logger.new("vagrant::trigger::#{self.class.to_s.downcase}") end - # Fires all triggers, if any are defined for the action and guest. Returns early + # Fires all triggers, if any are defined for the named type and guest. Returns early # and logs a warning if the community plugin `vagrant-triggers` is installed # - # @param [Symbol] action Vagrant command to fire trigger on + # @param [Symbol] name Name of `type` thing to fire trigger on # @param [Symbol] stage :before or :after - # @param [String] guest_name The guest that invoked firing the triggers - def fire_triggers(action, stage, guest_name, type) + # @param [String] guest The guest that invoked firing the triggers + # @param [Symbol] type Type of trigger to fire (:action, :hook, :command) + def fire(name, stage, guest, type) if community_plugin_detected? @logger.warn("Community plugin `vagrant-triggers detected, so core triggers will not fire") return end - if !action - @logger.warn("Action given is nil, no triggers will fire") - return - else - action = action.to_sym - end + return @logger.warn("Name given is nil, no triggers will fire") if !name + return @logger.warn("Name given cannot be symbolized, no triggers will fire") if + !name.respond_to?(:to_sym) + + name = name.to_sym # get all triggers matching action - triggers = [] + triggers = find(name, stage, guest, type) + + if !triggers.empty? + @logger.info("Firing trigger for #{type} #{name} on guest #{guest}") + @ui.info(I18n.t("vagrant.trigger.start", type: type, stage: stage, name: name)) + execute(triggers) + end + end + + # Find all triggers defined for the named type and guest. + # + # @param [Symbol] name Name of `type` thing to fire trigger on + # @param [Symbol] stage :before or :after + # @param [String] guest The guest that invoked firing the triggers + # @param [Symbol] type Type of trigger to fire + # @return [Array] + def find(name, stage, guest, type) + triggers = nil + name = nameify(name) + if stage == :before triggers = config.before_triggers.select do |t| - t.command == action || (t.command == :all && !t.ignore.include?(action)) + (t.command == :all && !t.ignore.include?(name)) || + (type == :hook && matched_hook?(t.command, name)) || + nameify(t.command) == name end elsif stage == :after triggers = config.after_triggers.select do |t| - t.command == action || (t.command == :all && !t.ignore.include?(action)) + (t.command == :all && !t.ignore.include?(name)) || + (type == :hook && matched_hook?(t.command, name)) || + nameify(t.command) == name end else raise Errors::TriggersNoStageGiven, - action: action, + name: name, stage: stage, - guest_name: guest_name - end - - triggers = filter_triggers(triggers, guest_name, type) - - if !triggers.empty? - @logger.info("Firing trigger for action #{action} on guest #{guest_name}") - @ui.info(I18n.t("vagrant.trigger.start", type: type, stage: stage, action: action)) - fire(triggers, guest_name) + type: type, + guest_name: guest end + filter_triggers(triggers, guest, type) end protected + # Convert object into name + # + # @param [Object, Class] object Object to name + # @return [String] + def nameify(object) + if object.is_a?(Class) + object.name.to_s + else + object.to_s + end + end #------------------------------------------------------------------- # Internal methods, don't call these. #------------------------------------------------------------------- + # Generate all valid lookup keys for given action key + # + # @param [Class, String] key Base key for generation + # @return [Array] all valid keys + def matched_hook?(key, subject) + subject = nameify(subject) + Vagrant.plugin("2").manager.generate_hook_keys(key).any? do |k| + k == subject + end + end + # Looks up if the community plugin `vagrant-triggers` is installed # and also caches the result # @@ -133,12 +172,11 @@ module Vagrant return triggers end - # Fires off all triggers in the given array + # Execute all triggers in the given array # # @param [Array] triggers An array of triggers to be fired - def fire(triggers, guest_name) + def execute(triggers) # ensure on_error is respected by exiting or continuing - triggers.each do |trigger| @logger.debug("Running trigger #{trigger.id}...") diff --git a/lib/vagrant/util.rb b/lib/vagrant/util.rb index 32babe50d..f8be2baf0 100644 --- a/lib/vagrant/util.rb +++ b/lib/vagrant/util.rb @@ -7,6 +7,7 @@ module Vagrant autoload :CredentialScrubber, 'vagrant/util/credential_scrubber' autoload :DeepMerge, 'vagrant/util/deep_merge' autoload :Env, 'vagrant/util/env' + autoload :Experimental, 'vagrant/util/experimental' autoload :HashWithIndifferentAccess, 'vagrant/util/hash_with_indifferent_access' autoload :GuestInspection, 'vagrant/util/guest_inspection' autoload :LoggingFormatter, 'vagrant/util/logging_formatter' diff --git a/plugins/kernel_v2/config/vm_trigger.rb b/plugins/kernel_v2/config/vm_trigger.rb index 2595eea49..182ea3d8a 100644 --- a/plugins/kernel_v2/config/vm_trigger.rb +++ b/plugins/kernel_v2/config/vm_trigger.rb @@ -114,7 +114,7 @@ module VagrantPlugins # Internal options @id = SecureRandom.uuid - @command = command.to_sym + @command = command.to_s @ruby_block = UNSET_VALUE @logger.debug("Trigger defined for: #{command}") @@ -214,10 +214,7 @@ module VagrantPlugins end if @type == :command || !@type - commands = [] - Vagrant.plugin("2").manager.commands.each do |key,data| - commands.push(key) - end + commands = Vagrant.plugin("2").manager.commands.keys.map(&:to_s) if !commands.include?(@command) && @command != :all machine.ui.warn(I18n.t("vagrant.config.triggers.bad_command_warning", diff --git a/templates/locales/en.yml b/templates/locales/en.yml index 4557a54e2..ea0394f5a 100644 --- a/templates/locales/en.yml +++ b/templates/locales/en.yml @@ -301,7 +301,7 @@ en: Vagrant has been configured to abort. Vagrant will terminate after remaining running actions have completed... start: |- - Running %{type} triggers %{stage} %{action} ... + Running %{type} triggers %{stage} %{name} ... fire_with_name: |- Running trigger: %{name}... fire: |- @@ -1529,7 +1529,8 @@ en: triggers_no_stage_given: |- The incorrect stage was given to the trigger plugin: Guest: %{guest_name} - Action: %{action} + Name: %{name} + Type: %{type} Stage: %{stage} This is an internal error that should be reported as a bug. diff --git a/test/unit/plugins/kernel_v2/config/vm_trigger_test.rb b/test/unit/plugins/kernel_v2/config/vm_trigger_test.rb index 6735e219f..6fbba226f 100644 --- a/test/unit/plugins/kernel_v2/config/vm_trigger_test.rb +++ b/test/unit/plugins/kernel_v2/config/vm_trigger_test.rb @@ -53,7 +53,7 @@ describe VagrantPlugins::Kernel_V2::VagrantConfigTrigger do it "sets a command" do subject.finalize! - expect(subject.command).to eq(command) + expect(subject.command).to eq(command.to_s) end it "uses default error behavior" do diff --git a/test/unit/vagrant/action/builder_test.rb b/test/unit/vagrant/action/builder_test.rb index 94f9d846a..93bf6ec2d 100644 --- a/test/unit/vagrant/action/builder_test.rb +++ b/test/unit/vagrant/action/builder_test.rb @@ -237,13 +237,19 @@ describe Vagrant::Action::Builder do end describe "action hooks" do - it "applies them properly" do - hook = double("hook") - allow(hook).to receive(:apply) do |builder| - builder.use appender_proc(2) - end + let(:hook) { double("hook") } + let(:manager) { Vagrant.plugin("2").manager } - data[:action_hooks] = [hook] + 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) @@ -253,11 +259,16 @@ describe Vagrant::Action::Builder do end it "applies without prepend/append if it has already" do - hook = double("hook") - expect(hook).to receive(:apply).with(anything, { no_prepend_or_append: true }).once + hook_proc = proc{ |h| h.append(appender_proc(2)) } + expect(manager).to receive(:action_hooks).with(:test_action). + and_return([hook_proc]) - data[:action_hooks] = [hook] - data[:action_hooks_already_ran] = true + 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 @@ -295,4 +306,456 @@ describe Vagrant::Action::Builder do "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 diff --git a/test/unit/vagrant/action/builtin/delayed_test.rb b/test/unit/vagrant/action/builtin/delayed_test.rb new file mode 100644 index 000000000..766062c0b --- /dev/null +++ b/test/unit/vagrant/action/builtin/delayed_test.rb @@ -0,0 +1,22 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::Delayed do + let(:app) { lambda {|*_|} } + let(:env) { {} } + + it "should raise error when callable does not provide #call" do + expect { described_class.new(app, env, true) }. + to raise_error(TypeError) + end + + it "should delay executing action to end of stack" do + result = [] + one = proc{ |*_| result << :one } + two = proc{ |*_| result << :two } + builder = Vagrant::Action::Builder.build(described_class, two) + builder.use(one) + builder.call(env) + expect(result.first).to eq(:one) + expect(result.last).to eq(:two) + end +end diff --git a/test/unit/vagrant/action/builtin/trigger_test.rb b/test/unit/vagrant/action/builtin/trigger_test.rb new file mode 100644 index 000000000..a389c944c --- /dev/null +++ b/test/unit/vagrant/action/builtin/trigger_test.rb @@ -0,0 +1,63 @@ +require File.expand_path("../../../../base", __FILE__) + +describe Vagrant::Action::Builtin::Trigger do + let(:app) { lambda { |env| } } + let(:env) { {machine: machine} } + let(:machine) { nil } + let(:triggers) { double("triggers") } + let(:name) { "trigger-name" } + let(:timing) { :before } + let(:type) { :action } + + let(:subject) { described_class. + new(app, env, name, triggers, timing, type) } + + before do + allow(triggers).to receive(:fire) + allow(app).to receive(:call) + end + + + it "should properly create a new instance" do + expect(subject).to be_a(described_class) + end + + it "should fire triggers" do + expect(triggers).to receive(:fire) + subject.call(env) + end + + it "should fire triggers without machine name" do + expect(triggers).to receive(:fire).with(name, timing, nil, type) + subject.call(env) + end + + context "when machine is provided" do + let(:machine) { double("machine", name: "machine-name") } + + it "should include machine name when firing triggers" do + expect(triggers).to receive(:fire).with(name, timing, "machine-name", type) + subject.call(env) + end + end + + context "when timing is :before" do + it "should not error" do + expect { subject }.not_to raise_error + end + end + + context "when timing is :after" do + it "should not error" do + expect { subject }.not_to raise_error + end + end + + context "when timing is not :before or :after" do + let(:timing) { :unknown } + + it "should raise error" do + expect { subject }.to raise_error(ArgumentError) + end + end +end diff --git a/test/unit/vagrant/action/runner_test.rb b/test/unit/vagrant/action/runner_test.rb index 7ffc1d14e..44db5536f 100644 --- a/test/unit/vagrant/action/runner_test.rb +++ b/test/unit/vagrant/action/runner_test.rb @@ -96,4 +96,49 @@ describe Vagrant::Action::Runner do instance.run(callable) expect(result).to eq("bar") end + + describe "triggers" do + let(:environment) { double("environment", ui: nil) } + let(:machine) { double("machine", triggers: machine_triggers, name: "") } + let(:env_triggers) { double("env_triggers", find: []) } + let(:machine_triggers) { double("machine_triggers", find: []) } + + before do + allow(environment).to receive_message_chain(:vagrantfile, :config, :trigger) + allow(Vagrant::Plugin::V2::Trigger).to receive(:new). + and_return(env_triggers) + end + + context "when only environment is provided" do + let(:instance) { described_class.new(action_name: "test", env: environment) } + + it "should use environment to build new trigger" do + expect(environment).to receive_message_chain(:vagrantfile, :config, :trigger) + instance.run(lambda{|_|}) + end + + it "should pass environment based triggers to callable" do + callable = lambda { |env| expect(env[:triggers]).to eq(env_triggers) } + instance.run(callable) + end + end + + context "when only machine is provided" do + let(:instance) { described_class.new(action_name: "test", machine: machine) } + + it "should pass machine based triggers to callable" do + callable = lambda { |env| expect(env[:triggers]).to eq(machine_triggers) } + instance.run(callable) + end + end + + context "when both environment and machine is provided" do + let(:instance) { described_class.new(action_name: "test", machine: machine, env: environment) } + + it "should pass machine based triggers to callable" do + callable = lambda { |env| expect(env[:triggers]).to eq(machine_triggers) } + instance.run(callable) + end + end + end end diff --git a/test/unit/vagrant/action/warden_test.rb b/test/unit/vagrant/action/warden_test.rb index 439dead01..6cb649f4f 100644 --- a/test/unit/vagrant/action/warden_test.rb +++ b/test/unit/vagrant/action/warden_test.rb @@ -103,135 +103,4 @@ describe Vagrant::Action::Warden do # The recover should not have been called expect(data.key?(:recover)).not_to be end - - describe "dynamic action hooks" do - 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.new([ActionTwo], data) - 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.new([described_class.new([ActionTwo], data), ActionOne], data) - 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.new([ActionTwo], data) - 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.new([Vagrant::Test::ActionTest], data) } - - 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 end diff --git a/test/unit/vagrant/cli_test.rb b/test/unit/vagrant/cli_test.rb index db7e7e9bd..f0d31a58f 100644 --- a/test/unit/vagrant/cli_test.rb +++ b/test/unit/vagrant/cli_test.rb @@ -59,8 +59,9 @@ describe Vagrant::CLI do it "fires triggers, if enabled" do allow(Vagrant::Util::Experimental).to receive(:feature_enabled?). - with("typed_triggers").and_return("true") - allow(triggers).to receive(:fire_triggers) + with("typed_triggers").and_return(true) + allow(triggers).to receive(:fire) + allow(triggers).to receive(:find).and_return([double("trigger-result")]) commands[:destroy] = [command_lambda("destroy", 42), {}] @@ -68,7 +69,7 @@ describe Vagrant::CLI do subject = described_class.new(["destroy"], env) - expect(triggers).to receive(:fire_triggers).twice + expect(triggers).to receive(:fire).twice expect(subject).not_to receive(:help) expect(subject.execute).to eql(42) @@ -76,13 +77,13 @@ describe Vagrant::CLI do it "does not fire triggers if disabled" do allow(Vagrant::Util::Experimental).to receive(:feature_enabled?). - with("typed_triggers").and_return("false") + with("typed_triggers").and_return(false) commands[:destroy] = [command_lambda("destroy", 42), {}] subject = described_class.new(["destroy"], env) - expect(triggers).not_to receive(:fire_triggers) + expect(triggers).not_to receive(:fire) expect(subject).not_to receive(:help) expect(subject.execute).to eql(42) diff --git a/test/unit/vagrant/environment_test.rb b/test/unit/vagrant/environment_test.rb index 834dd418d..3694560d6 100644 --- a/test/unit/vagrant/environment_test.rb +++ b/test/unit/vagrant/environment_test.rb @@ -987,7 +987,6 @@ VF Dir.chdir(temp_dir) do instance = described_class.new(local_data_path: "foo") expect(instance.local_data_path).to eq(instance.cwd.join("foo")) - expect(File.exist?(instance.local_data_path)).to be(false) end end end @@ -1108,7 +1107,6 @@ VF end expect { instance }.to_not raise_error - expect(Pathname.new(local_data_path)).to_not be_exist end it "should upgrade all active VMs" do diff --git a/test/unit/vagrant/plugin/v2/trigger_test.rb b/test/unit/vagrant/plugin/v2/trigger_test.rb index 370b4b5db..6a4fff094 100644 --- a/test/unit/vagrant/plugin/v2/trigger_test.rb +++ b/test/unit/vagrant/plugin/v2/trigger_test.rb @@ -36,31 +36,99 @@ describe Vagrant::Plugin::V2::Trigger do triggers.finalize! end - let(:subject) { described_class.new(env, triggers, machine, ui) } - context "#fire_triggers" do - it "raises an error if an inproper stage is given" do - expect{ subject.fire_triggers(:up, :not_real, "guest", :action) }. + describe "#fire" do + it "raises an error if an improper stage is given" do + expect{ subject.fire(:up, :not_real, "guest", :action) }. to raise_error(Vagrant::Errors::TriggersNoStageGiven) end it "does not fire triggers if community plugin is detected" do allow(subject).to receive(:community_plugin_detected?).and_return(true) - expect(subject).not_to receive(:fire) - subject.fire_triggers(:up, :before, "guest", :action) + expect(subject).not_to receive(:execute) + subject.fire(:up, :before, "guest", :action) end it "does fire triggers if community plugin is not detected" do allow(subject).to receive(:community_plugin_detected?).and_return(false) - expect(subject).to receive(:fire) - subject.fire_triggers(:up, :before, "guest", :action) + expect(subject).to receive(:execute) + subject.fire(:up, :before, "guest", :action) end end - context "#filter_triggers" do + describe "#find" do + it "raises an error if an improper stage is given" do + expect { subject.find(:up, :not_real, "guest", :action) }. + to raise_error(Vagrant::Errors::TriggersNoStageGiven) + end + + it "returns empty array when no triggers are found" do + expect(subject.find(:halt, :after, "guest", :action)).to be_empty + end + + it "returns items in array when triggers are found" do + expect(subject.find(:halt, :before, "guest", :action)).not_to be_empty + end + + it "returns the execpted number of items in the array when triggers are found" do + expect(subject.find(:halt, :before, "guest", :action).count).to eq(1) + end + + it "filters all found triggers" do + expect(subject).to receive(:filter_triggers) + subject.find(:halt, :before, "guest", :action) + end + + it "should not attempt to match hook name with non-hook type" do + expect(subject).not_to receive(:matched_hook?) + subject.find(:halt, :before, "guest", :action) + end + + context "with hook type" do + before do + triggers.before(:environment_load, hash_block.merge(type: :hook)) + triggers.before(Vagrant::Action::Builtin::SyncedFolders, hash_block.merge(type: :hook)) + triggers.finalize! + end + + it "returns empty array when no triggers are found" do + expect(subject.find(:environment_unload, :before, "guest", :hook)).to be_empty + end + + it "returns items in array when triggers are found" do + expect(subject.find(:environment_load, :before, "guest", :hook).size).to eq(1) + end + + it "should locate hook trigger using class constant" do + expect(subject.find(Vagrant::Action::Builtin::SyncedFolders, :before, "guest", :hook)). + not_to be_empty + end + + it "should locate hook trigger using string" do + expect(subject.find("environment_load", :before, "guest", :hook)).not_to be_empty + end + + it "should locate hook trigger using full converted name" do + expect(subject.find(:vagrant_action_builtin_synced_folders, :before, "guest", :hook)). + not_to be_empty + end + + it "should locate hook trigger using partial suffix converted name" do + expect(subject.find(:builtin_synced_folders, :before, "guest", :hook)). + not_to be_empty + end + + it "should not locate hook trigger using partial prefix converted name" do + expect(subject.find(:vagrant_action, :before, "guest", :hook)). + to be_empty + end + end + end + + describe "#filter_triggers" do it "returns all triggers if no constraints" do before_triggers = triggers.before_triggers filtered_triggers = subject.send(:filter_triggers, before_triggers, "guest", :action) @@ -101,17 +169,17 @@ describe Vagrant::Plugin::V2::Trigger do end end - context "#fire" do + describe "#execute" do it "calls the corresponding trigger methods if options set" do expect(subject).to receive(:info).twice expect(subject).to receive(:warn).once expect(subject).to receive(:run).twice expect(subject).to receive(:run_remote).once - subject.send(:fire, triggers.before_triggers, "guest") + subject.send(:execute, triggers.before_triggers) end end - context "#info" do + describe "#info" do let(:message) { "Printing some info" } it "prints messages at INFO" do @@ -125,7 +193,7 @@ describe Vagrant::Plugin::V2::Trigger do end end - context "#warn" do + describe "#warn" do let(:message) { "Printing some warnings" } it "prints messages at WARN" do @@ -139,7 +207,7 @@ describe Vagrant::Plugin::V2::Trigger do end end - context "#run" do + describe "#run" do let(:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new } let(:shell_block) { {info: "hi", run: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}}} } let(:shell_block_exit_codes) { @@ -305,7 +373,7 @@ describe Vagrant::Plugin::V2::Trigger do end end - context "#run_remote" do + describe "#run_remote" do let (:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new } let (:shell_block) { {info: "hi", run_remote: {inline: "echo 'hi'", env: {"KEY"=>"VALUE"}}} } let (:path_block) { {warn: "bye", @@ -412,7 +480,7 @@ describe Vagrant::Plugin::V2::Trigger do end end - context "#trigger_abort" do + describe "#trigger_abort" do it "system exits when called" do allow(Process).to receive(:exit!).and_return(true) output = "" @@ -456,7 +524,7 @@ describe Vagrant::Plugin::V2::Trigger do end end - context "#ruby" do + describe "#ruby" do let(:trigger_run) { VagrantPlugins::Kernel_V2::TriggerConfig.new } let(:block) { proc{var = 1+1} } let(:ruby_trigger) { {info: "hi", ruby: block} } @@ -471,4 +539,18 @@ describe Vagrant::Plugin::V2::Trigger do subject.send(:execute_ruby, block) end end + + describe "#nameify" do + it "should return empty string when object" do + expect(subject.send(:nameify, "")).to eq("") + end + + it "should return name of class" do + expect(subject.send(:nameify, String)).to eq("String") + end + + it "should return empty string when class has no name" do + expect(subject.send(:nameify, Class.new)).to eq("") + end + end end