2020-09-06 16:31:23 +00:00
|
|
|
require "./example_context_delegate"
|
2020-09-05 21:01:22 +00:00
|
|
|
require "./example_group"
|
2020-11-08 03:56:30 +00:00
|
|
|
require "./harness"
|
2021-02-13 05:46:22 +00:00
|
|
|
require "./location"
|
2021-05-08 02:09:33 +00:00
|
|
|
require "./node"
|
2020-10-17 20:56:31 +00:00
|
|
|
require "./pending_result"
|
2020-09-05 21:01:22 +00:00
|
|
|
require "./result"
|
2021-01-30 19:07:23 +00:00
|
|
|
require "./tags"
|
2018-10-14 23:10:12 +00:00
|
|
|
|
2018-08-19 07:15:32 +00:00
|
|
|
module Spectator
|
2020-09-05 21:01:22 +00:00
|
|
|
# Standard example that runs a test case.
|
2021-05-08 02:09:33 +00:00
|
|
|
class Example < Node
|
2020-11-08 03:56:30 +00:00
|
|
|
# Currently running example.
|
|
|
|
class_getter! current : Example
|
|
|
|
|
2021-05-08 03:04:17 +00:00
|
|
|
# Group the node belongs to.
|
|
|
|
getter! group : ExampleGroup
|
|
|
|
|
|
|
|
# Assigns the node to the specified *group*.
|
|
|
|
# This is an internal method and should only be called from `ExampleGroup`.
|
|
|
|
# `ExampleGroup` manages the association of nodes to groups.
|
|
|
|
protected setter group : ExampleGroup?
|
|
|
|
|
2020-09-05 21:01:22 +00:00
|
|
|
# Indicates whether the example already ran.
|
|
|
|
getter? finished : Bool = false
|
|
|
|
|
2021-04-27 00:47:11 +00:00
|
|
|
# Result of the last time the example ran.
|
|
|
|
# Is pending if the example hasn't run.
|
|
|
|
getter result : Result = PendingResult.new
|
2020-09-05 21:01:22 +00:00
|
|
|
|
|
|
|
# Creates the example.
|
2020-11-08 22:06:49 +00:00
|
|
|
# An instance to run the test code in is given by *context*.
|
|
|
|
# The *entrypoint* defines the test code (typically inside *context*).
|
2020-09-05 21:01:22 +00:00
|
|
|
# The *name* describes the purpose of the example.
|
|
|
|
# It can be a `Symbol` to describe a type.
|
2021-02-13 05:46:22 +00:00
|
|
|
# The *location* tracks where the example exists in source code.
|
2020-09-05 21:01:22 +00:00
|
|
|
# The example will be assigned to *group* if it is provided.
|
2021-01-30 18:20:20 +00:00
|
|
|
# A set of *tags* can be used for filtering and modifying example behavior.
|
2021-01-30 23:39:41 +00:00
|
|
|
# Note: The tags will not be merged with the parent tags.
|
2020-11-08 23:53:54 +00:00
|
|
|
def initialize(@context : Context, @entrypoint : self ->,
|
2021-02-13 05:46:22 +00:00
|
|
|
name : String? = nil, location : Location? = nil,
|
2021-05-08 03:04:17 +00:00
|
|
|
@group : ExampleGroup? = nil, tags = Tags.new)
|
|
|
|
super(name, location, tags)
|
|
|
|
|
|
|
|
# Ensure group is linked.
|
|
|
|
group << self if group
|
2020-09-05 21:01:22 +00:00
|
|
|
end
|
|
|
|
|
2020-11-07 21:43:59 +00:00
|
|
|
# Creates a dynamic example.
|
|
|
|
# A block provided to this method will be called as-if it were the test code for the example.
|
|
|
|
# The block will be given this example instance as an argument.
|
|
|
|
# The *name* describes the purpose of the example.
|
|
|
|
# It can be a `Symbol` to describe a type.
|
2021-02-13 05:46:22 +00:00
|
|
|
# The *location* tracks where the example exists in source code.
|
2020-11-07 21:43:59 +00:00
|
|
|
# The example will be assigned to *group* if it is provided.
|
2021-01-30 18:20:20 +00:00
|
|
|
# A set of *tags* can be used for filtering and modifying example behavior.
|
2021-01-30 23:39:41 +00:00
|
|
|
# Note: The tags will not be merged with the parent tags.
|
2021-05-08 18:10:27 +00:00
|
|
|
def initialize(name : String? = nil, location : Location? = nil,
|
|
|
|
@group : ExampleGroup? = nil, tags = Tags.new, &block : self ->)
|
2021-05-08 03:04:17 +00:00
|
|
|
super(name, location, tags)
|
|
|
|
|
2020-11-08 22:06:49 +00:00
|
|
|
@context = NullContext.new
|
|
|
|
@entrypoint = block
|
2021-05-08 03:04:17 +00:00
|
|
|
|
|
|
|
# Ensure group is linked.
|
|
|
|
group << self if group
|
2020-11-07 21:43:59 +00:00
|
|
|
end
|
|
|
|
|
2021-06-05 18:51:46 +00:00
|
|
|
# Creates a pending example.
|
|
|
|
# The *name* describes the purpose of the example.
|
|
|
|
# It can be a `Symbol` to describe a type.
|
|
|
|
# The *location* tracks where the example exists in source code.
|
|
|
|
# The example will be assigned to *group* if it is provided.
|
|
|
|
# A set of *tags* can be used for filtering and modifying example behavior.
|
|
|
|
# Note: The tags will not be merged with the parent tags.
|
|
|
|
def self.pending(name : String? = nil, location : Location? = nil,
|
|
|
|
group : ExampleGroup? = nil, tags = Tags.new)
|
|
|
|
new(name, location, group, tags.add(:pending)) { nil }
|
|
|
|
end
|
|
|
|
|
2020-09-05 21:01:22 +00:00
|
|
|
# Executes the test case.
|
|
|
|
# Returns the result of the execution.
|
|
|
|
# The result will also be stored in `#result`.
|
2018-10-10 19:05:17 +00:00
|
|
|
def run : Result
|
2020-11-08 03:56:30 +00:00
|
|
|
Log.debug { "Running example #{self}" }
|
2020-11-08 22:06:49 +00:00
|
|
|
Log.warn { "Example #{self} already ran" } if @finished
|
2020-11-15 18:25:07 +00:00
|
|
|
|
2021-01-30 23:36:15 +00:00
|
|
|
if pending?
|
|
|
|
Log.debug { "Skipping example #{self} - marked pending" }
|
2021-05-29 23:59:16 +00:00
|
|
|
@finished = true
|
2021-04-27 00:47:11 +00:00
|
|
|
return @result = PendingResult.new
|
2021-01-30 23:36:15 +00:00
|
|
|
end
|
|
|
|
|
2021-01-16 23:28:33 +00:00
|
|
|
previous_example = @@current
|
|
|
|
@@current = self
|
2020-11-15 18:25:07 +00:00
|
|
|
|
2021-01-16 23:28:33 +00:00
|
|
|
begin
|
|
|
|
@result = Harness.run do
|
2021-05-08 18:10:27 +00:00
|
|
|
@group.try(&.call_once_before_all)
|
|
|
|
if (parent = @group)
|
2021-01-16 23:28:33 +00:00
|
|
|
parent.call_around_each(self) { run_internal }
|
|
|
|
else
|
|
|
|
run_internal
|
|
|
|
end
|
2021-05-08 18:10:27 +00:00
|
|
|
if (parent = @group)
|
2021-01-17 00:04:42 +00:00
|
|
|
parent.call_once_after_all if parent.finished?
|
|
|
|
end
|
2020-11-15 18:25:07 +00:00
|
|
|
end
|
2021-01-16 23:28:33 +00:00
|
|
|
ensure
|
|
|
|
@@current = previous_example
|
|
|
|
@finished = true
|
2020-11-15 18:25:07 +00:00
|
|
|
end
|
2021-01-16 23:28:33 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
private def run_internal
|
2021-05-08 03:04:17 +00:00
|
|
|
@group.try(&.call_before_each(self))
|
2021-01-16 23:28:33 +00:00
|
|
|
@entrypoint.call(self)
|
2020-10-17 20:56:31 +00:00
|
|
|
@finished = true
|
2021-05-08 03:04:17 +00:00
|
|
|
@group.try(&.call_after_each(self))
|
2019-03-23 03:29:20 +00:00
|
|
|
end
|
2020-09-06 01:54:55 +00:00
|
|
|
|
2020-11-08 22:06:49 +00:00
|
|
|
# Executes code within the example's test context.
|
|
|
|
# This is an advanced method intended for internal usage only.
|
|
|
|
#
|
|
|
|
# The *klass* defines the type of the test context.
|
|
|
|
# This is typically only known by the code constructing the example.
|
|
|
|
# An error will be raised if *klass* doesn't match the test context's type.
|
|
|
|
# The block given to this method will be executed within the test context.
|
|
|
|
#
|
2021-01-16 23:52:16 +00:00
|
|
|
# The context casted to an instance of *klass* is provided as a block argument.
|
|
|
|
#
|
2020-11-08 22:06:49 +00:00
|
|
|
# TODO: Benchmark compiler performance using this method versus client-side casting in a proc.
|
2021-01-16 23:52:16 +00:00
|
|
|
protected def with_context(klass)
|
2020-11-08 23:53:54 +00:00
|
|
|
context = klass.cast(@context)
|
2020-11-08 22:06:49 +00:00
|
|
|
with context yield
|
|
|
|
end
|
|
|
|
|
2021-01-16 23:52:16 +00:00
|
|
|
# Casts the example's test context to a specific type.
|
|
|
|
# This is an advanced method intended for internal usage only.
|
|
|
|
#
|
|
|
|
# The *klass* defines the type of the test context.
|
|
|
|
# This is typically only known by the code constructing the example.
|
|
|
|
# An error will be raised if *klass* doesn't match the test context's type.
|
|
|
|
#
|
|
|
|
# The context casted to an instance of *klass* is returned.
|
|
|
|
#
|
|
|
|
# TODO: Benchmark compiler performance using this method versus client-side casting in a proc.
|
|
|
|
protected def cast_context(klass)
|
|
|
|
klass.cast(@context)
|
|
|
|
end
|
|
|
|
|
2021-01-16 18:49:43 +00:00
|
|
|
# Constructs the full name or description of the example.
|
|
|
|
# This prepends names of groups this example is part of.
|
|
|
|
def to_s(io)
|
2021-05-08 03:04:17 +00:00
|
|
|
name = @name
|
|
|
|
|
|
|
|
# Prefix with group's full name if the node belongs to a group.
|
2021-05-08 18:10:27 +00:00
|
|
|
if (parent = @group)
|
|
|
|
parent.to_s(io)
|
2021-05-08 03:04:17 +00:00
|
|
|
|
|
|
|
# Add padding between the node names
|
|
|
|
# only if the names don't appear to be symbolic.
|
|
|
|
# Skip blank group names (like the root group).
|
2021-05-08 18:10:27 +00:00
|
|
|
io << ' ' unless !parent.name? || # ameba:disable Style/NegatedConditionsInUnless
|
|
|
|
(parent.name?.is_a?(Symbol) && name.is_a?(String) &&
|
2021-05-08 03:04:17 +00:00
|
|
|
(name.starts_with?('#') || name.starts_with?('.')))
|
2021-01-16 18:49:43 +00:00
|
|
|
end
|
2021-05-08 03:04:17 +00:00
|
|
|
|
|
|
|
super
|
2021-01-16 18:49:43 +00:00
|
|
|
end
|
|
|
|
|
2020-09-06 01:54:55 +00:00
|
|
|
# Exposes information about the example useful for debugging.
|
|
|
|
def inspect(io)
|
2021-05-08 19:22:13 +00:00
|
|
|
super
|
2021-05-30 20:21:42 +00:00
|
|
|
io << ' ' << result
|
2020-09-06 01:54:55 +00:00
|
|
|
end
|
2021-01-16 23:28:33 +00:00
|
|
|
|
2021-01-31 03:07:36 +00:00
|
|
|
# Creates the JSON representation of the example,
|
|
|
|
# which is just its name.
|
2021-06-03 04:48:48 +00:00
|
|
|
def to_json(json : JSON::Builder)
|
|
|
|
json.object do
|
|
|
|
json.field("description", name? || "<anonymous>")
|
|
|
|
json.field("full_description", to_s)
|
2021-06-03 05:35:41 +00:00
|
|
|
if location = location?
|
|
|
|
json.field("file_path", location.path)
|
|
|
|
json.field("line_number", location.line)
|
|
|
|
end
|
2021-06-03 04:48:48 +00:00
|
|
|
@result.to_json(json) if @finished
|
|
|
|
end
|
2021-01-31 02:42:46 +00:00
|
|
|
end
|
|
|
|
|
2021-05-08 18:43:41 +00:00
|
|
|
# Creates a procsy from this example and the provided block.
|
|
|
|
def procsy(&block : ->)
|
|
|
|
Procsy.new(self, &block)
|
|
|
|
end
|
|
|
|
|
2021-01-16 23:28:33 +00:00
|
|
|
# Wraps an example to behave like a `Proc`.
|
|
|
|
# This is typically used for an *around_each* hook.
|
|
|
|
# Invoking `#call` or `#run` will run the example.
|
|
|
|
struct Procsy
|
|
|
|
# Underlying example that will run.
|
|
|
|
getter example : Example
|
|
|
|
|
|
|
|
# Creates the example proxy.
|
|
|
|
# The *example* should be run eventually.
|
|
|
|
# The *proc* defines the block of code to run when `#call` or `#run` is invoked.
|
|
|
|
def initialize(@example : Example, &@proc : ->)
|
|
|
|
end
|
|
|
|
|
|
|
|
# Invokes the proc.
|
|
|
|
def call : Nil
|
|
|
|
@proc.call
|
|
|
|
end
|
|
|
|
|
|
|
|
# Invokes the proc.
|
|
|
|
def run : Nil
|
|
|
|
@proc.call
|
|
|
|
end
|
|
|
|
|
|
|
|
# Creates a new procsy for a block and the example from this instance.
|
|
|
|
def wrap(&block : ->) : self
|
|
|
|
self.class.new(@example, &block)
|
|
|
|
end
|
|
|
|
|
2021-01-16 23:52:16 +00:00
|
|
|
# Executes code within the example's test context.
|
|
|
|
# This is an advanced method intended for internal usage only.
|
|
|
|
#
|
|
|
|
# The *klass* defines the type of the test context.
|
|
|
|
# This is typically only known by the code constructing the example.
|
|
|
|
# An error will be raised if *klass* doesn't match the test context's type.
|
|
|
|
# The block given to this method will be executed within the test context.
|
|
|
|
#
|
|
|
|
# TODO: Benchmark compiler performance using this method versus client-side casting in a proc.
|
|
|
|
protected def with_context(klass)
|
|
|
|
context = @example.cast_context(klass)
|
|
|
|
with context yield
|
|
|
|
end
|
|
|
|
|
2021-01-16 23:28:33 +00:00
|
|
|
# Allow instance to behave like an example.
|
|
|
|
forward_missing_to @example
|
|
|
|
end
|
2018-08-19 07:15:32 +00:00
|
|
|
end
|
|
|
|
end
|