Some initial work on cleaned up groups

This commit is contained in:
Michael Miller 2020-09-12 16:01:29 -06:00
parent 1d32946760
commit 225c358cb8
No known key found for this signature in database
GPG key ID: FB9F12F7C646A4AD
2 changed files with 68 additions and 45 deletions

View file

@ -1,55 +1,66 @@
require "../spec_builder" module Spectator::DSL
# DSL methods and macros for creating example groups.
module Spectator # This module should be included as a mix-in.
module DSL module Groups
macro context(what, _source_file = __FILE__, _source_line = __LINE__, &block) # Defines a new example group.
class Context%context < {{@type.id}} # The *what* argument is a name or description of the group.
{% # If it isn't a string literal, then it is symbolized for `ExampleNode#name`.
description = if what.is_a?(StringLiteral) macro example_group(what, *, _source_file = __FILE__, _source_line = __LINE__, &block)
if what.starts_with?("#") || what.starts_with?(".") # Example group {{name.stringify}}
what.id.symbolize # Source: {{_source_file}}:{{_source_line}}
else class Group%group < {{@type.id}}
what _spectator_group_subject({{what}})
end
else
what.symbolize
end
%}
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
::Spectator::SpecBuilder.start_group({{description}}, %source) ::Spectator::DSL::Builder.start_group({{what.is_a?(StringLiteral) ? what : what.stringify}}, %source)
# Oddly, `#resolve?` can return a constant's value, which isn't a TypeNode.
# Ensure `described_class` and `subject` are only set for real types (is a `TypeNode`).
{% if (what.is_a?(Path) || what.is_a?(Generic)) && (described_type = what.resolve?).is_a?(TypeNode) %}
macro described_class
{{what}}
end
subject do
{% if described_type < Reference || described_type < Value %}
described_class.new
{% else %}
described_class
{% end %}
end
{% else %}
def _spectator_implicit_subject(*args)
{{what}}
end
{% end %}
{{block.body}} {{block.body}}
::Spectator::SpecBuilder.end_group ::Spectator::DSL::Builder.end_group
end end
end end
macro describe(what, &block) macro describe(what, *, _source_file = __FILE__, _source_line = __LINE__, &block)
context({{what}}) {{block}} example_group({{what}}, _source_file: {{_source_file}}, _source_line: {{_source_line}}) {{block}}
end end
macro sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) macro context(what, *, _source_file = __FILE__, _source_line = __LINE__, &block)
example_group({{what}}, _source_file: {{_source_file}}, _source_line: {{_source_line}}) {{block}}
end
# Defines the implicit subject for the test context.
# If *what* is a type, then the `described_class` method will be defined.
# Additionally, the implicit subject is set to an instance of *what* if it's not a module.
#
# There is no common macro type that has the `#resolve?` method.
# Also, `#responds_to?` can't be used in macros.
# So the large if statement in this macro is used to look for type signatures.
private macro _spectator_group_subject(what)
{% if (what.is_a?(Generic) ||
what.is_a?(Path) ||
what.is_a?(TypeNode) ||
what.is_a?(Union)) &&
(described_type = what.resolve?).is_a?(TypeNode) %}
private def described_class
{{described_type}}
end
private def _spectator_implicit_subject
{% if described_type < Reference || described_type < Value %}
described_class.new
{% else %}
described_class
{% end %}
end
{% else %}
private def _spectator_implicit_subject
{{what}}
end
{% end %}
end
end
macro sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block)
{% name = block.args.empty? ? :value.id : block.args.first.id %} {% name = block.args.empty? ? :value.id : block.args.first.id %}
def %collection def %collection
@ -81,7 +92,7 @@ module Spectator
end end
end end
macro random_sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block) macro random_sample(collection, count = nil, _source_file = __FILE__, _source_line = __LINE__, &block)
{% name = block.args.empty? ? :value.id : block.args.first.id %} {% name = block.args.empty? ? :value.id : block.args.first.id %}
def %collection def %collection
@ -118,7 +129,7 @@ module Spectator
end end
end end
macro given(*assignments, &block) macro given(*assignments, &block)
context({{assignments.splat.stringify}}) do context({{assignments.splat.stringify}}) do
{% for assignment in assignments %} {% for assignment in assignments %}
let({{assignment.target}}) { {{assignment.value}} } let({{assignment.target}}) { {{assignment.value}} }
@ -150,5 +161,4 @@ module Spectator
{% end %} {% end %}
end end
end end
end
end end

View file

@ -4,4 +4,17 @@
# This type is intentionally outside the `Spectator` module. # This type is intentionally outside the `Spectator` module.
# The reason for this is to prevent name collision when using the DSL to define a spec. # The reason for this is to prevent name collision when using the DSL to define a spec.
abstract class SpectatorContext abstract class SpectatorContext
# Initial implicit subject for tests.
# This method should be overridden by example groups when an object is described.
private def _spectator_implicit_subject
nil
end
# Initial subject for tests.
# Returns the implicit subject.
# This method should be overridden when an explicit subject is defined by the DSL.
# TODO: Subject needs to be cached.
private def subject
_spectator_implicit_subject
end
end end