Start rework on capturing expressions

This commit is contained in:
Michael Miller 2021-01-09 00:12:28 -07:00
parent 1352110871
commit fbe9f22e02
No known key found for this signature in database
GPG key ID: F9A0C5C65B162436
10 changed files with 81 additions and 250 deletions

View file

@ -0,0 +1,52 @@
require "./label"
module Spectator
# Represents an expression from a test.
# This is typically captured by an `expect` macro.
# It consists of a label and the value of the expression.
# The label should be a string recognizable by the user,
# or nil if one isn't available.
# This base class is provided so that all generic sub-classes can be stored as this one type.
# The value of the expression can be retrieved by downcasting to the expected type with `#cast`.
abstract class AbstractExpression
# User recognizable string for the expression.
# This can be something like a variable name or a snippet of Crystal code.
getter label : Label
# Creates the expression.
# The *label* is usually the Crystal code evaluating to the `#value`.
# It can be nil if it isn't available.
def initialize(@label : Label)
end
# Retrieves the real value of the expression.
abstract def value
# Attempts to cast `#value` to the type *T* and return it.
def cast(type : T.class) : T forall T
value.as(T)
end
# Produces a string representation of the expression.
# This consists of the label (if one is available) and the value.
def to_s(io)
if (label = @label)
io << label
io << ':'
io << ' '
end
io << value
end
# Produces a detailed string representation of the expression.
# This consists of the label (if one is available) and the value.
def inspect(io)
if (label = @label)
io << @label
io << ':'
io << ' '
end
value.inspect(io)
end
end
end

View file

@ -0,0 +1,22 @@
require "./abstract_expression"
require "./label"
module Spectator
# Represents an expression from a test.
# This is typically captured by an `expect` macro.
# It consists of a label and the value of the expression.
# The label should be a string recognizable by the user,
# or nil if one isn't available.
class Expression(T) < AbstractExpression
# Raw value of the expression.
getter value
# Creates the expression.
# Expects the *value* of the expression and a *label* describing it.
# The *label* is usually the Crystal code evaluating to the *value*.
# It can be nil if it isn't available.
def initialize(@value : T, label : Label)
super(label)
end
end
end

7
src/spectator/label.cr Normal file
View file

@ -0,0 +1,7 @@
module Spectator
# Identifier used in the spec.
# Signficant to the user.
# When a label is a symbol, then it is referencing a type or method.
# A label is nil when one can't be provided or captured.
alias Label = String | Symbol | Nil
end

View file

@ -1,48 +0,0 @@
require "./test_expression"
module Spectator
# Captures an block from a test and its label.
struct TestBlock(ReturnType) < TestExpression(ReturnType)
# Calls the block and retrieves the value.
def value : ReturnType
@proc.call
end
# Creates the block expression with a custom label.
# Typically the label is the code in the block/proc.
def initialize(@proc : -> ReturnType, label : String)
super(label)
end
def self.create(proc : -> T, label : String) forall T
{% if T.id == "ReturnType".id %}
wrapper = ->{ proc.call; nil }
TestBlock(Nil).new(wrapper, label)
{% else %}
TestBlock(T).new(proc, label)
{% end %}
end
# Creates the block expression with a generic label.
# This is used for the "should" syntax and when the label doesn't matter.
def initialize(@proc : -> ReturnType)
super("<Proc>")
end
def self.create(proc : -> T) forall T
{% if T.id == "ReturnType".id %}
wrapper = ->{ proc.call; nil }
TestBlock(Nil).new(wrapper)
{% else %}
TestBlock(T).new(proc)
{% end %}
end
# Reports complete information about the expression.
def inspect(io)
io << label
io << " -> "
io << value
end
end
end

View file

@ -1,82 +0,0 @@
require "./example_hooks"
require "./test_values"
module Spectator
class TestContext
getter! parent
getter values
getter stubs : Hash(String, Deque(Mocks::MethodStub))
def initialize(@parent : TestContext?,
@hooks : ExampleHooks,
@conditions : ExampleConditions,
@values : TestValues,
@stubs : Hash(String, Deque(Mocks::MethodStub)))
@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

View file

@ -1,25 +0,0 @@
module Spectator
# Base type for capturing an expression from a test.
abstract struct TestExpression(T)
# User-friendly string displayed for the actual expression being tested.
# For instance, in the expectation:
# ```
# expect(foo).to eq(bar)
# ```
# This property will be "foo".
# It will be the literal string "foo",
# and not the actual value of the foo.
getter label : String
# Creates the common base of the expression.
def initialize(@label)
end
abstract def value : T
# String representation of the expression.
def to_s(io)
io << label
end
end
end

View file

@ -1,29 +0,0 @@
require "./test_expression"
module Spectator
# Captures a value from a test and its label.
struct TestValue(T) < TestExpression(T)
# Actual value.
getter value : T
# Creates the expression value with a custom label.
def initialize(@value : T, label : String)
super(label)
end
# Creates the expression with a stringified value.
# This is used for the "should" syntax and when the label doesn't matter.
def initialize(@value : T)
super(@value.to_s)
end
# Reports complete information about the expression.
def inspect(io)
io << label
io << '='
io << @value
end
end
alias LabeledValue = TestValue(String)
end

View file

@ -1,42 +0,0 @@
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.
def description
@description || @source.to_s
end
# 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 description?
!@description.nil?
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

View file

@ -1,17 +0,0 @@
require "./value_wrapper"
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`
# so that the type is deferred to runtime.
# This trick allows the DSL to store values without explicitly knowing their type.
class TypedValueWrapper(T) < ValueWrapper
# Wrapped value.
getter value : T
# Creates a new wrapper for a value.
def initialize(@value : T)
end
end
end

View file

@ -1,7 +0,0 @@
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
end
end