2018-09-15 17:21:23 +00:00
|
|
|
require "../example_group"
|
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
module Spectator::DSL
|
2018-11-03 18:13:53 +00:00
|
|
|
# Domain specific language for the main structure of a spec.
|
|
|
|
# The primary components of this are `#describe`, `#context`, and `#it`.
|
|
|
|
#
|
|
|
|
# These macros define modules and classes.
|
|
|
|
# Those modules and classes are used to create the test cases.
|
|
|
|
#
|
|
|
|
# A class is created for every block of code that contains test code.
|
|
|
|
# An `#it` block creates a class derived from `RunnableExample`.
|
|
|
|
# A `#pending` block creates a class derived from `PendingExample`.
|
|
|
|
# The classes are built so that they run the example's code when invoked.
|
|
|
|
# However, the actual example code is placed into a separate "wrapper" class.
|
|
|
|
# This is done to avoid overlap with the Spectator namespace.
|
|
|
|
# The example code ends up having visibility only into itself and the DSL.
|
|
|
|
#
|
|
|
|
# Here's some skeleton code to demonstrate this:
|
|
|
|
# ```
|
|
|
|
# it "does something" do
|
|
|
|
# # Test code goes here...
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # becomes...
|
|
|
|
#
|
|
|
|
# # Class describing the example
|
|
|
|
# # and provides a means of running the test.
|
|
|
|
# # Typically every class, module, and method
|
|
|
|
# # that the user might see or be able to reference is obscured.
|
|
|
|
# # Fresh variables from Crystal's macros are used to achive this.
|
|
|
|
# # It makes debugging Spectator more difficult,
|
|
|
|
# # but prevents name collision with user code.
|
|
|
|
# class Example123 < RunnableExample
|
|
|
|
# def initialize(group, sample_values)
|
|
|
|
# # group and sample_values are covered later.
|
|
|
|
# super
|
|
|
|
# @wrapper = Wrapper123.new(sample_values)
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # Returns the text provided by the user.
|
|
|
|
# # This isn't stored as a member
|
|
|
|
# # so that it can be referenced directly in compiled code.
|
|
|
|
# def what
|
|
|
|
# "does something"
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # This method is called by `RunnableExample`
|
|
|
|
# # when the example code should be ran.
|
|
|
|
# def run_instance
|
|
|
|
# @wrapper._run123
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # Wrapper class for the example code.
|
|
|
|
# # This isolates it from Spectator's internals.
|
|
|
|
# class Wrapper123
|
|
|
|
# include Context123 # More on this in a bit.
|
|
|
|
# include ExampleDSL # Include DSL for the example code.
|
|
|
|
#
|
|
|
|
# # Generated method name to avoid conflicts.
|
|
|
|
# def _run123
|
|
|
|
# # Test code goes here...
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
# ```
|
|
|
|
#
|
|
|
|
# Modules are used to provide context and share methods across examples.
|
|
|
|
# They are used as mix-ins for the example code.
|
|
|
|
# The example code wrapper class includes its parent module.
|
|
|
|
# This allows the example to access anything that was defined in the same context.
|
|
|
|
# Contexts can be nested, and this is achieved by including the parent module.
|
|
|
|
# Whenever a module or class is defined,
|
|
|
|
# it includes its parent so that functionality can be inherited.
|
|
|
|
#
|
|
|
|
# For example:
|
|
|
|
# ```
|
|
|
|
# describe "#foo" do
|
|
|
|
# subject { described_class.foo(value) }
|
|
|
|
#
|
|
|
|
# context "when given :bar" do
|
|
|
|
# let(value) { :bar }
|
|
|
|
#
|
|
|
|
# it "does something" do
|
|
|
|
# # ...
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # becomes...
|
|
|
|
#
|
|
|
|
# # describe "#foo"
|
|
|
|
# module Group123
|
|
|
|
#
|
|
|
|
# # Start a new group.
|
|
|
|
# # More on this in a bit.
|
|
|
|
# Builder.start_group("#foo")
|
|
|
|
#
|
|
|
|
# def subject
|
|
|
|
# described_class.foo(value)
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # context "when given :bar"
|
|
|
|
# module Group456
|
|
|
|
# include Group123 # Inherit parent module.
|
|
|
|
#
|
|
|
|
# # Start a nested group.
|
|
|
|
# Builder.start_group("when given :bar")
|
|
|
|
#
|
|
|
|
# def value
|
|
|
|
# :bar
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # Wrapper class for the test case.
|
|
|
|
# class Wrapper456
|
|
|
|
# include Group456 # Include context.
|
|
|
|
#
|
|
|
|
# # Rest of example code...
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # Example class for the test case.
|
|
|
|
# class Example456 < RunnableExample
|
|
|
|
# # Rest of example code...
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # Add example to group.
|
|
|
|
# Builder.add_example(Example456)
|
|
|
|
#
|
|
|
|
# # End "when given :bar" group.
|
|
|
|
# Builder.end_group
|
|
|
|
#
|
|
|
|
# end
|
|
|
|
#
|
|
|
|
# # End "#foo" group.
|
|
|
|
# Builder.end_group
|
|
|
|
#
|
|
|
|
# end
|
|
|
|
# ```
|
|
|
|
#
|
|
|
|
# In addition to providing modules as mix-ins,
|
|
|
|
# example groups are defined with `#describe` and `#context`.
|
|
|
|
# The DSL makes use of `Builder` to construct the run-time portion of the spec.
|
|
|
|
# As groups are defined, they are pushed on a stack
|
|
|
|
# and popped off after everything nested in them is defined.
|
|
|
|
# `Builder` tracks the current group (top of the stack).
|
|
|
|
# This way, examples, hooks, nested groups, and other items can be added to it.
|
|
|
|
# Groups and examples are nested in a parent group.
|
|
|
|
# The only group that isn't nested is the root group - `RootExampleGroup`.
|
2018-09-27 22:20:55 +00:00
|
|
|
module StructureDSL
|
|
|
|
def initialize(sample_values : Internals::SampleValues)
|
|
|
|
end
|
2018-09-23 18:19:07 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
macro describe(what, &block)
|
|
|
|
context({{what}}) {{block}}
|
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
macro context(what, &block)
|
|
|
|
module Group%group
|
|
|
|
include {{@type.id}}
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
{% if what.is_a?(Path) || what.is_a?(Generic) %}
|
|
|
|
_spectator_described_class {{what}}
|
|
|
|
{% end %}
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
::Spectator::DSL::Builder.start_group(
|
|
|
|
{{what.is_a?(StringLiteral) ? what : what.stringify}}
|
|
|
|
)
|
2018-09-22 21:15:29 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
{{block.body}}
|
2018-09-22 21:15:29 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
::Spectator::DSL::Builder.end_group
|
2018-09-15 17:21:23 +00:00
|
|
|
end
|
2018-09-27 22:20:55 +00:00
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
macro given(collection, &block)
|
2018-11-03 00:11:46 +00:00
|
|
|
{% name = block.args.empty? ? "value".id : block.args.first %}
|
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
module Group%group
|
|
|
|
include {{@type.id}}
|
2018-09-20 02:49:01 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
def %collection
|
|
|
|
{{collection}}
|
|
|
|
end
|
2018-09-20 05:00:17 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
@%wrapper : ::Spectator::Internals::ValueWrapper
|
2018-09-20 05:00:17 +00:00
|
|
|
|
2018-11-03 00:11:46 +00:00
|
|
|
def {{name}}
|
2018-09-27 22:20:55 +00:00
|
|
|
@%wrapper.as(::Spectator::Internals::TypedValueWrapper(typeof(%collection.first))).value
|
|
|
|
end
|
2018-09-20 05:00:17 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
def initialize(sample_values : ::Spectator::Internals::SampleValues)
|
|
|
|
super
|
|
|
|
@%wrapper = sample_values.get_wrapper(:%group)
|
|
|
|
end
|
2018-09-20 02:00:26 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
_spectator_given_collection Collection%collection, %to_a, %collection
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
::Spectator::DSL::Builder.start_given_group(
|
|
|
|
{{collection.stringify}},
|
2018-09-28 00:47:54 +00:00
|
|
|
Collection%collection.new.%to_a,
|
2018-11-03 00:11:46 +00:00
|
|
|
{{name.stringify}},
|
2018-09-27 22:20:55 +00:00
|
|
|
:%group
|
|
|
|
)
|
2018-09-20 02:49:01 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
{{block.body}}
|
2018-09-22 21:15:29 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
::Spectator::DSL::Builder.end_group
|
2018-09-20 02:00:26 +00:00
|
|
|
end
|
2018-09-27 22:20:55 +00:00
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
macro subject(&block)
|
|
|
|
let(:subject) {{block}}
|
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
macro let(name, &block)
|
|
|
|
let!(%value) {{block}}
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
@%wrapper : ::Spectator::Internals::ValueWrapper?
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
def {{name.id}}
|
|
|
|
if (wrapper = @%wrapper)
|
|
|
|
wrapper.unsafe_as(::Spectator::Internals::TypedValueWrapper(typeof(%value))).value
|
|
|
|
else
|
|
|
|
%value.tap do |value|
|
|
|
|
@%wrapper = ::Spectator::Internals::TypedValueWrapper(typeof(%value)).new(value)
|
2018-09-15 17:21:23 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2018-09-27 22:20:55 +00:00
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
macro let!(name, &block)
|
|
|
|
def {{name.id}}
|
|
|
|
{{block.body}}
|
2018-09-15 17:21:23 +00:00
|
|
|
end
|
2018-09-27 22:20:55 +00:00
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
macro before_all(&block)
|
|
|
|
::Spectator::DSL::Builder.add_before_all_hook {{block}}
|
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
macro before_each(&block)
|
|
|
|
::Spectator::DSL::Builder.add_before_each_hook {{block}}
|
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
macro after_all(&block)
|
|
|
|
::Spectator::DSL::Builder.add_after_all_hook {{block}}
|
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
macro after_each(&block)
|
|
|
|
::Spectator::DSL::Builder.add_after_each_hook {{block}}
|
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
macro around_each(&block)
|
|
|
|
::Spectator::DSL::Builder.add_around_each_hook {{block}}
|
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
def include_examples
|
|
|
|
raise NotImplementedError.new("Spectator::DSL#include_examples")
|
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-10-14 17:50:08 +00:00
|
|
|
macro it(what, &block)
|
2018-09-27 22:20:55 +00:00
|
|
|
_spectator_example_wrapper(Wrapper%example, %run) {{block}}
|
2018-09-23 20:39:05 +00:00
|
|
|
|
2018-10-14 17:50:08 +00:00
|
|
|
_spectator_example(Example%example, Wrapper%example, ::Spectator::RunnableExample, {{what}}) do
|
2018-09-27 22:20:55 +00:00
|
|
|
protected def run_instance
|
|
|
|
@instance.%run
|
2018-09-15 17:21:23 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
::Spectator::DSL::Builder.add_example(Example%example)
|
|
|
|
end
|
2018-09-23 20:53:32 +00:00
|
|
|
|
2018-10-14 17:50:08 +00:00
|
|
|
macro pending(what, &block)
|
2018-09-27 22:20:55 +00:00
|
|
|
_spectator_example_wrapper(Wrapper%example, %run) {{block}}
|
2018-09-23 20:53:32 +00:00
|
|
|
|
2018-10-14 17:50:08 +00:00
|
|
|
_spectator_example(Example%example, Wrapper%example, ::Spectator::PendingExample, {{what}})
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
::Spectator::DSL::Builder.add_example(Example%example)
|
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
macro it_behaves_like
|
|
|
|
{% raise NotImplementedError.new("it_behaves_like functionality is not implemented") %}
|
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
private macro _spectator_described_class(what)
|
|
|
|
def described_class
|
|
|
|
{{what}}.tap do |thing|
|
|
|
|
raise "#{thing} must be a type name to use #described_class or #subject,\
|
|
|
|
but it is a #{typeof(thing)}" unless thing.is_a?(Class)
|
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
end
|
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
_spectator_implicit_subject
|
|
|
|
end
|
|
|
|
|
|
|
|
private macro _spectator_implicit_subject
|
|
|
|
def subject
|
|
|
|
described_class.new
|
2018-09-15 17:21:23 +00:00
|
|
|
end
|
2018-09-27 22:20:55 +00:00
|
|
|
end
|
2018-09-23 22:07:03 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
private macro _spectator_given_collection(class_name, to_a_method_name, collection_method_name)
|
|
|
|
class {{class_name.id}}
|
|
|
|
include {{@type.id}}
|
2018-09-23 22:07:03 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
def {{to_a_method_name.id}}
|
|
|
|
{{collection_method_name.id}}.to_a
|
2018-09-23 22:07:03 +00:00
|
|
|
end
|
|
|
|
end
|
2018-09-27 22:20:55 +00:00
|
|
|
end
|
2018-09-23 22:07:03 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
private macro _spectator_example_wrapper(class_name, run_method_name, &block)
|
|
|
|
class {{class_name.id}}
|
|
|
|
include ::Spectator::DSL::ExampleDSL
|
|
|
|
include {{@type.id}}
|
2018-09-23 22:07:03 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
def initialize(sample_values : ::Spectator::Internals::SampleValues)
|
|
|
|
super
|
|
|
|
end
|
2018-09-23 22:07:03 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
def {{run_method_name.id}}
|
|
|
|
{{block.body}}
|
2018-09-23 22:07:03 +00:00
|
|
|
end
|
|
|
|
end
|
2018-09-27 22:20:55 +00:00
|
|
|
end
|
2018-09-23 22:07:03 +00:00
|
|
|
|
2018-10-14 17:50:08 +00:00
|
|
|
private macro _spectator_example(example_class_name, wrapper_class_name, base_class, what, &block)
|
2018-09-27 22:20:55 +00:00
|
|
|
class {{example_class_name.id}} < {{base_class.id}}
|
|
|
|
def initialize(group : ::Spectator::ExampleGroup, sample_values : ::Spectator::Internals::SampleValues)
|
|
|
|
super
|
|
|
|
@instance = {{wrapper_class_name.id}}.new(sample_values)
|
|
|
|
end
|
2018-09-23 22:07:03 +00:00
|
|
|
|
2018-09-27 22:20:55 +00:00
|
|
|
{% if block.is_a?(Block) %}
|
|
|
|
{{block.body}}
|
|
|
|
{% end %}
|
2018-09-23 22:07:03 +00:00
|
|
|
|
2018-10-14 17:50:08 +00:00
|
|
|
def what
|
|
|
|
{{what.is_a?(StringLiteral) ? what : what.stringify}}
|
2018-09-23 22:07:03 +00:00
|
|
|
end
|
|
|
|
end
|
2018-09-15 17:21:23 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|