mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Overhaul hooks
Mostly cleanup and make managing hooks simpler, hopefully. Tests indicate this configuration matches hook execution order of RSpec.
This commit is contained in:
parent
10b652f4b5
commit
b9f0a31a4a
12 changed files with 279 additions and 415 deletions
|
@ -26,9 +26,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Support custom messages for failed expectations. [#28](https://gitlab.com/arctic-fox/spectator/-/issues/28)
|
||||
- Allow named arguments and assignments for `provided` (`given`) block.
|
||||
- Add `aggregate_failures` to capture and report multiple failed expectations. [#24](https://gitlab.com/arctic-fox/spectator/-/issues/24)
|
||||
- Add `append_` and `prepend_` variants of hook creation methods.
|
||||
|
||||
### Changed
|
||||
- `around_each` hooks wrap `before_all` and `after_all` hooks. [#12](https://github.com/icy-arctic-fox/spectator/issues/12)
|
||||
- Hook execution order has been tweaked to match RSpec.
|
||||
- `given` (now `provided`) blocks changed to produce a single example. `it` can no longer be nested in a `provided` block.
|
||||
- The "should" syntax no longer reports the source as inside Spectator.
|
||||
- Short-hand "should" syntax must be included by using `require "spectator/should"` - `it { should eq("foo") }`
|
||||
|
|
|
@ -21,25 +21,25 @@ module Spectator
|
|||
getter example_filter : ExampleFilter
|
||||
|
||||
# List of hooks to run before all examples in the test suite.
|
||||
protected getter before_suite_hooks : Array(ExampleGroupHook)
|
||||
protected getter before_suite_hooks : Deque(ExampleGroupHook)
|
||||
|
||||
# List of hooks to run before each top-level example group.
|
||||
protected getter before_all_hooks : Array(ExampleGroupHook)
|
||||
protected getter before_all_hooks : Deque(ExampleGroupHook)
|
||||
|
||||
# List of hooks to run before every example.
|
||||
protected getter before_each_hooks : Array(ExampleHook)
|
||||
protected getter before_each_hooks : Deque(ExampleHook)
|
||||
|
||||
# List of hooks to run after all examples in the test suite.
|
||||
protected getter after_suite_hooks : Array(ExampleGroupHook)
|
||||
protected getter after_suite_hooks : Deque(ExampleGroupHook)
|
||||
|
||||
# List of hooks to run after each top-level example group.
|
||||
protected getter after_all_hooks : Array(ExampleGroupHook)
|
||||
protected getter after_all_hooks : Deque(ExampleGroupHook)
|
||||
|
||||
# List of hooks to run after every example.
|
||||
protected getter after_each_hooks : Array(ExampleHook)
|
||||
protected getter after_each_hooks : Deque(ExampleHook)
|
||||
|
||||
# List of hooks to run around every example.
|
||||
protected getter around_each_hooks : Array(ExampleProcsyHook)
|
||||
protected getter around_each_hooks : Deque(ExampleProcsyHook)
|
||||
|
||||
# Creates a new configuration.
|
||||
# Properties are pulled from *source*.
|
||||
|
|
|
@ -21,100 +21,107 @@ module Spectator
|
|||
@filters = [] of ExampleFilter
|
||||
|
||||
# List of hooks to run before all examples in the test suite.
|
||||
protected getter before_suite_hooks = [] of ExampleGroupHook
|
||||
protected getter before_suite_hooks = Deque(ExampleGroupHook).new
|
||||
|
||||
# List of hooks to run before each top-level example group.
|
||||
protected getter before_all_hooks = [] of ExampleGroupHook
|
||||
protected getter before_all_hooks = Deque(ExampleGroupHook).new
|
||||
|
||||
# List of hooks to run before every example.
|
||||
protected getter before_each_hooks = [] of ExampleHook
|
||||
protected getter before_each_hooks = Deque(ExampleHook).new
|
||||
|
||||
# List of hooks to run after all examples in the test suite.
|
||||
protected getter after_suite_hooks = [] of ExampleGroupHook
|
||||
protected getter after_suite_hooks = Deque(ExampleGroupHook).new
|
||||
|
||||
# List of hooks to run after each top-level example group.
|
||||
protected getter after_all_hooks = [] of ExampleGroupHook
|
||||
protected getter after_all_hooks = Deque(ExampleGroupHook).new
|
||||
|
||||
# List of hooks to run after every example.
|
||||
protected getter after_each_hooks = [] of ExampleHook
|
||||
protected getter after_each_hooks = Deque(ExampleHook).new
|
||||
|
||||
# List of hooks to run around every example.
|
||||
protected getter around_each_hooks = [] of ExampleProcsyHook
|
||||
protected getter around_each_hooks = Deque(ExampleProcsyHook).new
|
||||
|
||||
# Attaches a hook to be invoked before all examples in the test suite.
|
||||
def add_before_suite_hook(hook)
|
||||
@before_suite_hooks << hook
|
||||
@before_suite_hooks.push(hook)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute before all examples in the test suite.
|
||||
def before_suite(&block)
|
||||
@before_suite_hooks << ExampleGroupHook.new(&block)
|
||||
hook = ExampleGroupHook.new(&block)
|
||||
add_before_suite_hook(hook)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked before each top-level example group.
|
||||
def add_before_all_hook(hook)
|
||||
@before_all_hooks << hook
|
||||
@before_all_hooks.push(hook)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute before each top-level example group.
|
||||
def before_all(&block)
|
||||
@before_all_hooks << ExampleGroupHook.new(&block)
|
||||
hook = ExampleGroupHook.new(&block)
|
||||
add_before_all_hook(hook)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked before every example.
|
||||
# The current example is provided as a block argument.
|
||||
def add_before_each_hook(hook)
|
||||
@before_each_hooks << hook
|
||||
@before_each_hooks.push(hook)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute before every.
|
||||
# The current example is provided as a block argument.
|
||||
def before_each(&block : Example -> _)
|
||||
@before_each_hooks << ExampleHook.new(&block)
|
||||
hook = ExampleHook.new(&block)
|
||||
add_before_each_hook(hook)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked after all examples in the test suite.
|
||||
def add_after_suite_hook(hook)
|
||||
@after_suite_hooks << hook
|
||||
@after_suite_hooks.unshift(hook)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute after all examples in the test suite.
|
||||
def after_suite(&block)
|
||||
@after_suite_hooks << ExampleGroupHook.new(&block)
|
||||
hook = ExampleGroupHook.new(&block)
|
||||
add_after_suite_hook(hook)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked after each top-level example group.
|
||||
def add_after_all_hook(hook)
|
||||
@after_all_hooks << hook
|
||||
@after_all_hooks.unshift(hook)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute after each top-level example group.
|
||||
def after_all(&block)
|
||||
@after_all_hooks << ExampleGroupHook.new(&block)
|
||||
hook = ExampleGroupHook.new(&block)
|
||||
add_after_all_hook(hook)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked after every example.
|
||||
# The current example is provided as a block argument.
|
||||
def add_after_each_hook(hook)
|
||||
@after_each_hooks << hook
|
||||
@after_each_hooks.unshift(hook)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute after every example.
|
||||
# The current example is provided as a block argument.
|
||||
def after_each(&block : Example -> _)
|
||||
@after_each_hooks << ExampleHook.new(&block)
|
||||
hook = ExampleHook.new(&block)
|
||||
add_after_each_hook(hook)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked around every example.
|
||||
# The current example in procsy form is provided as a block argument.
|
||||
def add_around_each_hook(hook)
|
||||
@around_each_hooks << hook
|
||||
@around_each_hooks.push(hook)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute around every example.
|
||||
# The current example in procsy form is provided as a block argument.
|
||||
def around_each(&block : Example -> _)
|
||||
@around_each_hooks << ExampleProcsyHook.new(label: "around_each", &block)
|
||||
def around_each(&block : Example::Procsy -> _)
|
||||
hook = ExampleProcsyHook.new(label: "around_each", &block)
|
||||
add_around_each_hook(hook)
|
||||
end
|
||||
|
||||
# Creates a configuration.
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
require "./example_group_hook"
|
||||
require "./example_hook"
|
||||
|
||||
module Spectator
|
||||
# Mix-in for managing events and hooks.
|
||||
# This module is intended to be included by `ExampleGroup`.
|
||||
module Events
|
||||
# Defines an event for an example group.
|
||||
# This event typically runs before or after an example group finishes.
|
||||
# No contextual information (or example) is provided to the hooks.
|
||||
#
|
||||
# The *name* defines the name of the event.
|
||||
# This must be unique across all events, not just group events.
|
||||
# Four public methods are defined - two to add a hook and the others to trigger the event which calls every hook.
|
||||
# One trigger method, prefixed with *call_* will always call the event hooks.
|
||||
# The other trigger method, prefixed with *call_once_* will only call the event hooks on the first invocation.
|
||||
#
|
||||
# A block must be provided to this macro.
|
||||
# The block defines the logic for invoking all of the hooks.
|
||||
# A single argument is yielded to the block - the set of hooks for the event.
|
||||
#
|
||||
# ```
|
||||
# group_event some_hook do |hooks|
|
||||
# hooks.each(&.call)
|
||||
# end
|
||||
# ```
|
||||
private macro group_event(name, &block)
|
||||
@{{name.id}}_hooks = [] of ExampleGroupHook
|
||||
@{{name.id}}_called = Atomic::Flag.new
|
||||
|
||||
# Adds a hook to be invoked when the *{{name.id}}* event occurs.
|
||||
def add_{{name.id}}_hook(hook : ExampleGroupHook) : Nil
|
||||
@{{name.id}}_hooks << hook
|
||||
end
|
||||
|
||||
# Adds a hook to be invoked when the *{{name.id}}* event occurs.
|
||||
# The hook is added to the front of the list.
|
||||
def prepend_{{name.id}}_hook(hook : ExampleGroupHook) : Nil
|
||||
@{{name.id}}_hooks.unshift(hook)
|
||||
end
|
||||
|
||||
# Defines a hook for the *{{name.id}}* event.
|
||||
# The block of code given to this method is invoked when the event occurs.
|
||||
def {{name.id}}(&block : -> _) : Nil
|
||||
hook = ExampleGroupHook.new(label: {{name.stringify}}, &block)
|
||||
add_{{name.id}}_hook(hook)
|
||||
end
|
||||
|
||||
# Signals that the *{{name.id}}* event has occurred.
|
||||
# All hooks associated with the event will be called.
|
||||
def call_{{name.id}} : Nil
|
||||
handle_{{name.id}}(@{{name.id}}_hooks)
|
||||
end
|
||||
|
||||
# Signals that the *{{name.id}}* event has occurred.
|
||||
# Only calls the hooks if the event hasn't been triggered before by this method.
|
||||
# Returns true if the hooks were called and false if they weren't (called previously).
|
||||
def call_once_{{name.id}} : Bool
|
||||
first = @{{name.id}}_called.test_and_set
|
||||
call_{{name.id}} if first
|
||||
first
|
||||
end
|
||||
|
||||
# Logic specific to invoking the *{{name.id}}* hook.
|
||||
private def handle_{{name.id}}({{block.args.splat}})
|
||||
{{block.body}}
|
||||
end
|
||||
end
|
||||
|
||||
# Defines an event for an example.
|
||||
# This event typically runs before or after an example finishes.
|
||||
# The current example is provided to the hooks.
|
||||
#
|
||||
# The *name* defines the name of the event.
|
||||
# This must be unique across all events.
|
||||
# Three public methods are defined - two to add a hook and the other to trigger the event which calls every hook.
|
||||
#
|
||||
# A block must be provided to this macro.
|
||||
# The block defines the logic for invoking all of the hooks.
|
||||
# Two arguments are yielded to the block - the set of hooks for the event, and the current example.
|
||||
#
|
||||
# ```
|
||||
# example_event some_hook do |hooks, example|
|
||||
# hooks.each(&.call(example))
|
||||
# end
|
||||
# ```
|
||||
private macro example_event(name, &block)
|
||||
@{{name.id}}_hooks = [] of ExampleHook
|
||||
|
||||
# Adds a hook to be invoked when the *{{name.id}}* event occurs.
|
||||
def add_{{name.id}}_hook(hook : ExampleHook) : Nil
|
||||
@{{name.id}}_hooks << hook
|
||||
end
|
||||
|
||||
# Adds a hook to be invoked when the *{{name.id}}* event occurs.
|
||||
# The hook is added to the front of the list.
|
||||
def prepend_{{name.id}}_hook(hook : ExampleHook) : Nil
|
||||
@{{name.id}}_hooks.unshift(hook)
|
||||
end
|
||||
|
||||
# Defines a hook for the *{{name.id}}* 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 {{name.id}}(&block : Example ->) : Nil
|
||||
hook = ExampleHook.new(label: {{name.stringify}}, &block)
|
||||
add_{{name.id}}_hook(hook)
|
||||
end
|
||||
|
||||
# Signals that the *{{name.id}}* event has occurred.
|
||||
# All hooks associated with the event will be called.
|
||||
# The *example* should be the current example.
|
||||
def call_{{name.id}}(example : Example) : Nil
|
||||
handle_{{name.id}}(@{{name.id}}_hooks, example)
|
||||
end
|
||||
|
||||
# Logic specific to invoking the *{{name.id}}* hook.
|
||||
private def handle_{{name.id}}({{block.args.splat}})
|
||||
{{block.body}}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -98,14 +98,14 @@ module Spectator
|
|||
|
||||
begin
|
||||
@result = Harness.run do
|
||||
@group.try(&.call_once_before_all)
|
||||
@group.try(&.call_before_all)
|
||||
if (parent = @group)
|
||||
parent.call_around_each(self) { run_internal }
|
||||
parent.call_around_each(procsy).call
|
||||
else
|
||||
run_internal
|
||||
end
|
||||
if (parent = @group)
|
||||
parent.call_once_after_all if parent.finished?
|
||||
parent.call_after_all if parent.finished?
|
||||
end
|
||||
end
|
||||
ensure
|
||||
|
@ -191,6 +191,11 @@ module Spectator
|
|||
end
|
||||
end
|
||||
|
||||
# Creates a procsy from this example that runs the example.
|
||||
def procsy
|
||||
Procsy.new(self) { run_internal }
|
||||
end
|
||||
|
||||
# Creates a procsy from this example and the provided block.
|
||||
def procsy(&block : ->)
|
||||
Procsy.new(self, &block)
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
require "./events"
|
||||
require "./example_procsy_hook"
|
||||
require "./hooks"
|
||||
require "./node"
|
||||
|
||||
module Spectator
|
||||
# Collection of examples and sub-groups.
|
||||
class ExampleGroup < Node
|
||||
include Enumerable(Node)
|
||||
include Events
|
||||
include Hooks
|
||||
include Iterable(Node)
|
||||
|
||||
@nodes = [] of Node
|
||||
|
@ -19,66 +19,44 @@ module Spectator
|
|||
# `ExampleGroup` manages the association of nodes to groups.
|
||||
protected setter group : ExampleGroup?
|
||||
|
||||
# Calls all hooks from the parent group if there is a parent.
|
||||
# The *hook* is the method name of the group hook to invoke.
|
||||
private macro call_parent_hooks(hook)
|
||||
if (parent = @group)
|
||||
parent.{{hook.id}}
|
||||
end
|
||||
end
|
||||
|
||||
# Calls all hooks from the parent group if there is a parent.
|
||||
# The *hook* is the method name of the example hook to invoke.
|
||||
# The current *example* must be provided.
|
||||
private macro call_parent_hooks(hook, example)
|
||||
if (parent = @group)
|
||||
parent.{{hook.id}}({{example}})
|
||||
end
|
||||
end
|
||||
|
||||
# Calls group hooks of the current group.
|
||||
private def call_hooks(hooks)
|
||||
hooks.each do |hook|
|
||||
Log.trace { "Invoking hook #{hook}" }
|
||||
hook.call
|
||||
end
|
||||
end
|
||||
|
||||
# Calls example hooks of the current group.
|
||||
# Requires the current *example*.
|
||||
private def call_hooks(hooks, example)
|
||||
hooks.each do |hook|
|
||||
Log.trace { "Invoking hook #{hook}" }
|
||||
hook.call(example)
|
||||
end
|
||||
end
|
||||
|
||||
group_event before_all do |hooks|
|
||||
define_hook before_all : ExampleGroupHook do
|
||||
Log.trace { "Processing before_all hooks for #{self}" }
|
||||
|
||||
call_parent_hooks(:call_once_before_all)
|
||||
call_hooks(hooks)
|
||||
@group.try &.call_before_all
|
||||
before_all_hooks.each &.call_once
|
||||
end
|
||||
|
||||
group_event after_all do |hooks|
|
||||
define_hook after_all : ExampleGroupHook do
|
||||
Log.trace { "Processing after_all hooks for #{self}" }
|
||||
|
||||
call_hooks(hooks)
|
||||
call_parent_hooks(:call_once_after_all) if @group.try(&.finished?)
|
||||
after_all_hooks.each &.call_once if finished?
|
||||
if group = @group
|
||||
group.call_after_all if group.finished?
|
||||
end
|
||||
end
|
||||
|
||||
example_event before_each do |hooks, example|
|
||||
define_hook before_each : ExampleHook do |example|
|
||||
Log.trace { "Processing before_each hooks for #{self}" }
|
||||
|
||||
call_parent_hooks(:call_before_each, example)
|
||||
call_hooks(hooks, example)
|
||||
@group.try &.call_before_each(example)
|
||||
before_each_hooks.each &.call(example)
|
||||
end
|
||||
|
||||
example_event after_each do |hooks, example|
|
||||
define_hook after_each : ExampleHook do |example|
|
||||
Log.trace { "Processing after_each hooks for #{self}" }
|
||||
|
||||
call_hooks(hooks, example)
|
||||
call_parent_hooks(:call_after_each, example)
|
||||
after_each_hooks.each &.call(example)
|
||||
@group.try &.call_after_each(example)
|
||||
end
|
||||
|
||||
define_hook around_each : ExampleProcsyHook do |procsy|
|
||||
Log.trace { "Processing around_each hooks for #{self}" }
|
||||
|
||||
around_each_hooks.reverse_each { |hook| procsy = hook.wrap(procsy) }
|
||||
if group = @group
|
||||
procsy = group.call_around_each(procsy)
|
||||
end
|
||||
procsy
|
||||
end
|
||||
|
||||
# Creates the example group.
|
||||
|
@ -158,57 +136,5 @@ module Spectator
|
|||
@nodes << node
|
||||
node.group = self
|
||||
end
|
||||
|
||||
@around_hooks = [] of ExampleProcsyHook
|
||||
|
||||
# Adds a hook to be invoked when the *around_each* event occurs.
|
||||
def add_around_each_hook(hook : ExampleProcsyHook) : Nil
|
||||
@around_hooks << hook
|
||||
end
|
||||
|
||||
# Adds a hook to be invoked when the *around_each* event occurs.
|
||||
# The hook is added to the front of the list.
|
||||
def prepend_around_each_hook(hook : ExampleProcsyHook) : Nil
|
||||
@around_hooks.unshift(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) : 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, &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(&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)
|
||||
# Avoid overhead if there's no hooks.
|
||||
return procsy if @around_hooks.empty?
|
||||
|
||||
# Wrap each hook with the next.
|
||||
outer = procsy
|
||||
@around_hooks.reverse_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
|
||||
|
|
|
@ -2,6 +2,7 @@ require "./example_group"
|
|||
require "./example_group_hook"
|
||||
require "./example_hook"
|
||||
require "./example_procsy_hook"
|
||||
require "./hooks"
|
||||
require "./label"
|
||||
require "./location"
|
||||
require "./metadata"
|
||||
|
@ -12,12 +13,15 @@ module Spectator
|
|||
# Hooks and builders for child nodes can be added over time to this builder.
|
||||
# When done, call `#build` to produce an `ExampleGroup`.
|
||||
class ExampleGroupBuilder < NodeBuilder
|
||||
include Hooks
|
||||
|
||||
define_hook before_all : ExampleGroupHook
|
||||
define_hook after_all : ExampleGroupHook, :prepend
|
||||
define_hook before_each : ExampleHook
|
||||
define_hook after_each : ExampleHook, :prepend
|
||||
define_hook around_each : ExampleProcsyHook
|
||||
|
||||
@children = [] of NodeBuilder
|
||||
@before_all_hooks = [] of ExampleGroupHook
|
||||
@before_each_hooks = [] of ExampleHook
|
||||
@after_all_hooks = [] of ExampleGroupHook
|
||||
@after_each_hooks = [] of ExampleHook
|
||||
@around_each_hooks = [] of ExampleProcsyHook
|
||||
|
||||
# Creates the builder.
|
||||
# Initially, the builder will have no children and no hooks.
|
||||
|
@ -25,62 +29,6 @@ module Spectator
|
|||
def initialize(@name : Label = nil, @location : Location? = nil, @metadata : Metadata = Metadata.new)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked before any and all examples in the current group.
|
||||
def add_before_all_hook(hook)
|
||||
@before_all_hooks << hook
|
||||
end
|
||||
|
||||
# Defines a block of code to execute before any and all examples in the current group.
|
||||
def before_all(&block)
|
||||
@before_all_hooks << ExampleGroupHook.new(&block)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked before every example in the current group.
|
||||
# The current example is provided as a block argument.
|
||||
def add_before_each_hook(hook)
|
||||
@before_each_hooks << hook
|
||||
end
|
||||
|
||||
# Defines a block of code to execute before every example in the current group.
|
||||
# The current example is provided as a block argument.
|
||||
def before_each(&block : Example -> _)
|
||||
@before_each_hooks << ExampleHook.new(&block)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked after any and all examples in the current group.
|
||||
def add_after_all_hook(hook)
|
||||
@after_all_hooks << hook
|
||||
end
|
||||
|
||||
# Defines a block of code to execute after any and all examples in the current group.
|
||||
def after_all(&block)
|
||||
@after_all_hooks << ExampleGroupHook.new(&block)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked after every example in the current group.
|
||||
# The current example is provided as a block argument.
|
||||
def add_after_each_hook(hook)
|
||||
@after_each_hooks << hook
|
||||
end
|
||||
|
||||
# Defines a block of code to execute after every example in the current group.
|
||||
# The current example is provided as a block argument.
|
||||
def after_each(&block : Example -> _)
|
||||
@after_each_hooks << ExampleHook.new(&block)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked around every example in the current group.
|
||||
# The current example in procsy form is provided as a block argument.
|
||||
def add_around_each_hook(hook)
|
||||
@around_each_hooks << hook
|
||||
end
|
||||
|
||||
# Defines a block of code to execute around every example in the current group.
|
||||
# The current example in procsy form is provided as a block argument.
|
||||
def around_each(&block : Example -> _)
|
||||
@around_each_hooks << ExampleProcsyHook.new(label: "around_each", &block)
|
||||
end
|
||||
|
||||
# Constructs an example group with previously defined attributes, children, and hooks.
|
||||
# The *parent* is an already constructed example group to nest the new example group under.
|
||||
# It can be nil if the new example group won't have a parent.
|
||||
|
@ -100,11 +48,11 @@ module Spectator
|
|||
|
||||
# Adds all previously configured hooks to an example group.
|
||||
private def apply_hooks(group)
|
||||
@before_all_hooks.each { |hook| group.add_before_all_hook(hook) }
|
||||
@before_each_hooks.each { |hook| group.add_before_each_hook(hook) }
|
||||
@after_all_hooks.each { |hook| group.prepend_after_all_hook(hook) }
|
||||
@after_each_hooks.each { |hook| group.prepend_after_each_hook(hook) }
|
||||
@around_each_hooks.each { |hook| group.add_around_each_hook(hook) }
|
||||
before_all_hooks.each { |hook| group.before_all(hook) }
|
||||
before_each_hooks.each { |hook| group.before_each(hook) }
|
||||
after_all_hooks.each { |hook| group.after_all(hook) }
|
||||
after_each_hooks.each { |hook| group.after_each(hook) }
|
||||
around_each_hooks.each { |hook| group.around_each(hook) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,6 +11,7 @@ module Spectator
|
|||
getter! label : Label
|
||||
|
||||
@proc : ->
|
||||
@called = Atomic::Flag.new
|
||||
|
||||
# Creates the hook with a proc.
|
||||
# The *proc* will be called when the hook is invoked.
|
||||
|
@ -27,9 +28,18 @@ module Spectator
|
|||
|
||||
# Invokes the hook.
|
||||
def call : Nil
|
||||
@called.test_and_set
|
||||
@proc.call
|
||||
end
|
||||
|
||||
# Invokes the hook if it hasn't already been invoked.
|
||||
# Returns true if the hook was invoked (first time being called).
|
||||
def call_once : Bool
|
||||
first = @called.test_and_set
|
||||
@proc.call if first
|
||||
first
|
||||
end
|
||||
|
||||
# Produces the string representation of the hook.
|
||||
# Includes the location and label if they're not nil.
|
||||
def to_s(io)
|
||||
|
|
|
@ -4,25 +4,28 @@ require "./location"
|
|||
module Spectator
|
||||
# Information about a hook tied to an example and a proc to invoke it.
|
||||
class ExampleHook
|
||||
# Method signature for example hooks.
|
||||
alias Proc = Example ->
|
||||
|
||||
# Location of the hook in source code.
|
||||
getter! location : Location
|
||||
|
||||
# User-defined description of the hook.
|
||||
getter! label : Label
|
||||
|
||||
@proc : Example ->
|
||||
@proc : Proc
|
||||
|
||||
# Creates the hook with a proc.
|
||||
# The *proc* will be called when the hook is invoked.
|
||||
# A *location* and *label* can be provided for debugging.
|
||||
def initialize(@proc : (Example ->), *, @location : Location? = nil, @label : Label = nil)
|
||||
def initialize(@proc : Proc, *, @location : Location? = nil, @label : Label = nil)
|
||||
end
|
||||
|
||||
# Creates the hook with a block.
|
||||
# The block must take a single argument - the current example.
|
||||
# The block will be executed when the hook is invoked.
|
||||
# A *location* and *label* can be provided for debugging.
|
||||
def initialize(*, @location : Location? = nil, @label : Label = nil, &block : Example -> _)
|
||||
def initialize(*, @location : Location? = nil, @label : Label = nil, &block : Proc)
|
||||
@proc = block
|
||||
end
|
||||
|
||||
|
|
114
src/spectator/hooks.cr
Normal file
114
src/spectator/hooks.cr
Normal file
|
@ -0,0 +1,114 @@
|
|||
module Spectator
|
||||
# Mix-in for defining hook methods.
|
||||
module Hooks
|
||||
# Defines various methods for adding hooks of a specific type.
|
||||
#
|
||||
# The *declaration* defines the name and type of hook.
|
||||
# It should be a type declaration in the form: `some_hook : ExampleHook`,
|
||||
# where `some_hook` is the name of the hook, and `ExampleHook` is type type.
|
||||
#
|
||||
# A default order can be specified by *order*.
|
||||
# The *order* argument must be *append* or *prepend*.
|
||||
# This indicates the order hooks are added by default when called by client code.
|
||||
#
|
||||
# Multiple methods are generated.
|
||||
# The primary methods will be named the same as the hook (from *declaration*).
|
||||
# These take a pre-built hook instance, or arguments to pass to the hook type's initializer.
|
||||
# The new hook is added a collection in the order specified by *order*.
|
||||
#
|
||||
# Alternate methods are also generated that add hooks in the opposite order of *order*.
|
||||
# These are prefixed with the opposite order word.
|
||||
# For instance, when *order* is "append", the prefix will be "prepend",
|
||||
# resulting in a method named `prepend_some_hook`.
|
||||
#
|
||||
# A private getter method is created so that the hooks can be accessed if needed.
|
||||
# The getter method has `_hooks` appended to the hook name.
|
||||
# For instance, if the *declaration* contains `important_thing`, then the getter is `important_thing_hooks`.
|
||||
#
|
||||
# Lastly, an optional block can be provided.
|
||||
# If given, a protected method will be defined with the block's contents.
|
||||
# This method typically operates on (calls) the hooks.
|
||||
# The private getter method mentioned above can be used to access the hooks.
|
||||
# Any block arguments will be used as argument in the method.
|
||||
# The method name has the prefix `call_` followed by the hook name.
|
||||
#
|
||||
# ```
|
||||
# define_hook important_event : ImportantHook do |example|
|
||||
# important_event_hooks.each &.call(example)
|
||||
# end
|
||||
#
|
||||
# # ...
|
||||
#
|
||||
# important_event do |example|
|
||||
# puts "An important event occurred for #{example}"
|
||||
# end
|
||||
# ```
|
||||
macro define_hook(declaration, order = :append, &block)
|
||||
{% if order.id == :append.id
|
||||
method = :push.id
|
||||
alt_method = :unshift.id
|
||||
alt_prefix = :prepend.id
|
||||
elsif order.id == :prepend.id
|
||||
method = :unshift.id
|
||||
alt_method = :push.id
|
||||
alt_prefix = :append.id
|
||||
else
|
||||
raise "Unknown hook order type - #{order}"
|
||||
end %}
|
||||
|
||||
# Retrieves all registered hooks for {{declaration.var}}.
|
||||
protected getter {{declaration.var}}_hooks = Deque({{declaration.type}}).new
|
||||
|
||||
# Registers a new "{{declaration.var}}" hook.
|
||||
# The hook will be {{order.id}}ed to the list.
|
||||
def {{declaration.var}}(hook : {{declaration.type}}) : Nil
|
||||
@{{declaration.var}}_hooks.{{method}}(hook)
|
||||
end
|
||||
|
||||
# Registers a new "{{declaration.var}}" hook.
|
||||
# The hook will be {{order.id}}ed to the list.
|
||||
# A new hook will be created by passing args to `{{declaration.type}}.new`.
|
||||
def {{declaration.var}}(*args, **kwargs) : Nil
|
||||
hook = {{declaration.type}}.new(*args, **kwargs)
|
||||
{{declaration.var}}(hook)
|
||||
end
|
||||
|
||||
# Registers a new "{{declaration.var}}" hook.
|
||||
# The hook will be {{order.id}}ed to the list.
|
||||
# A new hook will be created by passing args to `{{declaration.type}}.new`.
|
||||
def {{declaration.var}}(*args, **kwargs, &block) : Nil
|
||||
hook = {{declaration.type}}.new(*args, **kwargs, &block)
|
||||
{{declaration.var}}(hook)
|
||||
end
|
||||
|
||||
# Registers a new "{{declaration.var}}" hook.
|
||||
# The hook will be {{alt_prefix}}ed to the list.
|
||||
def {{alt_prefix}}_{{declaration.var}}(hook : {{declaration.type}}) : Nil
|
||||
@{{declaration.var}}_hooks.{{alt_method}}(hook)
|
||||
end
|
||||
|
||||
# Registers a new "{{declaration.var}}" hook.
|
||||
# The hook will be {{alt_prefix}}ed to the list.
|
||||
# A new hook will be created by passing args to `{{declaration.type}}.new`.
|
||||
def {{alt_prefix}}_{{declaration.var}}(*args, **kwargs) : Nil
|
||||
hook = {{declaration.type}}.new(*args, **kwargs)
|
||||
{{alt_prefix}}_{{declaration.var}}(hook)
|
||||
end
|
||||
|
||||
# Registers a new "{{declaration.var}}" hook.
|
||||
# The hook will be {{alt_prefix}}ed to the list.
|
||||
# A new hook will be created by passing args to `{{declaration.type}}.new`.
|
||||
def {{alt_prefix}}_{{declaration.var}}(*args, **kwargs, &block) : Nil
|
||||
hook = {{declaration.type}}.new(*args, **kwargs, &block)
|
||||
{{alt_prefix}}_{{declaration.var}}(hook)
|
||||
end
|
||||
|
||||
{% if block %}
|
||||
# Handles calling all "{{declaration.var}}" hooks.
|
||||
protected def call_{{declaration.var}}({{block.args.splat}})
|
||||
{{block.body}}
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -14,7 +14,6 @@ require "./context_delegate"
|
|||
require "./context_method"
|
||||
require "./dsl"
|
||||
require "./error_result"
|
||||
require "./events"
|
||||
require "./example_context_delegate"
|
||||
require "./example_context_method"
|
||||
require "./example"
|
||||
|
@ -30,6 +29,7 @@ require "./expression"
|
|||
require "./fail_result"
|
||||
require "./formatting"
|
||||
require "./harness"
|
||||
require "./hooks"
|
||||
require "./label"
|
||||
require "./lazy"
|
||||
require "./lazy_wrapper"
|
||||
|
|
|
@ -4,6 +4,7 @@ require "./example_builder"
|
|||
require "./example_context_method"
|
||||
require "./example_group"
|
||||
require "./example_group_builder"
|
||||
require "./hooks"
|
||||
require "./iterative_example_group_builder"
|
||||
require "./pending_example_builder"
|
||||
require "./spec"
|
||||
|
@ -17,6 +18,8 @@ module Spectator
|
|||
class SpecBuilder
|
||||
Log = ::Spectator::Log.for(self)
|
||||
|
||||
delegate before_all, after_all, before_each, after_each, around_each, to: current
|
||||
|
||||
# Stack tracking the current group.
|
||||
# The bottom of the stack (first element) is the root group.
|
||||
# The root group should never be removed.
|
||||
|
@ -44,23 +47,7 @@ module Spectator
|
|||
raise "Mismatched start and end groups" unless root?
|
||||
|
||||
group = root.build
|
||||
|
||||
# Apply hooks from configuration.
|
||||
config.before_suite_hooks.each { |hook| group.prepend_before_all_hook(hook) }
|
||||
config.after_suite_hooks.each { |hook| group.prepend_after_all_hook(hook) }
|
||||
config.before_each_hooks.each { |hook| group.prepend_before_each_hook(hook) }
|
||||
config.after_each_hooks.each { |hook| group.prepend_after_each_hook(hook) }
|
||||
config.around_each_hooks.each { |hook| group.prepend_around_each_hook(hook) }
|
||||
|
||||
# `before_all` and `after_all` hooks are slightly different.
|
||||
# They are applied to every top-level group (groups just under root).
|
||||
group.each do |node|
|
||||
next unless node.is_a?(Events)
|
||||
|
||||
config.before_all_hooks.reverse_each { |hook| node.prepend_before_all_hook(hook) }
|
||||
config.after_all_hooks.reverse_each { |hook| node.prepend_after_all_hook(hook) }
|
||||
end
|
||||
|
||||
apply_config_hooks(group)
|
||||
Spec.new(group, config)
|
||||
end
|
||||
|
||||
|
@ -153,94 +140,60 @@ module Spectator
|
|||
current << PendingExampleBuilder.new(name, location, metadata, reason)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked before any and all examples in the test suite.
|
||||
def before_suite(hook)
|
||||
Log.trace { "Add before_suite hook #{hook}" }
|
||||
root.add_before_all_hook(hook)
|
||||
# Registers a new "before_suite" hook.
|
||||
# The hook will be appended to the list.
|
||||
# A new hook will be created by passing args to `ExampleGroupHook.new`.
|
||||
def before_suite(*args, **kwargs) : Nil
|
||||
root.before_all(*args, **kwargs)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute before any and all examples in the test suite.
|
||||
def before_suite(&block)
|
||||
Log.trace { "Add before_suite hook" }
|
||||
root.before_all(&block)
|
||||
# Registers a new "before_suite" hook.
|
||||
# The hook will be appended to the list.
|
||||
# A new hook will be created by passing args to `ExampleGroupHook.new`.
|
||||
def before_suite(*args, **kwargs, &block) : Nil
|
||||
root.before_all(*args, **kwargs, &block)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked before any and all examples in the current group.
|
||||
def before_all(hook)
|
||||
Log.trace { "Add before_all hook #{hook}" }
|
||||
current.add_before_all_hook(hook)
|
||||
# Registers a new "before_suite" hook.
|
||||
# The hook will be prepended to the list.
|
||||
# A new hook will be created by passing args to `ExampleGroupHook.new`.
|
||||
def prepend_before_suite(*args, **kwargs) : Nil
|
||||
root.prepend_before_all(*args, **kwargs)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute before any and all examples in the current group.
|
||||
def before_all(&block)
|
||||
Log.trace { "Add before_all hook" }
|
||||
current.before_all(&block)
|
||||
# Registers a new "before_suite" hook.
|
||||
# The hook will be prepended to the list.
|
||||
# A new hook will be created by passing args to `ExampleGroupHook.new`.
|
||||
def prepend_before_suite(*args, **kwargs, &block) : Nil
|
||||
root.prepend_before_all(*args, **kwargs, &block)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked before every example in the current group.
|
||||
# The current example is provided as a block argument.
|
||||
def before_each(hook)
|
||||
Log.trace { "Add before_each hook #{hook}" }
|
||||
current.add_before_each_hook(hook)
|
||||
# Registers a new "after_suite" hook.
|
||||
# The hook will be prepended to the list.
|
||||
# A new hook will be created by passing args to `ExampleGroupHook.new`.
|
||||
def after_suite(*args, **kwargs) : Nil
|
||||
root.before_all(*args, **kwargs)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute before every example in the current group.
|
||||
# The current example is provided as a block argument.
|
||||
def before_each(&block : Example -> _)
|
||||
Log.trace { "Add before_each hook block" }
|
||||
current.before_each(&block)
|
||||
# Registers a new "after_suite" hook.
|
||||
# The hook will be prepended to the list.
|
||||
# A new hook will be created by passing args to `ExampleGroupHook.new`.
|
||||
def after_suite(*args, **kwargs, &block) : Nil
|
||||
root.after_all(*args, **kwargs, &block)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked after any and all examples in the test suite.
|
||||
def after_suite(hook)
|
||||
Log.trace { "Add after_suite hook #{hook}" }
|
||||
root.add_after_all_hook(hook)
|
||||
# Registers a new "after_suite" hook.
|
||||
# The hook will be appended to the list.
|
||||
# A new hook will be created by passing args to `ExampleGroupHook.new`.
|
||||
def append_after_suite(*args, **kwargs) : Nil
|
||||
root.append_after_all(*args, **kwargs)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute after any and all examples in the test suite.
|
||||
def after_suite(&block)
|
||||
Log.trace { "Add after_suite hook" }
|
||||
root.after_all(&block)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked after any and all examples in the current group.
|
||||
def after_all(hook)
|
||||
Log.trace { "Add after_all hook #{hook}" }
|
||||
current.add_after_all_hook(hook)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute after any and all examples in the current group.
|
||||
def after_all(&block)
|
||||
Log.trace { "Add after_all hook" }
|
||||
current.after_all(&block)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked after every example in the current group.
|
||||
# The current example is provided as a block argument.
|
||||
def after_each(hook)
|
||||
Log.trace { "Add after_each hook #{hook}" }
|
||||
current.add_after_each_hook(hook)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute after every example in the current group.
|
||||
# The current example is provided as a block argument.
|
||||
def after_each(&block : Example -> _)
|
||||
Log.trace { "Add after_each hook" }
|
||||
current.after_each(&block)
|
||||
end
|
||||
|
||||
# Attaches a hook to be invoked around every example in the current group.
|
||||
# The current example in procsy form is provided as a block argument.
|
||||
def around_each(hook)
|
||||
Log.trace { "Add around_each hook #{hook}" }
|
||||
current.add_around_each_hook(hook)
|
||||
end
|
||||
|
||||
# Defines a block of code to execute around every example in the current group.
|
||||
# The current example in procsy form is provided as a block argument.
|
||||
def around_each(&block : Example -> _)
|
||||
Log.trace { "Add around_each hook" }
|
||||
current.around_each(&block)
|
||||
# Registers a new "after_suite" hook.
|
||||
# The hook will be appended to the list.
|
||||
# A new hook will be created by passing args to `ExampleGroupHook.new`.
|
||||
def append_after_suite(*args, **kwargs, &block) : Nil
|
||||
root.append_after_all(*args, **kwargs, &block)
|
||||
end
|
||||
|
||||
# Builds the configuration to use for the spec.
|
||||
|
@ -279,5 +232,23 @@ module Spectator
|
|||
private def config : Config
|
||||
@config || Config.default
|
||||
end
|
||||
|
||||
# Copy all hooks from config to top-level group.
|
||||
private def apply_config_hooks(group)
|
||||
config.before_suite_hooks.reverse_each { |hook| group.prepend_before_all(hook) }
|
||||
config.after_suite_hooks.each { |hook| group.after_all(hook) }
|
||||
config.before_each_hooks.reverse_each { |hook| group.prepend_before_each(hook) }
|
||||
config.after_each_hooks.each { |hook| group.after_each(hook) }
|
||||
config.around_each_hooks.reverse_each { |hook| group.prepend_around_each(hook) }
|
||||
|
||||
# `before_all` and `after_all` hooks from config are slightly different.
|
||||
# They are applied to every top-level group (groups just under root).
|
||||
group.each do |node|
|
||||
next unless node.is_a?(Hooks)
|
||||
|
||||
config.before_all_hooks.reverse_each { |hook| node.prepend_before_all(hook.dup) }
|
||||
config.after_all_hooks.each { |hook| node.after_all(hook.dup) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue