Merge remote-tracking branch 'origin/release/0.9' into mocks-and-doubles

This commit is contained in:
Michael Miller 2019-09-27 14:12:29 -06:00
commit 8c180e818f
88 changed files with 838 additions and 9229 deletions

View file

@ -1,11 +1,12 @@
require "./spectator/includes"
require "./spectator_test"
# Module that contains all functionality related to Spectator.
module Spectator
extend self
# Current version of the Spectator library.
VERSION = "0.8.2"
VERSION = "0.9.0"
# Top-level describe method.
# All specs in a file must be wrapped in this call.
@ -23,7 +24,7 @@ module Spectator
# NOTE: Inside the block, the `Spectator` prefix is no longer needed.
# Actually, prefixing methods and macros with `Spectator`
# most likely won't work and can cause compiler errors.
macro describe(what, &block)
macro describe(description, &block)
# This macro creates the foundation for all specs.
# Every group of examples is defined a separate module - `SpectatorExamples`.
# There's multiple reasons for this.
@ -32,30 +33,21 @@ module Spectator
# We don't want the spec code to accidentally pickup types and values from the `Spectator` module.
# Another reason is that we need a root module to put all examples and groups in.
# And lastly, the spec DSL needs to be given to the block of code somehow.
# The DSL is included in the `SpectatorExamples` module.
# The DSL is included in the `SpectatorTest` class.
#
# For more information on how the DSL works, see the `DSL` module.
# Root-level module that contains all examples and example groups.
module SpectatorExamples
# Include the DSL for creating groups, example, and more.
include ::Spectator::DSL::StructureDSL
# Placeholder initializer.
# This is needed because examples and groups call super in their initializer.
# Those initializers pass the sample values upward through their hierarchy.
def initialize(_sample_values : ::Spectator::Internals::SampleValues)
end
# Pass off the "what" argument and block to `DSL::StructureDSL.describe`.
# Root-level class that contains all examples and example groups.
class SpectatorTest
# Pass off the description argument and block to `DSL::StructureDSL.describe`.
# That method will handle creating a new group for this spec.
describe({{what}}) {{block}}
describe({{description}}) {{block}}
end
end
# ditto
macro context(what, &block)
describe({{what}}) {{block}}
macro context(description, &block)
describe({{description}}) {{block}}
end
# Flag indicating whether Spectator should automatically run tests.
@ -109,7 +101,7 @@ module Spectator
# Builds the tests and runs the framework.
private def run
# Build the test suite and run it.
suite = ::Spectator::DSL::Builder.build(config.example_filter)
suite = ::Spectator::SpecBuilder.build(config.example_filter)
Runner.new(suite, config).run
rescue ex
# Catch all unhandled exceptions here.

View file

@ -2,7 +2,6 @@ require "./dsl/*"
module Spectator
# Namespace containing methods representing the spec domain specific language.
# Also contains builders to generate classes and instances to later run the spec.
module DSL
end
end

View file

@ -2,14 +2,9 @@ require "../expectations/expectation_partial"
require "../source"
require "../test_block"
require "../test_value"
require "./matcher_dsl"
module Spectator::DSL
# Methods that are available inside test code.
# Basically, inside an `StructureDSL#it` block.
module ExampleDSL
include MatcherDSL
module Spectator
module DSL
# Starts an expectation.
# This should be followed up with `Spectator::Expectations::ExpectationPartial#to`
# or `Spectator::Expectations::ExpectationPartial#to_not`.
@ -23,9 +18,9 @@ module Spectator::DSL
# Where the actual value is returned by the system-under-test,
# and the expected value is what the actual value should be to satisfy the condition.
macro expect(actual, _source_file = __FILE__, _source_line = __LINE__)
test_value = ::Spectator::TestValue.new({{actual}}, {{actual.stringify}})
source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
::Spectator::Expectations::ExpectationPartial.new(test_value, source)
%test_value = ::Spectator::TestValue.new({{actual}}, {{actual.stringify}})
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
::Spectator::Expectations::ExpectationPartial.new(%test_value, %source)
end
# Starts an expectation on a block of code.

View file

@ -1,15 +0,0 @@
module Spectator::DSL
# Creates instances of examples from a specified class.
class ExampleFactory
# Creates the factory.
# The type passed to this constructor must be a sub-type of `Example`.
def initialize(@example_type : Example.class)
end
# Constructs a new example instance and returns it.
# The *group* and *sample_values* are passed to `Example#initialize`.
def build(group : ExampleGroup, sample_values : Internals::SampleValues) : Example
@example_type.new(group, sample_values)
end
end
end

View file

@ -1,93 +0,0 @@
module Spectator::DSL
# Base class for building all example groups.
abstract class ExampleGroupBuilder
# Type alias for valid children of example groups.
# NOTE: `NestedExampleGroupBuilder` is used instead of `ExampleGroupBuilder`.
# That is because `RootExampleGroupBuilder` also inherits from this class,
# and the root example group can't be a child.
alias Child = ExampleFactory | NestedExampleGroupBuilder
private getter doubles = {} of Symbol => DoubleFactory
# Factories and builders for all examples and groups.
@children = [] of Child
# Hooks added to the group so far.
@before_all_hooks = [] of ->
@before_each_hooks = [] of ->
@after_all_hooks = [] of ->
@after_each_hooks = [] of ->
@around_each_hooks = [] of Proc(Nil) ->
# Pre and post conditions so far.
@pre_conditions = [] of ->
@post_conditions = [] of ->
# Adds a new example factory or group builder to this group.
def add_child(child : Child)
@children << child
end
# Adds a hook to run before all examples (and nested examples) in this group.
def add_before_all_hook(block : ->) : Nil
@before_all_hooks << block
end
# Adds a hook to run before each example (and nested example) in this group.
def add_before_each_hook(block : ->) : Nil
@before_each_hooks << block
end
# Adds a hook to run after all examples (and nested examples) in this group.
def add_after_all_hook(block : ->) : Nil
@after_all_hooks << block
end
# Adds a hook to run after each example (and nested example) in this group.
def add_after_each_hook(block : ->) : Nil
@after_each_hooks << block
end
# Adds a hook to run around each example (and nested example) in this group.
# The block of code will be given another proc as an argument.
# It is expected that the block will call the proc.
def add_around_each_hook(block : Proc(Nil) ->) : Nil
@around_each_hooks << block
end
# Adds a pre-condition to run at the start of every example in this group.
def add_pre_condition(block : ->) : Nil
@pre_conditions << block
end
# Adds a post-condition to run at the end of every example in this group.
def add_post_condition(block : ->) : Nil
@post_conditions << block
end
def add_double(id : Symbol, double_factory : DoubleFactory) : Nil
@doubles[id] = double_factory
end
# Constructs an `ExampleHooks` instance with all the hooks defined for this group.
# This method should be called only when the group is being built,
# otherwise some hooks may be missing.
private def hooks
ExampleHooks.new(
@before_all_hooks,
@before_each_hooks,
@after_all_hooks,
@after_each_hooks,
@around_each_hooks
)
end
# Constructs an `ExampleConditions` instance
# with all the pre- and post-conditions defined for this group.
# This method should be called only when the group is being built,
# otherwise some conditions may be missing.
private def conditions
ExampleConditions.new(@pre_conditions, @post_conditions)
end
end
end

View file

@ -0,0 +1,64 @@
require "../source"
require "../spec_builder"
module Spectator
module DSL
macro it(description, _source_file = __FILE__, _source_line = __LINE__, &block)
{% if block.is_a?(Nop) %}
{% if description.is_a?(Call) %}
def %run
{{description}}
end
{% else %}
{% raise "Unrecognized syntax: `it #{description}` at #{_source_file}:#{_source_line}" %}
{% end %}
{% else %}
def %run
{{block.body}}
end
{% end %}
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
::Spectator::SpecBuilder.add_example(
{{description.is_a?(StringLiteral) ? description : description.stringify}},
%source,
{{@type.name}}
) { |test| test.as({{@type.name}}).%run }
end
macro specify(description, &block)
it({{description}}) {{block}}
end
macro pending(description, _source_file = __FILE__, _source_line = __LINE__, &block)
{% if block.is_a?(Nop) %}
{% if description.is_a?(Call) %}
def %run
{{description}}
end
{% else %}
{% raise "Unrecognized syntax: `pending #{description}` at #{_source_file}:#{_source_line}" %}
{% end %}
{% else %}
def %run
{{block.body}}
end
{% end %}
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
::Spectator::SpecBuilder.add_pending_example(
{{description.is_a?(StringLiteral) ? description : description.stringify}},
%source,
{{@type.name}}
) { |test| test.as({{@type.name}}).%run }
end
macro skip(description, &block)
pending({{description}}) {{block}}
end
macro xit(description, &block)
pending({{description}}) {{block}}
end
end
end

144
src/spectator/dsl/groups.cr Normal file
View file

@ -0,0 +1,144 @@
require "../spec_builder"
module Spectator
module DSL
macro context(what, _source_file = __FILE__, _source_line = __LINE__, &block)
class Context%context < {{@type.id}}
{%
description = if what.is_a?(StringLiteral)
if what.starts_with?("#") || what.starts_with?(".")
what.id.symbolize
else
what
end
else
what.symbolize
end
%}
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
::Spectator::SpecBuilder.start_group({{description}}, %source)
{% if what.is_a?(Path) || what.is_a?(Generic) %}
macro described_class
{{what}}
end
def subject(*args)
described_class.new(*args)
end
{% end %}
{{block.body}}
::Spectator::SpecBuilder.end_group
end
end
macro describe(what, &block)
context({{what}}) {{block}}
end
macro sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block)
{% name = block.args.empty? ? :value.id : block.args.first.id %}
def %collection
{{collection}}
end
def %to_a
{% if count %}
%collection.first({{count}})
{% else %}
%collection.to_a
{% end %}
end
class Context%sample < {{@type.id}}
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, %source, :%sample, {{name.stringify}}) do |values|
sample = {{@type.id}}.new(values)
sample.%to_a
end
def {{name}}
@spectator_test_values.get_value(:%sample, typeof(%to_a.first))
end
{{block.body}}
::Spectator::SpecBuilder.end_group
end
end
macro random_sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block)
{% name = block.args.empty? ? :value.id : block.args.first.id %}
def %collection
{{collection}}
end
def %to_a
{% if count %}
%collection.first({{count}})
{% else %}
%collection.to_a
{% end %}
end
class Context%sample < {{@type.id}}
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
::Spectator::SpecBuilder.start_sample_group({{collection.stringify}}, %source, :%sample, {{name.stringify}}) do |values|
sample = {{@type.id}}.new(values)
collection = sample.%to_a
{% if count %}
collection.sample({{count}}, ::Spectator.random)
{% else %}
collection.shuffle(::Spectator.random)
{% end %}
end
def {{name}}
@spectator_test_values.get_value(:%sample, typeof(%to_a.first))
end
{{block.body}}
::Spectator::SpecBuilder.end_group
end
end
macro given(*assignments, &block)
context({{assignments.splat.stringify}}) do
{% for assignment in assignments %}
let({{assignment.target}}) { {{assignment.value}} }
{% end %}
{% # Trick to get the contents of the block as an array of nodes.
# If there are multiple expressions/statements in the block,
# then the body will be a `Expressions` type.
# If there's only one expression, then the body is just that.
body = if block.is_a?(Nop)
raise "Missing block for 'given'"
elsif block.body.is_a?(Expressions)
# Get the expressions, which is already an array.
block.body.expressions
else
# Wrap the expression in an array.
[block.body]
end %}
{% for item in body %}
# If the item starts with "it", then leave it as-is.
# Otherwise, prefix it with "it"
# and treat it as the one-liner "it" syntax.
{% if item.is_a?(Call) && item.name == :it.id %}
{{item}}
{% else %}
it {{item}}
{% end %}
{% end %}
end
end
end
end

View file

@ -0,0 +1,79 @@
module Spectator
module DSL
macro before_each(&block)
def %hook({{block.args.splat}}) : Nil
{{block.body}}
end
::Spectator::SpecBuilder.add_before_each_hook do |test, example|
cast_test = test.as({{@type.id}})
{% if block.args.empty? %}
cast_test.%hook
{% else %}
cast_test.%hook(example)
{% end %}
end
end
macro after_each(&block)
def %hook({{block.args.splat}}) : Nil
{{block.body}}
end
::Spectator::SpecBuilder.add_after_each_hook do |test, example|
cast_test = test.as({{@type.id}})
{% if block.args.empty? %}
cast_test.%hook
{% else %}
cast_test.%hook(example)
{% end %}
end
end
macro before_all(&block)
::Spectator::SpecBuilder.add_before_all_hook {{block}}
end
macro after_all(&block)
::Spectator::SpecBuilder.add_after_all_hook {{block}}
end
macro around_each(&block)
def %hook({{block.args.splat}}) : Nil
{{block.body}}
end
::Spectator::SpecBuilder.add_around_each_hook { |test, proc| test.as({{@type.id}}).%hook(proc) }
end
macro pre_condition(&block)
def %hook({{block.args.splat}}) : Nil
{{block.body}}
end
::Spectator::SpecBuilder.add_pre_condition do |test, example|
cast_test = test.as({{@type.id}})
{% if block.args.empty? %}
cast_test.%hook
{% else %}
cast_test.%hook(example)
{% end %}
end
end
macro post_condition(&block)
def %hook({{block.args.splat}}) : Nil
{{block.body}}
end
::Spectator::SpecBuilder.add_post_condition do |test, example|
cast_test = test.as({{@type.id}})
{% if block.args.empty? %}
cast_test.%hook
{% else %}
cast_test.%hook(example)
{% end %}
end
end
end
end

View file

@ -2,9 +2,8 @@ require "../matchers"
require "../test_block"
require "../test_value"
module Spectator::DSL
# Methods for defining matchers for expectations.
module MatcherDSL
module Spectator
module DSL
# Indicates that some value should equal another.
# The == operator is used for this check.
# The value passed to this method is the expected value.

View file

@ -1,38 +0,0 @@
module Spectator::DSL
# Standard example group builder.
# Creates groups of examples and nested groups.
class NestedExampleGroupBuilder < ExampleGroupBuilder
# Creates a new group builder.
# The value for *what* should be the context for the group.
#
# For example, in these samples:
# ```
# describe String do
# # ...
# context "with an empty string" do
# # ...
# end
# end
# ```
# The value would be "String" for the describe block
# and "with an empty string" for the context block.
# Use a `Symbol` when referencing a type name.
def initialize(@what : Symbol | String)
end
# Builds the example group.
# A new `NestedExampleGroup` will be returned
# which can have instances of `Example` and `ExampleGroup` nested in it.
# The *parent* should be the group that contains this group.
# The *sample_values* will be given to all of the examples (and groups) nested in this group.
def build(parent : ExampleGroup, sample_values : Internals::SampleValues) : NestedExampleGroup
NestedExampleGroup.new(@what, parent, hooks, conditions, doubles).tap do |group|
# Set the group's children to built versions of the children from this instance.
group.children = @children.map do |child|
# Build the child and up-cast to prevent type errors.
child.build(group, sample_values).as(ExampleComponent)
end
end
end
end
end

View file

@ -1,18 +0,0 @@
module Spectator::DSL
# Top-level example group builder.
# There should only be one instance of this class,
# and it should be at the top of the spec "tree".
class RootExampleGroupBuilder < ExampleGroupBuilder
# Creates a `RootExampleGroup` which can have instances of `Example` and `ExampleGroup` nested in it.
# The *sample_values* will be given to all of the examples (and groups) nested in this group.
def build(sample_values : Internals::SampleValues) : RootExampleGroup
RootExampleGroup.new(hooks, conditions, doubles).tap do |group|
# Set the group's children to built versions of the children from this instance.
group.children = @children.map do |child|
# Build the child and up-cast to prevent type errors.
child.build(group, sample_values).as(ExampleComponent)
end
end
end
end
end

View file

@ -1,73 +0,0 @@
require "./nested_example_group_builder"
module Spectator::DSL
# Specialized example group builder for "sample" groups.
# The type parameter `C` is the type to instantiate to create the collection.
# The type parameter `T` should be the type of each element in the sample collection.
# This builder creates a container group with groups inside for each item in the collection.
# The hooks are only defined for the container group.
# By doing so, the hooks are defined once, are inherited, and use less memory.
class SampleExampleGroupBuilder(C, T) < NestedExampleGroupBuilder
# Creates a new group builder.
# The value for *what* should be the text the user specified for the collection.
# The *collection_type* is the type to create that will produce the items.
# The *collection_builder* is a proc that takes an instance of *collection_type*
# and returns an actual array of items to create examples for.
# The *name* is the variable name that the user accesses the current collection item with.
#
# In this code:
# ```
# sample random_integers do |integer|
# # ...
# end
# ```
# The *what* would be "random_integers"
# and the collection would contain the items returned by calling *random_integers*.
# The *name* would be "integer".
#
# The *symbol* is passed along to the sample values
# so that the example code can retrieve the current item from the collection.
# The symbol should be unique.
def initialize(what : String, @collection_type : C.class, @collection_builder : C -> Array(T),
@name : String, @symbol : Symbol)
super(what)
end
# Builds the example group.
# A new `NestedExampleGroup` will be returned
# which can have instances of `Example` and `ExampleGroup` nested in it.
# The *parent* should be the group that contains this group.
# The *sample_values* will be given to all of the examples (and groups) nested in this group.
def build(parent : ExampleGroup, sample_values : Internals::SampleValues) : NestedExampleGroup
collection = @collection_builder.call(@collection_type.new(sample_values))
# This creates the container for the sub-groups.
# The hooks are defined here, instead of repeating for each sub-group.
NestedExampleGroup.new(@what, parent, hooks, conditions, doubles).tap do |group|
# Set the container group's children to be sub-groups for each item in the collection.
group.children = collection.map do |value|
# Create a sub-group for each item in the collection.
build_sub_group(group, sample_values, value).as(ExampleComponent)
end
end
end
# Builds a sub-group for one item in the collection.
# The *parent* should be the container group currently being built by the `#build` call.
# The *sample_values* should be the same as what was passed to the `#build` call.
# The *value* is the current item in the collection.
# The value will be added to the sample values for the sub-group,
# so it shouldn't be added prior to calling this method.
private def build_sub_group(parent : ExampleGroup, sample_values : Internals::SampleValues, value : T) : NestedExampleGroup
# Add the value to sample values for this sub-group.
sub_values = sample_values.add(@symbol, @name, value)
NestedExampleGroup.new(value.to_s, parent, ExampleHooks.empty, ExampleConditions.empty, {} of Symbol => DoubleFactory).tap do |group|
# Set the sub-group's children to built versions of the children from this instance.
group.children = @children.map do |child|
# Build the child and up-cast to prevent type errors.
child.build(group, sub_values).as(ExampleComponent)
end
end
end
end
end

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,58 @@
module Spectator
module DSL
macro let(name, &block)
def %value
{{block.body}}
end
@%wrapper : ::Spectator::ValueWrapper?
def {{name.id}}
if (wrapper = @%wrapper)
wrapper.as(::Spectator::TypedValueWrapper(typeof(%value))).value
else
%value.tap do |value|
@%wrapper = ::Spectator::TypedValueWrapper.new(value)
end
end
end
end
macro let!(name, &block)
# TODO: Doesn't work with late-defined values (let).
@%value = {{yield}}
def {{name.id}}
@%value
end
end
macro subject(&block)
{% if block.is_a?(Nop) %}
self.subject
{% else %}
let(:subject) {{block}}
{% end %}
end
macro subject(name, &block)
let({{name.id}}) {{block}}
def subject
{{name.id}}
end
end
macro subject!(&block)
let!(:subject) {{block}}
end
macro subject!(name, &block)
let!({{name.id}}) {{block}}
def subject
{{name.id}}
end
end
end
end

View file

@ -1,37 +0,0 @@
require "./runnable_example"
module Spectator
# Example that does nothing.
# This is to workaround a Crystal compiler bug.
# See: [Issue 4225](https://github.com/crystal-lang/crystal/issues/4225)
# If there are no concrete implementations of an abstract class,
# the compiler gives an error.
# The error indicates an abstract method is undefined.
# This class shouldn't be used, it's just to trick the compiler.
private class DummyExample < RunnableExample
# Dummy description.
def what : Symbol | String
"DUMMY"
end
# Dummy symbolic flag.
def symbolic? : Bool
false
end
# Dummy source.
def source : Source
Source.new(__FILE__, __LINE__)
end
# Dummy instance.
def instance
nil
end
# Dummy run that does nothing.
def run_instance
raise "You shouldn't be running this."
end
end
end

View file

@ -1,8 +1,9 @@
require "./example_component"
require "./test_wrapper"
module Spectator
# Base class for all types of examples.
# Concrete types must implement the `#run_impl, `#what`, `#instance`, and `#source` methods.
# Concrete types must implement the `#run_impl` method.
abstract class Example < ExampleComponent
@finished = false
@ -15,10 +16,23 @@ module Spectator
getter group : ExampleGroup
# Retrieves the internal wrapped instance.
abstract def instance
protected getter test_wrapper : TestWrapper
# Source where the example originated from.
abstract def source : Source
def source : Source
@test_wrapper.source
end
def description : String | Symbol
@test_wrapper.description
end
def symbolic? : Bool
description = @test_wrapper.description
description.starts_with?('#') || description.starts_with?('.')
end
abstract def run_impl
protected getter sample_values : Internals::SampleValues
@ -28,17 +42,14 @@ module Spectator
# An exception is raised if an attempt is made to run it more than once.
def run : Result
raise "Attempted to run example more than once (#{self})" if finished?
@finished = true
run_impl
ensure
@finished = true
end
# Implementation-specific for running the example code.
private abstract def run_impl : Result
# Creates the base of the example.
# The group should be the example group the example belongs to.
# The *sample_values* are passed to the example code.
def initialize(@group, @sample_values)
def initialize(@group, @test_wrapper)
end
# Indicates there is only one example to run.
@ -57,7 +68,7 @@ module Spectator
def to_s(io)
@group.to_s(io)
io << ' ' unless symbolic? && @group.symbolic?
io << what
io << description
end
# Creates the JSON representation of the example,

View file

@ -3,7 +3,13 @@ module Spectator
# This is used as the base node type for the composite design pattern.
abstract class ExampleComponent
# Text that describes the context or test.
abstract def what : Symbol | String
abstract def description : Symbol | String
def full_description
to_s
end
abstract def source : Source
# Indicates whether the example (or group) has been completely run.
abstract def finished? : Bool

View file

@ -10,28 +10,32 @@ module Spectator
# This will effectively run nothing extra while running a test.
def self.empty
new(
[] of ->,
[] of ->
[] of TestMetaMethod,
[] of TestMetaMethod
)
end
# Creates a new set of conditions.
def initialize(
@pre_conditions : Array(->),
@post_conditions : Array(->)
@pre_conditions : Array(TestMetaMethod),
@post_conditions : Array(TestMetaMethod)
)
end
# Runs all pre-condition checks.
# These should be run before every test.
def run_pre_conditions
@pre_conditions.each &.call
def run_pre_conditions(wrapper : TestWrapper, example : Example)
@pre_conditions.each do |hook|
wrapper.call(hook, example)
end
end
# Runs all post-condition checks.
# These should be run after every test.
def run_post_conditions
@post_conditions.each &.call
def run_post_conditions(wrapper : TestWrapper, example : Example)
@post_conditions.each do |hook|
wrapper.call(hook, example)
end
end
end
end

View file

@ -15,13 +15,7 @@ module Spectator
include Enumerable(ExampleComponent)
include Iterable(ExampleComponent)
# Creates the example group.
# The hooks are stored to be triggered later.
def initialize(@hooks : ExampleHooks, @conditions : ExampleConditions, @doubles : Hash(Symbol, DSL::DoubleFactory))
@example_count = 0
@before_all_hooks_run = false
@after_all_hooks_run = false
end
@example_count = 0
# Retrieves the children in the group.
# This only returns the direct descends (non-recursive).
@ -45,6 +39,11 @@ module Spectator
@doubles[id].build(sample_values)
end
getter context
def initialize(@context : TestContext)
end
# Yields each direct descendant.
def each
children.each do |child|
@ -128,72 +127,5 @@ module Spectator
def finished? : Bool
children.all?(&.finished?)
end
# Runs all of the "before-each" and "before-all" hooks.
# This should run prior to every example in the group.
def run_before_hooks
run_before_all_hooks
run_before_each_hooks
end
# 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.
protected def run_before_all_hooks : Nil
return if @before_all_hooks_run
@hooks.run_before_all
@before_all_hooks_run = true
end
# Runs all of the "before-each" hooks.
# This method should run prior to every example in the group.
protected def run_before_each_hooks : Nil
@hooks.run_before_each
end
# Runs all of the "after-all" and "after-each" hooks.
# This should run following every example in the group.
def run_after_hooks
run_after_each_hooks
run_after_all_hooks
end
# 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.
protected def run_after_all_hooks(ignore_unfinished = false) : Nil
return if @after_all_hooks_run
return unless ignore_unfinished || finished?
@hooks.run_after_all
@after_all_hooks_run = true
end
# Runs all of the "after-each" hooks.
# This method should run following every example in the group.
protected def run_after_each_hooks : Nil
@hooks.run_after_each
end
# 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.
def wrap_around_each_hooks(&block : ->) : ->
@hooks.wrap_around_each(&block)
end
# Runs all of the pre-conditions for an example.
def run_pre_conditions
@conditions.run_pre_conditions
end
# Runs all of the post-conditions for an example.
def run_post_conditions
@conditions.run_post_conditions
end
end
end

View file

@ -1,4 +1,6 @@
module Spectator
alias TestMetaMethod = ::SpectatorTest, Example ->
# Collection of hooks that run at various times throughout testing.
# A hook is just a `Proc` (code block) that runs at a specified time.
class ExampleHooks
@ -7,20 +9,20 @@ module Spectator
def self.empty
new(
[] of ->,
[] of TestMetaMethod,
[] of ->,
[] of ->,
[] of ->,
[] of Proc(Nil) ->
[] of TestMetaMethod,
[] of ::SpectatorTest, Proc(Nil) ->
)
end
# Creates a new set of hooks.
def initialize(
@before_all : Array(->),
@before_each : Array(->),
@before_each : Array(TestMetaMethod),
@after_all : Array(->),
@after_each : Array(->),
@around_each : Array(Proc(Nil) ->)
@after_each : Array(TestMetaMethod),
@around_each : Array(::SpectatorTest, Proc(Nil) ->)
)
end
@ -32,8 +34,10 @@ module Spectator
# Runs all "before-each" hooks.
# These hooks should be run every time before each example in a group.
def run_before_each
@before_each.each &.call
def run_before_each(wrapper : TestWrapper, example : Example)
@before_each.each do |hook|
wrapper.call(hook, example)
end
end
# Runs all "after-all" hooks.
@ -44,27 +48,28 @@ module Spectator
# Runs all "after-all" hooks.
# These hooks should be run every time after each example in a group.
def run_after_each
@after_each.each &.call
def run_after_each(wrapper : TestWrapper, example : Example)
@after_each.each do |hook|
wrapper.call(hook, example)
end
end
# 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.
def wrap_around_each(&block : ->) : ->
def wrap_around_each(test, block : ->)
wrapper = block
# Must wrap in reverse order,
# otherwise hooks will run in the wrong order.
@around_each.reverse_each do |hook|
wrapper = wrap_proc(hook, wrapper)
wrapper = wrap_foo(test, hook, wrapper)
end
wrapper
end
# Utility method for wrapping one proc with another.
private def wrap_proc(inner : Proc(Nil) ->, wrapper : ->)
->{ inner.call(wrapper) }
private def wrap_foo(test, hook, wrapper)
->{ hook.call(test, wrapper) }
end
end
end

View file

@ -40,7 +40,7 @@ module Spectator::Expectations
# Reports an expectation to the current harness.
private def report(match_data : Matchers::MatchData)
expectation = Expectation.new(match_data, @source)
Internals::Harness.current.report_expectation(expectation)
Harness.current.report_expectation(expectation)
end
end
end

View file

@ -28,7 +28,7 @@ module Spectator::Formatting
# Produces a single character output based on a result.
def end_example(result)
@previous_hierarchy.size.times { @io.print INDENT }
@io.puts result.call(Color) { result.example.what }
@io.puts result.call(Color) { result.example.description }
end
# Produces a list of groups making up the hierarchy for an example.
@ -56,7 +56,7 @@ module Spectator::Formatting
private def print_sub_hierarchy(index, sub_hierarchy)
sub_hierarchy.each do |group|
index.times { @io.print INDENT }
@io.puts group.what
@io.puts group.description
index += 1
end
end

View file

@ -1,4 +1,4 @@
module Spectator::Internals
module Spectator
# Helper class that acts as a gateway between example code and the test framework.
# Every example must be invoked by passing it to `#run`.
# This sets up the harness so that the example code can use it.
@ -9,7 +9,7 @@ module Spectator::Internals
# ```
# Then from the example code, the harness can be accessed via `#current` like so:
# ```
# harness = ::Spectator::Internals::Harness.current
# harness = ::Spectator::Harness.current
# # Do something with the harness.
# ```
# Of course, the end-user shouldn't see this or work directly with the harness.
@ -34,6 +34,11 @@ module Spectator::Internals
# Retrieves the current running example.
getter example : Example
# Retrieves the group for the current running example.
def group
example.group
end
# Reports the outcome of an expectation.
# An exception will be raised when a failing result is given.
def report_expectation(expectation : Expectations::Expectation) : Nil

View file

@ -11,18 +11,17 @@
require "openssl"
# First the sub-modules.
require "./internals"
require "./dsl"
require "./expectations"
require "./matchers"
require "./formatting"
# Then all of the top-level types.
require "./spec_builder"
require "./example_component"
require "./example"
require "./runnable_example"
require "./pending_example"
require "./dummy_example"
require "./example_conditions"
require "./example_hooks"

View file

@ -1,7 +0,0 @@
require "./internals/*"
module Spectator
# Utilities and black magic (hacks) employed by the testing framework.
module Internals
end
end

View file

@ -6,92 +6,31 @@ module Spectator
class NestedExampleGroup < ExampleGroup
# Description from the user of the group's contents.
# This is a symbol when referencing a type.
getter what : Symbol | String
getter description : Symbol | String
getter source : Source
# Group that this is nested in.
getter parent : ExampleGroup
# Creates a new example group.
# The *what* argument is a description from the user.
# The *description* argument is a description from the user.
# The *parent* should contain this group.
# After creating this group, the parent's children should be updated.
# The parent's children must contain this group,
# otherwise there may be unexpected behavior.
# The *hooks* are stored to be triggered later.
def initialize(@what, @parent, hooks : ExampleHooks, conditions : ExampleConditions, doubles : Hash(Symbol, DSL::DoubleFactory))
super(hooks, conditions, doubles)
def initialize(@description, @source, @parent, context)
super(context)
end
# Indicates wheter the group references a type.
def symbolic? : Bool
@what.is_a?(Symbol)
end
# 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.
# Parent "before-all" hooks will be run first.
protected def run_before_all_hooks : Nil
parent.run_before_all_hooks
super
end
# Runs all of the "before-each" hooks.
# This method should run prior to every example in the group.
# Parent "before-each" hooks will be run first.
protected def run_before_each_hooks : Nil
parent.run_before_each_hooks
super
end
# 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.
# Parent "after-all" hooks will be run last.
protected def run_after_all_hooks(ignore_unfinished = false) : Nil
super
parent.run_after_all_hooks(ignore_unfinished)
end
# Runs all of the "after-each" hooks.
# This method should run following every example in the group.
# Parent "after-each" hooks will be run last.
protected def run_after_each_hooks : Nil
super
parent.run_after_each_hooks
end
# 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.
# Parent "around-each" hooks will be in the outermost wrappings.
def wrap_around_each_hooks(&block : ->) : ->
wrapper = super(&block)
parent.wrap_around_each_hooks(&wrapper)
end
# Runs all of the pre-condition checks.
# This method should run prior to every example in the group.
# Parent pre-conditions will be checked first.
def run_pre_conditions : Nil
parent.run_pre_conditions
super
end
# Runs all of the post-condition checks.
# This method should run following every example in the group.
# Parent post-conditions will be checked last.
def run_post_conditions : Nil
super
parent.run_post_conditions
@description.is_a?(Symbol)
end
# Creates a string representation of the group.
# The string consists of `#what` appended to the parent.
# The string consists of `#description` appended to the parent.
# This results in a string like:
# ```text
# Foo#bar does something
@ -109,7 +48,7 @@ module Spectator
def to_s(io)
parent.to_s(io)
io << ' ' unless (symbolic? || parent.is_a?(RootExampleGroup)) && parent.symbolic?
io << what
io << description
end
end
end

View file

@ -3,7 +3,7 @@ require "./example"
module Spectator
# Common class for all examples marked as pending.
# This class will not run example code.
abstract class PendingExample < Example
class PendingExample < Example
# Returns a pending result.
private def run_impl : Result
PendingResult.new(self)

View file

@ -5,8 +5,12 @@ module Spectator
# The root has no parent.
class RootExampleGroup < ExampleGroup
# Dummy value - this should never be used.
def what : Symbol | String
"ROOT"
def description : Symbol | String
:root
end
def source : Source
Source.new(__FILE__, __LINE__)
end
# Indicates that the group is symbolic.

View file

@ -1,78 +1,39 @@
require "./example"
module Spectator
# Common base for all examples that can be run.
# This class includes all the logic for running example hooks,
# Includes all the logic for running example hooks,
# the example code, and capturing a result.
# Sub-classes need to implement the `#what` and `#run_instance` methods.
abstract class RunnableExample < Example
class RunnableExample < Example
# Runs the example, hooks, and captures the result
# and translates to a usable result.
def run_impl : Result
result = capture_result
expectations = Internals::Harness.current.expectations
expectations = Harness.current.expectations
translate_result(result, expectations)
end
# Runs the actual test code.
private abstract def run_instance
# Runs the hooks that should be performed before starting the test code.
private def run_before_hooks
group.run_before_hooks
rescue ex
# If an error occurs in the before hooks, skip running the example.
raise Exception.new("Error encountered while running before hooks", ex)
end
# Runs the hooks that should be performed after the test code finishes.
private def run_after_hooks
group.run_after_hooks
rescue ex
# If an error occurs in the after hooks, elevate it to abort testing.
raise Exception.new("Error encountered while running after hooks", ex)
end
# Runs all hooks and the example code.
# A captured result is returned.
private def capture_result
context = group.context
ResultCapture.new.tap do |result|
# Get the proc that will call around-each hooks and the example.
wrapper = wrap_run_example(result)
run_before_hooks
run_wrapper(wrapper)
run_after_hooks
end
end
private def run_wrapper(wrapper)
wrapper.call
rescue ex
# If an error occurs calling the wrapper,
# it means it came from the "around-each" hooks.
# This is because the test code is completely wrapped with a begin/rescue block.
raise Exception.new("Error encountered while running around hooks", ex)
end
# Creates a proc that runs the test code
# and captures the result.
private def wrap_run_example(result)
# Wrap the method that runs and captures
# the test code with the around-each hooks.
group.wrap_around_each_hooks do
context.run_before_hooks(self)
run_example(result)
context.run_after_hooks(self)
end
end
# Runs the test code and captures the result.
private def run_example(result)
context = group.context
wrapper = test_wrapper.around_hook(context)
# Capture how long it takes to run the test code.
result.elapsed = Time.measure do
begin
group.run_pre_conditions
run_instance # Actually run the example code.
group.run_post_conditions
context.run_pre_conditions(self)
wrapper.call
context.run_post_conditions(self)
rescue ex # Catch all errors and handle them later.
result.error = ex
end

View file

@ -1,3 +1,5 @@
require "./harness"
module Spectator
# Main driver for executing tests and feeding results to formatters.
class Runner
@ -35,7 +37,7 @@ module Spectator
result = run_example(example).as(Result)
results << result
if @config.fail_fast? && result.is_a?(FailedResult)
example.group.run_after_all_hooks(ignore_unfinished: true)
example.group.context.run_after_all_hooks(example.group, ignore_unfinished: true)
break
end
end
@ -57,7 +59,7 @@ module Spectator
result = if @config.dry_run? && example.is_a?(RunnableExample)
dry_run_result(example)
else
Internals::Harness.run(example)
Harness.run(example)
end
@config.each_formatter(&.end_example(result))
result

View file

@ -1,31 +1,14 @@
module Spectator::DSL
require "./spec_builder/*"
module Spectator
# Global builder used to create the runtime instance of the spec.
# The DSL methods call into this module to generate parts of the spec.
# Once the DSL is done, the `#build` method can be invoked
# to create the entire spec as a runtime instance.
module Builder
module SpecBuilder
extend self
# Root group that contains all examples and groups in the spec.
private class_getter root_group = RootExampleGroupBuilder.new
# Stack for tracking the current group the spec is working in.
# The last item (top of the stack) is the current group.
# The first item (bottom of the stack) is the root group (`#root_group`).
# The root group should never be popped.
@@group_stack = Array(ExampleGroupBuilder).new(1, root_group)
# Retrieves the current group the spec is working in.
private def current_group
@@group_stack.last
end
# Adds a new group to the stack.
# Calling this method indicates the spec has entered a nested group.
private def push_group(group : NestedExampleGroupBuilder)
current_group.add_child(group)
@@group_stack.push(group)
end
@@stack = ExampleGroupStack.new
# Begins a new nested group in the spec.
# A corresponding `#end_group` call must be made
@ -34,7 +17,7 @@ module Spectator::DSL
# as arguments to this method are passed directly to it.
def start_group(*args) : Nil
group = NestedExampleGroupBuilder.new(*args)
push_group(group)
@@stack.push(group)
end
# Begins a new sample group in the spec -
@ -43,9 +26,9 @@ module Spectator::DSL
# when the group being started is finished.
# See `SampleExampleGroupBuilder#initialize` for the arguments
# as arguments to this method are passed directly to it.
def start_sample_group(*args) : Nil
group = SampleExampleGroupBuilder.new(*args)
push_group(group)
def start_sample_group(*args, &block : TestValues -> Array(T)) : Nil forall T
group = SampleExampleGroupBuilder(T).new(*args, block)
@@stack.push(group)
end
# Marks the end of a group in the spec.
@ -53,52 +36,64 @@ module Spectator::DSL
# It is also important to line up the start and end calls.
# Otherwise examples might get placed into wrong groups.
def end_group : Nil
@@group_stack.pop
@@stack.pop
end
# Adds an example type to the current group.
# The class name of the example should be passed as an argument.
# The example will be instantiated later.
def add_example(example_type : Example.class) : Nil
factory = ExampleFactory.new(example_type)
current_group.add_child(factory)
def add_example(description : String, source : Source,
example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil
builder = ->(values : TestValues) { example_type.new(values).as(::SpectatorTest) }
factory = RunnableExampleBuilder.new(description, source, builder, runner)
@@stack.current.add_child(factory)
end
# Adds an example type to the current group.
# The class name of the example should be passed as an argument.
# The example will be instantiated later.
def add_pending_example(description : String, source : Source,
example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil
builder = ->(values : TestValues) { example_type.new(values).as(::SpectatorTest) }
factory = PendingExampleBuilder.new(description, source, builder, runner)
@@stack.current.add_child(factory)
end
# Adds a block of code to run before all examples in the current group.
def add_before_all_hook(&block : ->) : Nil
current_group.add_before_all_hook(block)
@@stack.current.add_before_all_hook(block)
end
# Adds a block of code to run before each example in the current group.
def add_before_each_hook(&block : ->) : Nil
current_group.add_before_each_hook(block)
def add_before_each_hook(&block : TestMetaMethod) : Nil
@@stack.current.add_before_each_hook(block)
end
# Adds a block of code to run after all examples in the current group.
def add_after_all_hook(&block : ->) : Nil
current_group.add_after_all_hook(block)
@@stack.current.add_after_all_hook(block)
end
# Adds a block of code to run after each example in the current group.
def add_after_each_hook(&block : ->) : Nil
current_group.add_after_each_hook(block)
def add_after_each_hook(&block : TestMetaMethod) : Nil
@@stack.current.add_after_each_hook(block)
end
# Adds a block of code to run before and after each example in the current group.
# The block of code will be given another proc as an argument.
# It is expected that the block will call the proc.
def add_around_each_hook(&block : Proc(Nil) ->) : Nil
current_group.add_around_each_hook(block)
# The block of code will be given another hook as an argument.
# It is expected that the block will call the hook.
def add_around_each_hook(&block : ::SpectatorTest, Proc(Nil) ->) : Nil
@@stack.current.add_around_each_hook(block)
end
# Adds a pre-condition to run at the start of every example in the current group.
def add_pre_condition(&block : ->) : Nil
current_group.add_pre_condition(block)
def add_pre_condition(&block : TestMetaMethod) : Nil
@@stack.current.add_pre_condition(block)
end
# Adds a post-condition to run at the end of every example in the current group.
def add_post_condition(&block : ->) : Nil
current_group.add_post_condition(block)
def add_post_condition(&block : TestMetaMethod) : Nil
@@stack.current.add_post_condition(block)
end
def add_double(id : Symbol, double_type : Double.class) : Nil
@ -109,7 +104,7 @@ module Spectator::DSL
# Builds the entire spec and returns it as a test suite.
# This should be called only once after the entire spec has been defined.
protected def build(filter : ExampleFilter) : TestSuite
group = root_group.build(Internals::SampleValues.empty)
group = @@stack.root.build
TestSuite.new(group, filter)
end
end

View file

@ -0,0 +1,19 @@
require "../../spectator_test"
require "../test_values"
require "../test_wrapper"
module Spectator::SpecBuilder
abstract class ExampleBuilder
alias FactoryMethod = TestValues -> ::SpectatorTest
def initialize(@description : String, @source : Source, @builder : FactoryMethod, @runner : TestMethod)
end
abstract def build(group) : ExampleComponent
private def build_test_wrapper(group)
test = @builder.call(group.context.values)
TestWrapper.new(@description, @source, test, @runner)
end
end
end

View file

@ -0,0 +1,67 @@
require "../test_context"
require "./example_builder"
module Spectator::SpecBuilder
abstract class ExampleGroupBuilder
alias Child = NestedExampleGroupBuilder | ExampleBuilder
private getter children = Deque(Child).new
@before_each_hooks = Deque(TestMetaMethod).new
@after_each_hooks = Deque(TestMetaMethod).new
@before_all_hooks = Deque(->).new
@after_all_hooks = Deque(->).new
@around_each_hooks = Deque(::SpectatorTest, Proc(Nil) ->).new
@pre_conditions = Deque(TestMetaMethod).new
@post_conditions = Deque(TestMetaMethod).new
def add_child(child : Child)
@children << child
end
def add_before_each_hook(hook : TestMetaMethod)
@before_each_hooks << hook
end
def add_after_each_hook(hook : TestMetaMethod)
@after_each_hooks << hook
end
def add_before_all_hook(hook : ->)
@before_all_hooks << hook
end
def add_after_all_hook(hook : ->)
@after_all_hooks << hook
end
def add_around_each_hook(hook : ::SpectatorTest, Proc(Nil) ->)
@around_each_hooks << hook
end
def add_pre_condition(hook : TestMetaMethod)
@pre_conditions << hook
end
def add_post_condition(hook : TestMetaMethod)
@post_conditions << hook
end
private def build_hooks
ExampleHooks.new(
@before_all_hooks.to_a,
@before_each_hooks.to_a,
@after_all_hooks.to_a,
@after_each_hooks.to_a,
@around_each_hooks.to_a
)
end
private def build_conditions
ExampleConditions.new(
@pre_conditions.to_a,
@post_conditions.to_a
)
end
end
end

View file

@ -0,0 +1,28 @@
require "./root_example_group_builder"
require "./nested_example_group_builder"
module Spectator::SpecBuilder
struct ExampleGroupStack
getter root
def initialize
@root = RootExampleGroupBuilder.new
@stack = Deque(ExampleGroupBuilder).new(1, @root)
end
def current
@stack.last
end
def push(group : NestedExampleGroupBuilder)
current.add_child(group)
@stack.push(group)
end
def pop
raise "Attempted to pop root example group from stack" if current == root
@stack.pop
end
end
end

View file

@ -0,0 +1,18 @@
require "../test_context"
require "./example_group_builder"
module Spectator::SpecBuilder
class NestedExampleGroupBuilder < ExampleGroupBuilder
def initialize(@description : String | Symbol, @source : Source)
end
def build(parent_group)
context = TestContext.new(parent_group.context, build_hooks, build_conditions, parent_group.context.values)
NestedExampleGroup.new(@description, @source, parent_group, context).tap do |group|
group.children = children.map do |child|
child.build(group).as(ExampleComponent)
end
end
end
end
end

View file

@ -0,0 +1,10 @@
require "./example_builder"
module Spectator::SpecBuilder
class PendingExampleBuilder < ExampleBuilder
def build(group) : ExampleComponent
wrapper = build_test_wrapper(group)
PendingExample.new(group, wrapper).as(ExampleComponent)
end
end
end

View file

@ -0,0 +1,15 @@
require "../test_values"
require "./example_group_builder"
module Spectator::SpecBuilder
class RootExampleGroupBuilder < ExampleGroupBuilder
def build
context = TestContext.new(nil, build_hooks, build_conditions, TestValues.empty)
RootExampleGroup.new(context).tap do |group|
group.children = children.map do |child|
child.build(group).as(ExampleComponent)
end
end
end
end
end

View file

@ -0,0 +1,10 @@
require "./example_builder"
module Spectator::SpecBuilder
class RunnableExampleBuilder < ExampleBuilder
def build(group) : ExampleComponent
wrapper = build_test_wrapper(group)
RunnableExample.new(group, wrapper).as(ExampleComponent)
end
end
end

View file

@ -0,0 +1,30 @@
require "./nested_example_group_builder"
module Spectator::SpecBuilder
class SampleExampleGroupBuilder(T) < NestedExampleGroupBuilder
def initialize(description : String | Symbol, source : Source, @id : Symbol, @label : String, @collection_builder : TestValues -> Array(T))
super(description, source)
end
def build(parent_group)
values = parent_group.context.values
collection = @collection_builder.call(values)
context = TestContext.new(parent_group.context, build_hooks, build_conditions, values)
NestedExampleGroup.new(@description, @source, parent_group, context).tap do |group|
group.children = collection.map do |element|
build_sub_group(group, element).as(ExampleComponent)
end
end
end
private def build_sub_group(parent_group, element)
values = parent_group.context.values.add(@id, @description.to_s, element)
context = TestContext.new(parent_group.context, ExampleHooks.empty, ExampleConditions.empty, values)
NestedExampleGroup.new("#{@label} = #{element.inspect}", @source, parent_group, context).tap do |group|
group.children = children.map do |child|
child.build(group).as(ExampleComponent)
end
end
end
end
end

View file

@ -0,0 +1,74 @@
require "./example_hooks"
require "./test_values"
module Spectator
class TestContext
getter values
def initialize(@parent : TestContext?, @hooks : ExampleHooks, @conditions : ExampleConditions, @values : TestValues)
@before_all_hooks_run = false
@after_all_hooks_run = false
end
def run_before_hooks(example : Example)
run_before_all_hooks
run_before_each_hooks(example)
end
protected def run_before_all_hooks
return if @before_all_hooks_run
@parent.try &.run_before_all_hooks
@hooks.run_before_all
ensure
@before_all_hooks_run = true
end
protected def run_before_each_hooks(example : Example)
@parent.try &.run_before_each_hooks(example)
@hooks.run_before_each(example.test_wrapper, example)
end
def run_after_hooks(example : Example)
run_after_each_hooks(example)
run_after_all_hooks(example.group)
end
protected def run_after_all_hooks(group : ExampleGroup, *, ignore_unfinished = false)
return if @after_all_hooks_run
return unless ignore_unfinished || group.finished?
@hooks.run_after_all
@parent.try do |parent_context|
parent_group = group.as(NestedExampleGroup).parent
parent_context.run_after_all_hooks(parent_group, ignore_unfinished: ignore_unfinished)
end
ensure
@after_all_hooks_run = true
end
protected def run_after_each_hooks(example : Example)
@hooks.run_after_each(example.test_wrapper, example)
@parent.try &.run_after_each_hooks(example)
end
def wrap_around_each_hooks(test, &block : ->)
wrapper = @hooks.wrap_around_each(test, block)
if (parent = @parent)
parent.wrap_around_each_hooks(test, &wrapper)
else
wrapper
end
end
def run_pre_conditions(example)
@parent.try &.run_pre_conditions(example)
@conditions.run_pre_conditions(example.test_wrapper, example)
end
def run_post_conditions(example)
@conditions.run_post_conditions(example.test_wrapper, example)
@parent.try &.run_post_conditions(example)
end
end
end

View file

@ -1,10 +1,11 @@
require "./typed_value_wrapper"
require "./value_wrapper"
module Spectator::Internals
module Spectator
# Collection of test values supplied to examples.
# Each value is labeled by a symbol that the example knows.
# The values also come with a name that can be given to humans.
struct SampleValues
struct TestValues
# Creates an empty set of sample values.
def self.empty
new({} of Symbol => Entry)
@ -17,9 +18,9 @@ module Spectator::Internals
# Adds a new value by duplicating the current set and adding to it.
# The new sample values with the additional value is returned.
# The original set of sample values is not modified.
def add(id : Symbol, name : String, value : T) : SampleValues forall T
def add(id : Symbol, name : String, value : T) : TestValues forall T
wrapper = TypedValueWrapper(T).new(value)
SampleValues.new(@values.merge({
TestValues.new(@values.merge({
id => Entry.new(name, wrapper),
}))
end
@ -58,7 +59,6 @@ module Spectator::Internals
end
# This must be after `Entry` is defined.
# Could be a Cyrstal compiler bug?
include Enumerable(Entry)
end
end

View file

@ -0,0 +1,36 @@
require "../spectator_test"
require "./source"
module Spectator
alias TestMethod = ::SpectatorTest ->
# Stores information about a end-user test.
# Used to instantiate tests and run them.
struct TestWrapper
# Description the user provided for the test.
getter description
# Location of the test in source code.
getter source
# Creates a wrapper for the test.
def initialize(@description : String, @source : Source, @test : ::SpectatorTest, @runner : TestMethod)
end
def run
call(@runner)
end
def call(method : TestMethod) : Nil
method.call(@test)
end
def call(method, *args) : Nil
method.call(@test, *args)
end
def around_hook(context : TestContext)
context.wrap_around_each_hooks(@test) { run }
end
end
end

View file

@ -1,6 +1,6 @@
require "./value_wrapper"
module Spectator::Internals
module Spectator
# Implementation of a value wrapper for a specific type.
# Instances of this class should be created to wrap values.
# Then the wrapper should be stored as a `ValueWrapper`

View file

@ -1,9 +1,7 @@
module Spectator::Internals
module Spectator
# Base class for proxying test values to examples.
# This abstraction is required for inferring types.
# The DSL makes heavy use of this to defer types.
abstract class ValueWrapper
# Retrieves the underlying value.
abstract def value
end
end

11
src/spectator_test.cr Normal file
View file

@ -0,0 +1,11 @@
require "./spectator/dsl"
# Root-level class that all tests inherit from and are contained in.
# This class is intentionally outside of the scope of Spectator,
# so that the namespace isn't leaked into tests unexpectedly.
class SpectatorTest
include ::Spectator::DSL
def initialize(@spectator_test_values : ::Spectator::TestValues)
end
end