2018-10-14 23:10:12 +00:00
|
|
|
require "./example_component"
|
2018-08-19 07:15:32 +00:00
|
|
|
|
|
|
|
module Spectator
|
2018-11-20 04:52:10 +00:00
|
|
|
# Shared base class for groups of examples.
|
|
|
|
#
|
|
|
|
# Represents a collection of examples and other groups.
|
|
|
|
# Use the `#each` methods to iterate through each child.
|
|
|
|
# However, these methods do not recurse into sub-groups.
|
|
|
|
# If you need that functionality, see `ExampleIterator`.
|
|
|
|
# Additionally, the indexer method (`#[]`) will index into sub-groups.
|
|
|
|
#
|
|
|
|
# This class also stores hooks to be associated with all examples in the group.
|
|
|
|
# The hooks can be invoked by running one of the `#run_x_hooks` methods.
|
2018-10-14 23:10:12 +00:00
|
|
|
abstract class ExampleGroup < ExampleComponent
|
|
|
|
include Enumerable(ExampleComponent)
|
|
|
|
include Iterable(ExampleComponent)
|
2018-08-19 07:15:32 +00:00
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Creates the example group.
|
|
|
|
# The hooks are stored to be triggered later.
|
2018-10-14 23:10:12 +00:00
|
|
|
def initialize(@hooks : ExampleHooks)
|
|
|
|
@before_all_hooks_run = false
|
|
|
|
@after_all_hooks_run = false
|
|
|
|
end
|
2018-09-23 20:34:42 +00:00
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Retrieves the children in the group.
|
|
|
|
# This only returns the direct descends (non-recursive).
|
|
|
|
# The children must be set (with `#children=`) prior to calling this method.
|
2018-11-02 19:35:32 +00:00
|
|
|
getter! children : Array(ExampleComponent)
|
2018-09-23 20:34:42 +00:00
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Sets the children of the group.
|
|
|
|
# This should be called only from a builder in the `DSL` namespace.
|
|
|
|
# The children can be set only once -
|
|
|
|
# attempting to set more than once will raise an error.
|
|
|
|
# All sub-groups' children should be set before setting this group's children.
|
2018-10-14 23:10:12 +00:00
|
|
|
def children=(children : Array(ExampleComponent))
|
|
|
|
raise "Attempted to reset example group children" if @children
|
|
|
|
@children = children
|
2018-11-20 04:52:10 +00:00
|
|
|
# Recursively count the number of examples.
|
|
|
|
# This won't work if a sub-group hasn't had their children set (is still nil).
|
2018-10-15 01:08:07 +00:00
|
|
|
@example_count = children.sum(&.example_count)
|
2018-10-14 23:10:12 +00:00
|
|
|
end
|
2018-09-23 20:34:42 +00:00
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Yields each direct descendant.
|
2018-10-14 23:10:12 +00:00
|
|
|
def each
|
|
|
|
children.each do |child|
|
|
|
|
yield child
|
|
|
|
end
|
2018-09-15 16:45:47 +00:00
|
|
|
end
|
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Returns an iterator for each direct descendant.
|
2018-10-14 23:10:12 +00:00
|
|
|
def each : Iterator(ExampleComponent)
|
2018-10-15 00:29:01 +00:00
|
|
|
children.each
|
2018-10-14 23:10:12 +00:00
|
|
|
end
|
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Number of examples in this group and all sub-groups.
|
2018-10-15 01:08:07 +00:00
|
|
|
getter example_count = 0
|
2018-09-20 05:13:43 +00:00
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Retrieves an example by its index.
|
|
|
|
# This recursively searches for an example.
|
|
|
|
#
|
|
|
|
# Positive and negative indices can be used.
|
|
|
|
# Any value out of range will raise an `IndexError`.
|
|
|
|
#
|
|
|
|
# Examples are indexed as if they are in a flattened tree.
|
|
|
|
# For instance:
|
|
|
|
# ```
|
|
|
|
# examples = [0, 1, [2, 3, 4], [5, [6, 7], 8], 9, [10]].flatten
|
|
|
|
# ```
|
|
|
|
# The arrays symbolize groups,
|
|
|
|
# and the numbers are the index of the example in that slot.
|
2018-10-15 01:06:02 +00:00
|
|
|
def [](index : Int) : Example
|
2018-11-20 02:41:51 +00:00
|
|
|
offset = check_bounds(index)
|
|
|
|
find_nested(offset)
|
|
|
|
end
|
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Checks whether an index is within acceptable bounds.
|
|
|
|
# If the index is negative,
|
|
|
|
# it will be converted to its positive equivalent.
|
|
|
|
# If the index is out of bounds, an `IndexError` is raised.
|
|
|
|
# If the index is in bounds,
|
|
|
|
# the positive index is returned.
|
2018-11-20 02:41:51 +00:00
|
|
|
private def check_bounds(index)
|
|
|
|
if index < 0
|
|
|
|
raise IndexError.new if index < -example_count
|
|
|
|
index + example_count
|
|
|
|
else
|
|
|
|
raise IndexError.new if index >= example_count
|
|
|
|
index
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Finds the example with the specified index in the children.
|
|
|
|
# The `index` must be positive and within bounds (use `#check_bounds`).
|
2018-11-20 02:41:51 +00:00
|
|
|
private def find_nested(index)
|
2018-10-15 01:06:02 +00:00
|
|
|
offset = index
|
2018-11-20 04:52:10 +00:00
|
|
|
# Loop through each child
|
|
|
|
# until one is found to contain the index.
|
2018-11-20 02:41:51 +00:00
|
|
|
child = children.each do |child|
|
2018-10-15 01:06:02 +00:00
|
|
|
count = child.example_count
|
2018-11-20 04:52:10 +00:00
|
|
|
# Example groups consider their range to be [0, example_count).
|
|
|
|
# Each child is offset by the total example count of the previous children.
|
|
|
|
# The group exposes them in this way:
|
|
|
|
# 1. [0, example_count of group 1)
|
|
|
|
# 2. [example_count of group 1, example_count of group 2)
|
|
|
|
# 3. [example_count of group n, example_count of group n + 1)
|
|
|
|
# To iterate through children, the offset is tracked.
|
|
|
|
# Each iteration removes the previous child's count.
|
|
|
|
# This way the child receives the expected range.
|
2018-11-20 02:41:51 +00:00
|
|
|
break child if offset < count
|
|
|
|
offset -= count
|
2018-10-15 01:06:02 +00:00
|
|
|
end
|
2018-11-20 04:52:10 +00:00
|
|
|
# The remaining offset is passed along to the child.
|
|
|
|
# If it's an `Example`, it returns itself.
|
|
|
|
# Otherwise, the indexer repeats the process for the next child.
|
2018-11-20 02:41:51 +00:00
|
|
|
# It should be impossible to get `nil` here,
|
|
|
|
# provided the bounds check and example counts are correct.
|
|
|
|
child.not_nil![offset]
|
2018-10-15 01:06:02 +00:00
|
|
|
end
|
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Checks whether all examples in the group have been run.
|
2018-10-14 23:10:12 +00:00
|
|
|
def finished? : Bool
|
|
|
|
children.all?(&.finished?)
|
|
|
|
end
|
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Runs all of the `before_all` hooks.
|
|
|
|
# This should run prior to any examples in the group.
|
|
|
|
# The hooks will be run only once.
|
|
|
|
# Subsequent calls to this method will do nothing.
|
2018-10-14 23:10:12 +00:00
|
|
|
def run_before_all_hooks : Nil
|
2018-11-20 06:46:14 +00:00
|
|
|
return if @before_all_hooks_run
|
|
|
|
@hooks.run_before_all
|
|
|
|
@before_all_hooks_run = true
|
2018-09-15 16:45:47 +00:00
|
|
|
end
|
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Runs all of the `before_each` hooks.
|
|
|
|
# This method should run prior to every example in the group.
|
2018-10-14 23:10:12 +00:00
|
|
|
def run_before_each_hooks : Nil
|
2018-10-14 07:02:52 +00:00
|
|
|
@hooks.run_before_each
|
2018-09-15 16:45:47 +00:00
|
|
|
end
|
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Runs all of the `after_all` hooks.
|
|
|
|
# This should run following all examples in the group.
|
|
|
|
# The hooks will be run only once,
|
|
|
|
# and only after all examples in the group have finished.
|
|
|
|
# Subsequent calls after the hooks have been run will do nothing.
|
2018-10-14 23:10:12 +00:00
|
|
|
def run_after_all_hooks : Nil
|
2018-11-20 06:46:14 +00:00
|
|
|
return if @after_all_hooks_run
|
|
|
|
return unless finished?
|
|
|
|
@hooks.run_after_all
|
|
|
|
@after_all_hooks_run = true
|
2018-09-15 16:45:47 +00:00
|
|
|
end
|
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Runs all of the `after_each` hooks.
|
|
|
|
# This method should run following every example in the group.
|
2018-10-14 23:10:12 +00:00
|
|
|
def run_after_each_hooks : Nil
|
2018-10-14 07:02:52 +00:00
|
|
|
@hooks.run_after_each
|
2018-09-15 16:45:47 +00:00
|
|
|
end
|
|
|
|
|
2018-11-20 04:52:10 +00:00
|
|
|
# Creates a proc that runs the `around_each` hooks
|
|
|
|
# in addition to a block passed to this method.
|
|
|
|
# To call the block and all `around_each` hooks,
|
|
|
|
# just invoke `Proc#call` on the returned proc.
|
2018-10-14 23:10:12 +00:00
|
|
|
def wrap_around_each_hooks(&block : ->) : ->
|
|
|
|
@hooks.wrap_around_each(&block)
|
2018-09-24 02:24:28 +00:00
|
|
|
end
|
2018-08-19 07:15:32 +00:00
|
|
|
end
|
|
|
|
end
|