diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 5ff9750..6afac44 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -46,25 +46,48 @@ module Spectator # Returns the result of the execution. # The result will also be stored in `#result`. def run : Result - @@current = self Log.debug { "Running example #{self}" } Log.warn { "Example #{self} already ran" } if @finished - @result = Harness.run do - if (parent = group?) - parent.call_once_before_all - parent.call_before_each(self) - end - @entrypoint.call(self) + previous_example = @@current + @@current = self + + begin + @result = Harness.run do + if (parent = group?) + parent.call_around_each(self) { run_internal } + else + run_internal + end + end + ensure + @@current = previous_example @finished = true - - if (parent = group?) - parent.call_after_each(self) - parent.call_once_after_all if parent.finished? - end end - ensure - @@current = nil + end + + private def run_internal + run_before_hooks + run_test + run_after_hooks + end + + private def run_before_hooks : Nil + return unless (parent = group?) + + parent.call_once_before_all + parent.call_before_each(self) + end + + private def run_after_hooks : Nil + return unless (parent = group?) + + parent.call_after_each(self) + parent.call_once_after_all if parent.finished? + end + + private def run_test : Nil + @entrypoint.call(self) @finished = true end @@ -107,5 +130,37 @@ module Spectator io << result end + + # Wraps an example to behave like a `Proc`. + # This is typically used for an *around_each* hook. + # Invoking `#call` or `#run` will run the example. + struct Procsy + # Underlying example that will run. + getter example : Example + + # Creates the example proxy. + # The *example* should be run eventually. + # The *proc* defines the block of code to run when `#call` or `#run` is invoked. + def initialize(@example : Example, &@proc : ->) + end + + # Invokes the proc. + def call : Nil + @proc.call + end + + # Invokes the proc. + def run : Nil + @proc.call + end + + # Creates a new procsy for a block and the example from this instance. + def wrap(&block : ->) : self + self.class.new(@example, &block) + end + + # Allow instance to behave like an example. + forward_missing_to @example + end end end diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index 056950c..afd89b6 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -1,5 +1,6 @@ require "./events" require "./spec_node" +require "./example_procsy_hook" module Spectator # Collection of examples and sub-groups. @@ -101,5 +102,52 @@ module Spectator @nodes << node node.group = self end + + @around_hooks = [] of ExampleProcsyHook + + # Adds a hook to be invoked when the *{{name.id}}* event occurs. + def add_around_each_hook(hook : ExampleProcsyHook) : Nil + @around_hooks << hook + end + + # Defines a hook for the *around_each* event. + # The block of code given to this method is invoked when the event occurs. + # The current example is provided as a block argument. + def around_each(&block : Example::Procsy ->) : Nil + hook = ExampleProcsyHook.new(label: "around_each", &block) + add_around_each_hook(hook) + end + + + # Signals that the *around_each* event has occurred. + # All hooks associated with the event will be called. + def call_around_each(example : Example, &block : -> _) : Nil + # Avoid overhead if there's no hooks. + return yield if @around_hooks.empty? + + # Start with a procsy that wraps the original code. + procsy = Example::Procsy.new(example, &block) + procsy = wrap_around_each(procsy) + procsy.call + end + + # Wraps a procsy with the *around_each* hooks from this example group. + # The returned procsy will call each hook then *procsy*. + protected def wrap_around_each(procsy : Example::Procsy) : Example::Procsy + # Avoid overhead if there's no hooks. + return procsy if @around_hooks.empty? + + # Wrap each hook with the next. + outer = procsy + @around_hooks.each do |hook| + outer = hook.wrap(outer) + end + + # If there's a parent, wrap the procsy with its hooks. + # Otherwise, return the outermost procsy. + return outer unless (parent = group?) + + parent.wrap_around_each(outer) + end end end diff --git a/src/spectator/example_procsy_hook.cr b/src/spectator/example_procsy_hook.cr new file mode 100644 index 0000000..0b6ea63 --- /dev/null +++ b/src/spectator/example_procsy_hook.cr @@ -0,0 +1,56 @@ +require "./label" +require "./source" + +module Spectator + # Information about a hook tied to an example and a proc to invoke it. + class ExampleProcsyHook + # Location of the hook in source code. + getter! source : Source + + # User-defined description of the hook. + getter! label : Label + + @proc : Example::Procsy -> + + # Creates the hook with a proc. + # The *proc* will be called when the hook is invoked. + # A *source* and *label* can be provided for debugging. + def initialize(@proc : (Example::Procsy ->), *, @source : Source? = nil, @label : Label = nil) + end + + # Creates the hook with a block. + # The block must take a single argument - the current example wrapped in a procsy. + # The block will be executed when the hook is invoked. + # A *source* and *label* can be provided for debugging. + def initialize(*, @source : Source? = nil, @label : Label = nil, &block : Example::Procsy -> _) + @proc = block + end + + # Invokes the hook. + # The *example* refers to the current example. + def call(procsy : Example::Procsy) : Nil + @proc.call(procsy) + end + + # Creates an example procsy that invokes this hook. + def wrap(procsy : Example::Procsy) : Example::Procsy + procsy.wrap { call(procsy) } + end + + # Produces the string representation of the hook. + # Includes the source and label if they're not nil. + def to_s(io) + io << "example hook" + + if (label = @label) + io << ' ' + io << label + end + + if (source = @source) + io << " @ " + io << source + end + end + end +end