shard-spectator/src/spectator/example_group.cr

193 lines
5.7 KiB
Crystal
Raw Normal View History

require "./events"
2021-01-16 23:28:33 +00:00
require "./example_procsy_hook"
2021-05-08 02:09:33 +00:00
require "./node"
2020-09-06 01:55:46 +00:00
module Spectator
# Collection of examples and sub-groups.
2021-05-08 02:09:33 +00:00
class ExampleGroup < Node
include Enumerable(Node)
include Events
2021-05-08 02:09:33 +00:00
include Iterable(Node)
2021-05-08 02:09:33 +00:00
@nodes = [] of Node
2021-01-09 19:48:53 +00:00
# Parent group this group belongs to.
getter! group : ExampleGroup
# Assigns this group to the specified *group*.
# This is an internal method and should only be called from `ExampleGroup`.
# `ExampleGroup` manages the association of nodes to groups.
protected setter group : ExampleGroup?
group_event before_all do |hooks|
Log.trace { "Processing before_all hooks for #{self}" }
if (parent = group?)
parent.call_once_before_all
end
2021-01-09 18:14:27 +00:00
hooks.each do |hook|
Log.trace { "Invoking hook #{hook}" }
hook.call
end
end
group_event after_all do |hooks|
Log.trace { "Processing after_all hooks for #{self}" }
2021-01-09 18:14:27 +00:00
hooks.each do |hook|
Log.trace { "Invoking hook #{hook}" }
hook.call
end
if (parent = group?)
parent.call_once_after_all
end
end
example_event before_each do |hooks, example|
Log.trace { "Processing before_each hooks for #{self}" }
if (parent = group?)
parent.call_before_each(example)
end
2021-01-09 18:14:27 +00:00
hooks.each do |hook|
Log.trace { "Invoking hook #{hook}" }
hook.call(example)
end
end
example_event after_each do |hooks, example|
Log.trace { "Processing after_each hooks for #{self}" }
2021-01-09 18:14:27 +00:00
hooks.each do |hook|
Log.trace { "Invoking hook #{hook}" }
hook.call(example)
end
if (parent = group?)
parent.call_after_each(example)
end
end
# Creates the example group.
# The *name* describes the purpose of the group.
# It can be a `Symbol` to describe a type.
# The *location* tracks where the group exists in source code.
# This group will be assigned to the parent *group* if it is provided.
# A set of *tags* can be used for filtering and modifying example behavior.
def initialize(@name : Label = nil, @location : Location? = nil,
@group : ExampleGroup? = nil, @tags : Tags = Tags.new)
# Ensure group is linked.
group << self if group
end
# Removes the specified *node* from the group.
# The node will be unassigned from this group.
2021-05-08 02:09:33 +00:00
def delete(node : Node)
# Only remove from the group if it is associated with this group.
return unless node.group == self
node.group = nil
@nodes.delete(node)
end
# Yields each node (example and sub-group).
def each
@nodes.each { |node| yield node }
end
2020-10-17 17:23:51 +00:00
# Returns an iterator for each (example and sub-group).
def each
@nodes.each
end
2020-09-12 22:02:11 +00:00
# Checks if all examples and sub-groups have finished.
def finished? : Bool
@nodes.all?(&.finished?)
end
# Constructs the full name or description of the example group.
# This prepends names of groups this group is part of.
def to_s(io)
name = @name
# Prefix with group's full name if the node belongs to a group.
if (group = @group)
group.to_s(io)
# Add padding between the node names
# only if the names don't appear to be symbolic.
# Skip blank group names (like the root group).
io << ' ' unless !group.name? || # ameba:disable Style/NegatedConditionsInUnless
(group.name?.is_a?(Symbol) && name.is_a?(String) &&
(name.starts_with?('#') || name.starts_with?('.')))
end
super
end
# Adds the specified *node* to the group.
# Assigns the node to this group.
# If the node already belongs to a group,
# it will be removed from the previous group before adding it to this group.
2021-05-08 02:09:33 +00:00
def <<(node : Node)
# Remove from existing group if the node is part of one.
if (previous = node.group?)
previous.delete(node)
end
# Add the node to this group and associate with it.
@nodes << node
node.group = self
end
2021-01-16 23:28:33 +00:00
@around_hooks = [] of ExampleProcsyHook
2021-01-17 00:16:31 +00:00
# Adds a hook to be invoked when the *around_each* event occurs.
2021-01-16 23:28:33 +00:00
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.reverse_each do |hook|
2021-01-16 23:28:33 +00:00
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