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:
Michael Miller 2021-08-07 21:45:49 -06:00
parent 10b652f4b5
commit b9f0a31a4a
No known key found for this signature in database
GPG key ID: FB9F12F7C646A4AD
12 changed files with 279 additions and 415 deletions

View file

@ -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") }`

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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