mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Merge remote-tracking branch 'origin/release/0.9' into mocks-and-doubles
This commit is contained in:
commit
8c180e818f
88 changed files with 838 additions and 9229 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
64
src/spectator/dsl/examples.cr
Normal file
64
src/spectator/dsl/examples.cr
Normal 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
144
src/spectator/dsl/groups.cr
Normal 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
|
79
src/spectator/dsl/hooks.cr
Normal file
79
src/spectator/dsl/hooks.cr
Normal 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
|
|
@ -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.
|
|
@ -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
|
|
@ -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
|
|
@ -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
58
src/spectator/dsl/values.cr
Normal file
58
src/spectator/dsl/values.cr
Normal 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
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
require "./internals/*"
|
||||
|
||||
module Spectator
|
||||
# Utilities and black magic (hacks) employed by the testing framework.
|
||||
module Internals
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
19
src/spectator/spec_builder/example_builder.cr
Normal file
19
src/spectator/spec_builder/example_builder.cr
Normal 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
|
67
src/spectator/spec_builder/example_group_builder.cr
Normal file
67
src/spectator/spec_builder/example_group_builder.cr
Normal 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
|
28
src/spectator/spec_builder/example_group_stack.cr
Normal file
28
src/spectator/spec_builder/example_group_stack.cr
Normal 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
|
18
src/spectator/spec_builder/nested_example_group_builder.cr
Normal file
18
src/spectator/spec_builder/nested_example_group_builder.cr
Normal 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
|
10
src/spectator/spec_builder/pending_example_builder.cr
Normal file
10
src/spectator/spec_builder/pending_example_builder.cr
Normal 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
|
15
src/spectator/spec_builder/root_example_group_builder.cr
Normal file
15
src/spectator/spec_builder/root_example_group_builder.cr
Normal 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
|
10
src/spectator/spec_builder/runnable_example_builder.cr
Normal file
10
src/spectator/spec_builder/runnable_example_builder.cr
Normal 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
|
30
src/spectator/spec_builder/sample_example_group_builder.cr
Normal file
30
src/spectator/spec_builder/sample_example_group_builder.cr
Normal 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
|
74
src/spectator/test_context.cr
Normal file
74
src/spectator/test_context.cr
Normal 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
|
|
@ -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
|
36
src/spectator/test_wrapper.cr
Normal file
36
src/spectator/test_wrapper.cr
Normal 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
|
|
@ -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`
|
|
@ -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
11
src/spectator_test.cr
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue