shard-spectator/src/spectator/spec_builder.cr

213 lines
8.9 KiB
Crystal

require "./config"
require "./example"
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"
require "./metadata"
module Spectator
# Progressively builds a test spec.
#
# A stack is used to track the current example group.
# Adding an example or group will nest it under the group at the top of the stack.
class SpecBuilder
Log = ::Spectator::Log.for(self)
delegate before_all, after_all, before_each, after_each, around_each, pre_condition, post_condition, 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.
# The top of the stack (last element) is the current group.
# New examples should be added to the current group.
@stack : Deque(ExampleGroupBuilder)
# Creates a new spec builder.
# A root group is pushed onto the group stack.
def initialize(@config : Config)
root = ExampleGroupBuilder.new
@stack = Deque(ExampleGroupBuilder).new
@stack.push(root)
end
# Constructs the test spec.
# Returns the spec instance.
#
# Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`.
# This would indicate a logical error somewhere in Spectator or an extension of it.
def build : Spec
raise "Mismatched start and end groups" unless root?
group = root.build
apply_config_hooks(group)
Spec.new(group, @config)
end
# Defines a new example group and pushes it onto the group stack.
# Examples and groups defined after calling this method will be nested under the new group.
# The group will be finished and popped off the stack when `#end_example` is called.
#
# The *name* is the name or brief description of the group.
# This should be a symbol when describing a type - the type name is represented as a symbol.
# Otherwise, a string should be used.
#
# The *location* optionally defined where the group originates in source code.
#
# A set of *metadata* can be used for filtering and modifying example behavior.
# For instance, adding a "pending" tag will mark tests as pending and skip execution.
def start_group(name, location = nil, metadata = nil) : Nil
Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{metadata}" }
builder = ExampleGroupBuilder.new(name, location, metadata)
# `before_all` and `after_all` hooks from config are slightly different.
# They are applied to every top-level group (groups just under root).
apply_top_level_config_hooks(builder) if root?
# Add group to the stack.
current << builder
@stack.push(builder)
end
# Defines a new iterative example group and pushes it onto the group stack.
# Examples and groups defined after calling this method will be nested under the new group.
# The group will be finished and popped off the stack when `#end_example` is called.
#
# The *collection* is the set of items to iterate over.
# Child nodes in this group will be executed once for every item in the collection.
# The *name* should be a string representation of *collection*.
# The *iterator* is an optional name given to a single item in *collection*.
#
# The *location* optionally defined where the group originates in source code.
#
# A set of *metadata* can be used for filtering and modifying example behavior.
# For instance, adding a "pending" tag will mark tests as pending and skip execution.
def start_iterative_group(collection, name, iterator = nil, location = nil, metadata = nil) : Nil
Log.trace { "Start iterative group: #{name} (#{typeof(collection)}) @ #{location}; metadata: #{metadata}" }
builder = IterativeExampleGroupBuilder.new(collection, name, iterator, location, metadata)
# `before_all` and `after_all` hooks from config are slightly different.
# They are applied to every top-level group (groups just under root).
apply_top_level_config_hooks(builder) if root?
# Add group to the stack.
current << builder
@stack.push(builder)
end
# Completes a previously defined example group and pops it off the group stack.
# Be sure to call `#start_group` and `#end_group` symmetrically.
def end_group : Nil
Log.trace { "End group: #{current}" }
raise "Can't pop root group" if root?
@stack.pop
end
# Defines a new example.
# The example is added to the group currently on the top of the stack.
#
# The *name* is the name or brief description of the example.
# This should be a string or nil.
# When nil, the example's name will be populated by the first expectation run inside of the test code.
#
# The *location* optionally defined where the example originates in source code.
#
# The *context_builder* is a proc that creates a context the test code should run in.
# See `Context` for more information.
#
# A set of *metadata* can be used for filtering and modifying example behavior.
# For instance, adding a "pending" tag will mark the test as pending and skip execution.
#
# A block must be provided.
# It will be yielded two arguments - the example created by this method, and the *context* argument.
# The return value of the block is ignored.
# It is expected that the test code runs when the block is called.
def add_example(name, location, context_builder, metadata = nil, &block : Example -> _) : Nil
Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" }
current << ExampleBuilder.new(context_builder, block, name, location, metadata)
end
# Defines a new pending example.
# The example is added to the group currently on the top of the stack.
#
# The *name* is the name or brief description of the example.
# This should be a string or nil.
# When nil, the example's name will be an anonymous example reference.
#
# The *location* optionally defined where the example originates in source code.
#
# A set of *metadata* can be used for filtering and modifying example behavior.
# For instance, adding a "pending" tag will mark the test as pending and skip execution.
# A default *reason* can be given in case the user didn't provide one.
def add_pending_example(name, location, metadata = nil, reason = nil) : Nil
Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" }
current << PendingExampleBuilder.new(name, location, metadata, reason)
end
# 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
# 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
# Registers a new "after_suite" hook.
# The hook will be pre-pended 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
# Registers a new "after_suite" hook.
# The hook will be pre-pended 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
# Checks if the current group is the root group.
private def root?
@stack.size == 1
end
# Retrieves the root group.
private def root
@stack.first
end
# Retrieves the current group, which is at the top of the stack.
# This is the group that new examples should be added to.
private def current
@stack.last
end
# Copy all hooks from config to root group.
private def apply_config_hooks(group)
@config.before_suite_hooks.each { |hook| group.before_all(hook) }
@config.after_suite_hooks.reverse_each { |hook| group.after_all(hook) }
@config.before_each_hooks.each { |hook| group.before_each(hook) }
@config.after_each_hooks.reverse_each { |hook| group.after_each(hook) }
@config.around_each_hooks.each { |hook| group.around_each(hook) }
end
# Copy `before_all` and `after_all` hooks to a group.
private def apply_top_level_config_hooks(group)
# Hooks are dupped so that they retain their original state (call once).
@config.before_all_hooks.each { |hook| group.before_all(hook.dup) }
@config.after_all_hooks.reverse_each { |hook| group.after_all(hook.dup) }
end
end
end