Cleanup new DSL macros

This commit is contained in:
Michael Miller 2021-01-09 13:17:42 -07:00
parent fbd9713d52
commit 009ca4776a
No known key found for this signature in database
GPG key ID: FB9F12F7C646A4AD
4 changed files with 64 additions and 212 deletions

View file

@ -1,3 +1,4 @@
require "../context"
require "../source"
require "./builder"
@ -8,11 +9,23 @@ module Spectator::DSL
# The *name* is the name given to the macro.
# TODO: Mark example as pending if block is omitted.
macro define_example(name)
# Defines an example.
#
# If a block is given, it is treated as the code to test.
# The block is provided the current example instance as an argument.
#
# The first argument names the example (test).
# Typically, this specifies what is being tested.
# It has no effect on the test and is purely used for output.
# If omitted, a name is generated from the first assertion in the test.
#
# The example will be marked as pending if the block is omitted.
# A block or name must be provided.
macro {{name.id}}(what = nil, &block)
\{% raise "Cannot use '{{name.id}}' inside of a test block" if @def %}
\{% raise "A description or block must be provided. Cannot use '{{name.id}}' alone." unless what || block %}
def \%test # TODO: Pass example instance.
def \%test : Nil # TODO: Pass example instance.
\{{block.body}}
end
@ -27,7 +40,7 @@ module Spectator::DSL
# Inserts the correct representation of a example's name.
# If *what* is a string, then it is dropped in as-is.
# For anything else, it is stringified.
# This is intended to be used to convert a description from the spec DSL to `ExampleNode#name`.
# This is intended to be used to convert a description from the spec DSL to `SpecNode#name`.
private macro _spectator_example_name(what)
{% if what.is_a?(StringLiteral) ||
what.is_a?(StringInterpolation) ||
@ -38,77 +51,12 @@ module Spectator::DSL
{% end %}
end
# Defines an example.
#
# If a block is given, it is treated as the code to test.
# The block is provided the current example instance as an argument.
#
# The first argument names the example (test).
# Typically, this specifies what is being tested.
# It has no effect on the test and is purely used for output.
# If omitted, a name is generated from the first assertion in the test.
#
# The example will be marked as pending if the block is omitted.
# A block or name must be provided.
define_example :example
# Defines an example.
#
# If a block is given, it is treated as the code to test.
# The block is provided the current example instance as an argument.
#
# The first argument names the example (test).
# Typically, this specifies what is being tested.
# It has no effect on the test and is purely used for output.
# If omitted, a name is generated from the first assertion in the test.
#
# The example will be marked as pending if the block is omitted.
# A block or name must be provided.
define_example :it
# Defines an example.
#
# If a block is given, it is treated as the code to test.
# The block is provided the current example instance as an argument.
#
# The first argument names the example (test).
# Typically, this specifies what is being tested.
# It has no effect on the test and is purely used for output.
# If omitted, a name is generated from the first assertion in the test.
#
# The example will be marked as pending if the block is omitted.
# A block or name must be provided.
define_example :specify
# TODO: pending, skip, and xit
end
macro pending(description = nil, _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.is_a?(StringInterpolation) || description.is_a?(NilLiteral) ? description : description.stringify}},
%source,
{{@type.name}}
) { |test| test.as({{@type.name}}).%run }
end
macro skip(description = nil, &block)
pending({{description}}) {{block}}
end
macro xit(description = nil, &block)
pending({{description}}) {{block}}
end
end

View file

@ -32,7 +32,7 @@ module Spectator::DSL
# If *what* appears to be a type name, it will be symbolized.
# If it's a string, then it is dropped in as-is.
# For anything else, it is stringified.
# This is intended to be used to convert a description from the spec DSL to `ExampleNode#name`.
# This is intended to be used to convert a description from the spec DSL to `SpecNode#name`.
private macro _spectator_group_name(what)
{% if (what.is_a?(Generic) ||
what.is_a?(Path) ||
@ -85,107 +85,7 @@ module Spectator::DSL
define_example_group :describe
define_example_group :context
# TODO: sample, random_sample, and given
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

View file

@ -1,55 +1,61 @@
require "../source"
require "./builder"
module Spectator::DSL
# DSL methods for adding custom logic to key times of the spec execution.
module Hooks
# Defines code to run before any and all examples in an example group.
macro before_all(&block)
{% raise "Cannot use 'before_all' inside of a test block" if @def %}
# Defines a macro to create an example group hook.
# The *type* indicates when the hook runs and must be a method on `Spectator::DSL::Builder`.
macro define_example_group_hook(type)
macro {{type.id}}(&block)
\{% raise "Missing block for '{{type.id}}' hook" unless block %}
\{% raise "Cannot use '{{type.id}}' inside of a test block" if @def %}
def self.%hook : Nil
{{block.body}}
def self.\%hook : Nil
\{{block.body}}
end
::Spectator::DSL::Builder.{{type.id}}(
::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}})
) { \{{@type.name}}.\%hook }
end
::Spectator::DSL::Builder.before_all(
::Spectator::Source.new({{block.filename}}, {{block.line_number}})
) { {{@type.name}}.%hook }
end
macro before_each(&block)
{% raise "Cannot use 'before_each' inside of a test block" if @def %}
# Defines a macro to create an example hook.
# The *type* indicates when the hook runs and must be a method on `Spectator::DSL::Builder`.
macro define_example_hook(type)
macro {{type.id}}(&block)
\{% raise "Missing block for '{{type.id}}' hook" unless block %}
\{% raise "Cannot use '{{type.id}}' inside of a test block" if @def %}
def %hook : Nil
{{block.body}}
def \%hook : Nil # TODO: Pass example instance.
\{{block.body}}
end
::Spectator::DSL::Builder.{{type.id}}(
::Spectator::Source.new(\{{block.filename}}, \{{block.line_number}})
) { |example| example.with_context(\{{@type.name}}) { \%hook } }
end
::Spectator::DSL::Builder.before_each(
::Spectator::Source.new({{block.filename}}, {{block.line_number}})
) { |example| example.with_context({{@type.name}}) { %hook } }
end
macro after_all(&block)
{% raise "Cannot use 'after_all' inside of a test block" if @def %}
# Defines a block of code that will be invoked once before any examples in the group.
# The block will not run in the context of the current running example.
# This means that values defined by `let` and `subject` are not available.
define_example_group_hook :before_all
def self.%hook : Nil
{{block.body}}
end
# Defines a block of code that will be invoked once after all examples in the group.
# The block will not run in the context of the current running example.
# This means that values defined by `let` and `subject` are not available.
define_example_group_hook :after_all
::Spectator::DSL::Builder.after_all(
::Spectator::Source.new({{block.filename}}, {{block.line_number}})
) { {{@type.name}}.%hook }
end
# Defines a block of code that will be invoked before every example in the group.
# The block will be run in the context of the current running example.
# This means that values defined by `let` and `subject` are available.
define_example_hook :before_each
macro after_each(&block)
{% raise "Cannot use 'after_each' inside of a test block" if @def %}
def %hook : Nil
{{block.body}}
end
::Spectator::DSL::Builder.after_each(
::Spectator::Source.new({{block.filename}}, {{block.line_number}})
) { |example| example.with_context({{@type.name}}) { %hook } }
end
# Defines a block of code that will be invoked after every example in the group.
# The block will be run in the context of the current running example.
# This means that values defined by `let` and `subject` are available.
define_example_hook :after_each
end
end

View file

@ -16,9 +16,7 @@ module Spectator::DSL
# # Your examples for `Foo` go here.
# end
# ```
# 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.
# NOTE: Inside the block, the `Spectator` prefix _should not_ be used.
macro {{method.id}}(description, &block)
class ::SpectatorTestContext
{{method.id}}(\{{description}}) \{{block}}