shard-spectator/src/spectator/example.cr

298 lines
10 KiB
Crystal
Raw Normal View History

2020-09-06 16:31:23 +00:00
require "./example_context_delegate"
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"
require "./result"
require "./metadata"
2018-08-19 07:15:32 +00:00
module Spectator
# 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
# 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?
# Indicates whether the example already ran.
getter? finished : Bool = false
# Result of the last time the example ran.
# Is pending if the example hasn't run.
getter result : Result = PendingResult.new("Example not run")
@name_proc : Proc(Example, String)?
# Creates the example.
# An instance to run the test code in is given by *context*.
# The *entrypoint* defines the test code (typically inside *context*).
# The *name* describes the purpose of the example.
2021-02-13 05:46:22 +00:00
# The *location* tracks where the example exists in source code.
# The example will be assigned to *group* if it is provided.
# A set of *metadata* can be used for filtering and modifying example behavior.
# Note: The metadata will not be merged with the parent metadata.
def initialize(@context : Context, @entrypoint : self ->,
2021-02-13 05:46:22 +00:00
name : String? = nil, location : Location? = nil,
2022-11-30 06:22:42 +00:00
@group : ExampleGroup? = nil, metadata = nil)
super(name, location, metadata)
# Ensure group is linked.
group << self if group
end
# Creates the example.
# An instance to run the test code in is given by *context*.
# The *entrypoint* defines the test code (typically inside *context*).
# The *name* describes the purpose of the example.
# It can be a proc to be evaluated in the context of the example.
# The *location* tracks where the example exists in source code.
# The example will be assigned to *group* if it is provided.
# A set of *metadata* can be used for filtering and modifying example behavior.
# Note: The metadata will not be merged with the parent metadata.
def initialize(@context : Context, @entrypoint : self ->,
@name_proc : Example -> String, location : Location? = nil,
@group : ExampleGroup? = nil, metadata = nil)
super(nil, location, metadata)
# Ensure group is linked.
group << self if group
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.
# A set of *metadata* can be used for filtering and modifying example behavior.
# Note: The metadata will not be merged with the parent metadata.
2021-05-08 18:10:27 +00:00
def initialize(name : String? = nil, location : Location? = nil,
2022-11-30 06:22:42 +00:00
@group : ExampleGroup? = nil, metadata = nil, &block : self ->)
super(name, location, metadata)
@context = NullContext.new
@entrypoint = block
# Ensure group is linked.
group << self if group
2020-11-07 21:43:59 +00:00
end
# 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 *metadata* can be used for filtering and modifying example behavior.
# Note: The metadata will not be merged with the parent metadata.
def self.pending(name : String? = nil, location : Location? = nil,
2022-11-30 06:22:42 +00:00
group : ExampleGroup? = nil, metadata = nil, reason = nil)
# Add pending tag and reason if they don't exist.
2022-11-30 06:22:42 +00:00
tags = {:pending => nil, :reason => reason}
metadata = metadata ? metadata.merge(tags) { |_, v, _| v } : tags
new(name, location, group, metadata) { nil }
end
# Executes the test case.
# Returns the result of the execution.
# The result will also be stored in `#result`.
def run : Result
2022-11-05 04:05:27 +00:00
Log.debug { "Running example: #{self}" }
Log.warn { "Example already ran: #{self}" } 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
return @result = PendingResult.new(pending_reason)
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
if proc = @name_proc
self.name = proc.call(self)
end
@group.try(&.call_before_all)
2021-05-08 18:10:27 +00:00
if (parent = @group)
parent.call_around_each(procsy).call
2021-01-16 23:28:33 +00:00
else
run_internal
end
2021-05-08 18:10:27 +00:00
if (parent = @group)
parent.call_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
if group = @group
group.call_before_each(self)
group.call_pre_condition(self)
end
2022-11-05 04:05:27 +00:00
Log.trace { "Running example code for: #{self}" }
2021-01-16 23:28:33 +00:00
@entrypoint.call(self)
2020-10-17 20:56:31 +00:00
@finished = true
2022-11-05 04:05:27 +00:00
Log.trace { "Finished running example code for: #{self}" }
if group = @group
group.call_post_condition(self)
group.call_after_each(self)
end
end
# 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.
#
# The context casted to an instance of *klass* is provided as a block argument.
#
# TODO: Benchmark compiler performance using this method versus client-side casting in a proc.
protected def with_context(klass)
context = klass.cast(@context)
with context yield
end
# 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-08-18 04:10:01 +00:00
# Yields this example and all parent groups.
def ascend
node = self
while node
yield node
node = node.group?
end
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 : IO) : Nil
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)
# 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) &&
(name.starts_with?('#') || name.starts_with?('.')))
2021-01-16 18:49:43 +00:00
end
super
2021-01-16 18:49:43 +00:00
end
# Exposes information about the example useful for debugging.
def inspect(io : IO) : Nil
2021-05-08 19:22:13 +00:00
super
2022-11-05 04:05:27 +00:00
io << " - " << result
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
# Creates a procsy from this example that runs the example.
def procsy
Procsy.new(self) { run_internal }
end
# 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
# 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
# Constructs the full name or description of the example.
# This prepends names of groups this example is part of.
def to_s(io : IO) : Nil
@example.to_s(io)
end
2021-01-16 23:28:33 +00:00
end
2018-08-19 07:15:32 +00:00
end
end