mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Initial rework of example type structure
This commit is contained in:
parent
9c6502234b
commit
55900ebecd
4 changed files with 109 additions and 230 deletions
|
@ -1,83 +1,34 @@
|
||||||
require "./example_component"
|
require "./context_delegate"
|
||||||
require "./test_wrapper"
|
require "./example_base"
|
||||||
|
require "./example_group"
|
||||||
|
require "./result"
|
||||||
|
require "./source"
|
||||||
|
|
||||||
module Spectator
|
module Spectator
|
||||||
# Base class for all types of examples.
|
# Standard example that runs a test case.
|
||||||
# Concrete types must implement the `#run_impl` method.
|
class Example < ExampleBase
|
||||||
abstract class Example < ExampleComponent
|
# Indicates whether the example already ran.
|
||||||
@finished = false
|
getter? finished : Bool = false
|
||||||
@description : String? = nil
|
|
||||||
|
|
||||||
protected setter description
|
# Retrieves the result of the last time the example ran.
|
||||||
|
getter! result : Result
|
||||||
|
|
||||||
# Indicates whether the example has already been run.
|
# Creates the example.
|
||||||
def finished? : Bool
|
# The *delegate* contains the test context and method that runs the test case.
|
||||||
@finished
|
# The *name* describes the purpose of the example.
|
||||||
|
# It can be a `Symbol` to describe a type.
|
||||||
|
# The *source* tracks where the example exists in source code.
|
||||||
|
# The example will be assigned to *group* if it is provided.
|
||||||
|
def initialize(@delegate : ContextDelegate,
|
||||||
|
name : String | Symbol? = nil, source : Source? = nil, group : ExampleGroup? = nil)
|
||||||
|
super(name, source, group)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Group that the example belongs to.
|
# Executes the test case.
|
||||||
getter group : ExampleGroup
|
# Returns the result of the execution.
|
||||||
|
# The result will also be stored in `#result`.
|
||||||
# Retrieves the internal wrapped instance.
|
|
||||||
protected getter test_wrapper : TestWrapper
|
|
||||||
|
|
||||||
# Source where the example originated from.
|
|
||||||
def source : Source
|
|
||||||
@test_wrapper.source
|
|
||||||
end
|
|
||||||
|
|
||||||
def description : String | Symbol
|
|
||||||
@description || @test_wrapper.description
|
|
||||||
end
|
|
||||||
|
|
||||||
def symbolic? : Bool
|
|
||||||
return false unless @test_wrapper.description?
|
|
||||||
|
|
||||||
description = @test_wrapper.description
|
|
||||||
description.starts_with?('#') || description.starts_with?('.')
|
|
||||||
end
|
|
||||||
|
|
||||||
abstract def run_impl
|
|
||||||
|
|
||||||
# Runs the example code.
|
|
||||||
# A result is returned, which represents the outcome of the test.
|
|
||||||
# An example can be run only once.
|
|
||||||
# An exception is raised if an attempt is made to run it more than once.
|
|
||||||
def run : Result
|
def run : Result
|
||||||
raise "Attempted to run example more than once (#{self})" if finished?
|
raise NotImplementedError.new("#run")
|
||||||
run_impl
|
|
||||||
ensure
|
|
||||||
@finished = true
|
|
||||||
end
|
|
||||||
|
|
||||||
# Creates the base of the example.
|
|
||||||
# The group should be the example group the example belongs to.
|
|
||||||
def initialize(@group, @test_wrapper)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Indicates there is only one example to run.
|
|
||||||
def example_count : Int
|
|
||||||
1
|
|
||||||
end
|
|
||||||
|
|
||||||
# Retrieve the current example.
|
|
||||||
def [](index : Int) : Example
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
# String representation of the example.
|
|
||||||
# This consists of the groups the example is in and the description.
|
|
||||||
# The string can be given to end-users to identify the example.
|
|
||||||
def to_s(io)
|
|
||||||
@group.to_s(io)
|
|
||||||
io << ' ' unless symbolic? && @group.symbolic?
|
|
||||||
io << description
|
|
||||||
end
|
|
||||||
|
|
||||||
# Creates the JSON representation of the example,
|
|
||||||
# which is just its name.
|
|
||||||
def to_json(json : ::JSON::Builder)
|
|
||||||
json.string(to_s)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
85
src/spectator/example_base.cr
Normal file
85
src/spectator/example_base.cr
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
require "./example_group"
|
||||||
|
require "./result"
|
||||||
|
require "./source"
|
||||||
|
|
||||||
|
module Spectator
|
||||||
|
# Common base type for all examples.
|
||||||
|
abstract class ExampleBase
|
||||||
|
# Location of the example in source code.
|
||||||
|
getter! source : Source
|
||||||
|
|
||||||
|
# User-provided name or description of the test.
|
||||||
|
# This does not include the group name or descriptions.
|
||||||
|
# Use `#to_s` to get the full name.
|
||||||
|
#
|
||||||
|
# This value will be nil if no name was provided.
|
||||||
|
# In this case, the name should be set
|
||||||
|
# to the description of the first matcher that runs in the example.
|
||||||
|
#
|
||||||
|
# If this value is a `Symbol`, the user specified a type for the name.
|
||||||
|
getter! name : String | Symbol
|
||||||
|
|
||||||
|
# Group the example belongs to.
|
||||||
|
# Hooks are used from this group.
|
||||||
|
getter! group : ExampleGroup
|
||||||
|
|
||||||
|
# Assigns the group the example belongs to.
|
||||||
|
# If the example already belongs to a group,
|
||||||
|
# it will be removed from the previous group before adding it to the new group.
|
||||||
|
def group=(group : ExampleGroup?)
|
||||||
|
if (previous = @group)
|
||||||
|
previous.remove_example(self)
|
||||||
|
end
|
||||||
|
group.add_example(self) if group
|
||||||
|
@group = group
|
||||||
|
end
|
||||||
|
|
||||||
|
# Creates the base of the example.
|
||||||
|
# The *name* describes the purpose of the example.
|
||||||
|
# It can be a `Symbol` to describe a type.
|
||||||
|
# The *source* tracks where the example exists in source code.
|
||||||
|
# The example will be assigned to *group* if it is provided.
|
||||||
|
def initialize(@name : String | Symbol? = nil, @source : Source? = nil, group : ExampleGroup? = nil)
|
||||||
|
# Ensure group is linked.
|
||||||
|
self.group = group
|
||||||
|
end
|
||||||
|
|
||||||
|
# Indicates whether the example already ran.
|
||||||
|
abstract def finished? : Bool
|
||||||
|
|
||||||
|
# Retrieves the result of the last time the example ran.
|
||||||
|
# This will be nil if the example hasn't run,
|
||||||
|
# and should not be nil if it has.
|
||||||
|
abstract def result? : Result?
|
||||||
|
|
||||||
|
# Retrieves the result of the last time the example ran.
|
||||||
|
# Raises an error if the example hasn't run.
|
||||||
|
def result : Result
|
||||||
|
result? || raise(NilAssertionError("Example has no result"))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Constructs the full name or description of the example.
|
||||||
|
# This prepends names of groups this example is part of.
|
||||||
|
def to_s(io)
|
||||||
|
name = @name
|
||||||
|
|
||||||
|
# Prefix with group's full name if the example belongs to a group.
|
||||||
|
if (group = @group)
|
||||||
|
group.to_s(io)
|
||||||
|
|
||||||
|
# Add padding between the group name and example name,
|
||||||
|
# only if the names appear to be symbolic.
|
||||||
|
if group.name.is_a?(Symbol) && name.is_a?(String)
|
||||||
|
io << ' ' unless name.starts_with?('#') || name.starts_with?('.')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
name.to_s(io)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Exposes information about the example useful for debugging.
|
||||||
|
def inspect(io)
|
||||||
|
raise NotImplementedError.new("#inspect")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,26 +0,0 @@
|
||||||
module Spectator
|
|
||||||
# Abstract base for all examples and collections of examples.
|
|
||||||
# This is used as the base node type for the composite design pattern.
|
|
||||||
abstract class ExampleComponent
|
|
||||||
# Text that describes the context or test.
|
|
||||||
abstract def description : Symbol | String
|
|
||||||
|
|
||||||
def full_description
|
|
||||||
to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
abstract def source : Source
|
|
||||||
|
|
||||||
# Indicates whether the example (or group) has been completely run.
|
|
||||||
abstract def finished? : Bool
|
|
||||||
|
|
||||||
# The number of examples in this instance.
|
|
||||||
abstract def example_count : Int
|
|
||||||
|
|
||||||
# Lookup the example with the specified index.
|
|
||||||
abstract def [](index : Int) : Example
|
|
||||||
|
|
||||||
# Indicates that the component references a type or method.
|
|
||||||
abstract def symbolic? : Bool
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,131 +0,0 @@
|
||||||
require "./example_component"
|
|
||||||
|
|
||||||
module Spectator
|
|
||||||
# Shared base class for groups of examples.
|
|
||||||
#
|
|
||||||
# Represents a collection of examples and other groups.
|
|
||||||
# Use the `#each` methods to iterate through each child.
|
|
||||||
# However, these methods do not recurse into sub-groups.
|
|
||||||
# If you need that functionality, see `ExampleIterator`.
|
|
||||||
# Additionally, the indexer method (`#[]`) will index into sub-groups.
|
|
||||||
#
|
|
||||||
# This class also stores hooks to be associated with all examples in the group.
|
|
||||||
# The hooks can be invoked by running the `#run_before_hooks` and `#run_after_hooks` methods.
|
|
||||||
abstract class ExampleGroup < ExampleComponent
|
|
||||||
include Enumerable(ExampleComponent)
|
|
||||||
include Iterable(ExampleComponent)
|
|
||||||
|
|
||||||
@example_count = 0
|
|
||||||
|
|
||||||
# Retrieves the children in the group.
|
|
||||||
# This only returns the direct descends (non-recursive).
|
|
||||||
# The children must be set (with `#children=`) prior to calling this method.
|
|
||||||
getter! children : Array(ExampleComponent)
|
|
||||||
|
|
||||||
# Sets the children of the group.
|
|
||||||
# This should be called only from a builder in the `DSL` namespace.
|
|
||||||
# The children can be set only once -
|
|
||||||
# attempting to set more than once will raise an error.
|
|
||||||
# All sub-groups' children should be set before setting this group's children.
|
|
||||||
def children=(children : Array(ExampleComponent))
|
|
||||||
raise "Attempted to reset example group children" if @children
|
|
||||||
@children = children
|
|
||||||
# Recursively count the number of examples.
|
|
||||||
# This won't work if a sub-group hasn't had their children set (is still nil).
|
|
||||||
@example_count = children.sum(&.example_count)
|
|
||||||
end
|
|
||||||
|
|
||||||
def double(id, sample_values)
|
|
||||||
@doubles[id].build(sample_values)
|
|
||||||
end
|
|
||||||
|
|
||||||
getter context
|
|
||||||
|
|
||||||
def initialize(@context : TestContext)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Yields each direct descendant.
|
|
||||||
def each
|
|
||||||
children.each do |child|
|
|
||||||
yield child
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns an iterator for each direct descendant.
|
|
||||||
def each : Iterator(ExampleComponent)
|
|
||||||
children.each
|
|
||||||
end
|
|
||||||
|
|
||||||
# Number of examples in this group and all sub-groups.
|
|
||||||
def example_count : Int
|
|
||||||
@example_count
|
|
||||||
end
|
|
||||||
|
|
||||||
# Retrieves an example by its index.
|
|
||||||
# This recursively searches for an example.
|
|
||||||
#
|
|
||||||
# Positive and negative indices can be used.
|
|
||||||
# Any value out of range will raise an `IndexError`.
|
|
||||||
#
|
|
||||||
# Examples are indexed as if they are in a flattened tree.
|
|
||||||
# For instance:
|
|
||||||
# ```
|
|
||||||
# examples = [0, 1, [2, 3, 4], [5, [6, 7], 8], 9, [10]].flatten
|
|
||||||
# ```
|
|
||||||
# The arrays symbolize groups,
|
|
||||||
# and the numbers are the index of the example in that slot.
|
|
||||||
def [](index : Int) : Example
|
|
||||||
offset = check_bounds(index)
|
|
||||||
find_nested(offset)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Checks whether an index is within acceptable bounds.
|
|
||||||
# If the index is negative,
|
|
||||||
# it will be converted to its positive equivalent.
|
|
||||||
# If the index is out of bounds, an `IndexError` is raised.
|
|
||||||
# If the index is in bounds,
|
|
||||||
# the positive index is returned.
|
|
||||||
private def check_bounds(index)
|
|
||||||
if index < 0
|
|
||||||
raise IndexError.new if index < -example_count
|
|
||||||
index + example_count
|
|
||||||
else
|
|
||||||
raise IndexError.new if index >= example_count
|
|
||||||
index
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Finds the example with the specified index in the children.
|
|
||||||
# The *index* must be positive and within bounds (use `#check_bounds`).
|
|
||||||
private def find_nested(index)
|
|
||||||
offset = index
|
|
||||||
# Loop through each child
|
|
||||||
# until one is found to contain the index.
|
|
||||||
found = children.each do |child|
|
|
||||||
count = child.example_count
|
|
||||||
# Example groups consider their range to be [0, example_count).
|
|
||||||
# Each child is offset by the total example count of the previous children.
|
|
||||||
# The group exposes them in this way:
|
|
||||||
# 1. [0, example_count of group 1)
|
|
||||||
# 2. [example_count of group 1, example_count of group 2)
|
|
||||||
# 3. [example_count of group n, example_count of group n + 1)
|
|
||||||
# To iterate through children, the offset is tracked.
|
|
||||||
# Each iteration removes the previous child's count.
|
|
||||||
# This way the child receives the expected range.
|
|
||||||
break child if offset < count
|
|
||||||
offset -= count
|
|
||||||
end
|
|
||||||
# The remaining offset is passed along to the child.
|
|
||||||
# If it's an `Example`, it returns itself.
|
|
||||||
# Otherwise, the indexer repeats the process for the next child.
|
|
||||||
# It should be impossible to get nil here,
|
|
||||||
# provided the bounds check and example counts are correct.
|
|
||||||
found.not_nil![offset]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Checks whether all examples in the group have been run.
|
|
||||||
def finished? : Bool
|
|
||||||
children.all?(&.finished?)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in a new issue