Gut factories and example structure code

This commit is contained in:
Michael Miller 2019-08-31 13:12:40 -06:00
parent b8e125e38f
commit 19913a28d1
6 changed files with 2 additions and 328 deletions

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* is passed to `Example#initialize`.
def build(group : ExampleGroup, _sample_values : Internals::SampleValues) : Example
@example_type.new(group)
end
end
end

View file

@ -1,87 +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
# 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
# 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

@ -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, sample_values).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, sample_values).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)
# 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, sample_values).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, sub_values).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

View file

@ -1426,21 +1426,7 @@ module Spectator::DSL
_spectator_test(Test%example, %run) {{block}}
{% end %}
# Create a class derived from `RunnableExample` to run the test code.
_spectator_example(Example%example, Test%example, ::Spectator::RunnableExample, {{what}}) do
# Source where the example originated from.
def source
::Spectator::Source.new({{_source_file}}, {{_source_line}})
end
# Implement abstract method to run the wrapped example block.
protected def run_instance
@instance.%run
end
end
# Add the example to the current group.
::Spectator::DSL::Builder.add_example(Example%example)
# TODO
end
# Creates an example, or a test case.
@ -1500,19 +1486,7 @@ module Spectator::DSL
# By creating a `#pending` test, the code will be referenced.
# Thus, forcing the compiler to at least process the code, even if it isn't run.
macro pending(what, _source_file = __FILE__, _source_line = __LINE__, &block)
# Create the wrapper class for the test code.
_spectator_test(Test%example, %run) {{block}}
# Create a class derived from `PendingExample` to skip the test code.
_spectator_example(Example%example, Test%example, ::Spectator::PendingExample, {{what}}) do
# Source where the example originated from.
def source
::Spectator::Source.new({{_source_file}}, {{_source_line}})
end
end
# Add the example to the current group.
::Spectator::DSL::Builder.add_example(Example%example)
# TODO
end
# Creates an example, or a test case, that does not run.
@ -1553,74 +1527,5 @@ module Spectator::DSL
macro xit(&block)
pending({{block.body.stringify}}) {{block}}
end
# Creates a wrapper class for test code.
# The class serves multiple purposes, mostly dealing with scope.
# 1. Include the parent modules as mix-ins.
# 2. Enable DSL specific to examples.
# 3. Isolate methods in `Example` from the test code.
#
# Since the names are generated, and macros can't return values,
# the names for everything must be passed in as arguments.
# The *class_name* argument is the name of the class to define.
# The *run_method_name* argument is the name of the method in the wrapper class
# that will actually run the test code.
# The block passed to this macro is the actual test code.
private macro _spectator_test(class_name, run_method_name)
# Wrapper class for isolating the test code.
class {{class_name.id}} < {{@type.id}}
# Generated method for actually running the test code.
def {{run_method_name.id}}
{{yield}}
end
end
end
# Creates an example class.
# Since the names are generated, and macros can't return values,
# the names for everything must be passed in as arguments.
# The *example_class_name* argument is the name of the class to define.
# The *test_class_name* argument is the name of the wrapper class to reference.
# This must be the same as `class_name` for `#_spectator_example_wrapper`.
# The *base_class* argument specifies which type of example class the new class should derive from.
# This should typically be `RunnableExample` or `PendingExample`.
# The *what* argument is the description passed to the `#it` or `#pending` block.
# And lastly, the block specified is additional content to put in the class.
# For instance, to define a method in the class, do it in the block.
# ```
# _spectator_example(Example123, Test123, RunnableExample, "does something") do
# def something
# # This method is defined in the Example123 class.
# end
# end
# ```
private macro _spectator_example(example_class_name, test_class_name, base_class, what, &block)
# Example class containing meta information and instructions for running the test.
class {{example_class_name.id}} < {{base_class.id}}
# Stores the group the example belongs to
# and sample values specific to this instance of the test.
# This method's signature must match the one used in `ExampleFactory#build`.
def initialize(group : ::Spectator::ExampleGroup)
super
@instance = {{test_class_name.id}}.new
end
# Retrieves the underlying, wrapped test code.
getter instance
# Indicates whether the example references a method.
def symbolic?
{{what.is_a?(StringLiteral) && what.starts_with?('#') ? true : false}}
end
# Add the block's content.
{{block.body}}
# Description for the test.
def what
{{what.is_a?(StringLiteral) ? what : what.stringify}}
end
end
end
end
end