2020-11-08 03:56:30 +00:00
|
|
|
require "./error_result"
|
2021-07-10 09:32:55 +00:00
|
|
|
require "./example_failed"
|
2021-06-10 03:57:17 +00:00
|
|
|
require "./example_pending"
|
2021-01-21 07:03:57 +00:00
|
|
|
require "./expectation"
|
2021-07-31 17:56:53 +00:00
|
|
|
require "./expectation_failed"
|
|
|
|
require "./multiple_expectations_failed"
|
2020-11-08 03:56:30 +00:00
|
|
|
require "./pass_result"
|
|
|
|
require "./result"
|
2019-11-10 06:22:21 +00:00
|
|
|
|
2019-09-26 21:11:54 +00:00
|
|
|
module Spectator
|
2020-11-08 03:56:30 +00:00
|
|
|
# Helper class that acts as a gateway between test code and the framework.
|
|
|
|
#
|
|
|
|
# Test code should be wrapped with a call to `.run`.
|
|
|
|
# This class will catch all errors raised by the test code.
|
|
|
|
# Errors caused by failed assertions (`AssertionFailed`) are translated to failed results (`FailResult`).
|
|
|
|
# Errors not caused by assertions are translated to error results (`ErrorResult`).
|
|
|
|
#
|
|
|
|
# Every runnable example should invoke the test code by calling `.run`.
|
|
|
|
# This sets up the harness so that the test code can use it.
|
|
|
|
# The framework does the following:
|
2018-10-09 20:25:39 +00:00
|
|
|
# ```
|
2021-01-16 18:03:05 +00:00
|
|
|
# result = Harness.run { run_example_code }
|
2018-10-09 20:25:39 +00:00
|
|
|
# # Do something with the result.
|
|
|
|
# ```
|
2021-01-16 18:03:05 +00:00
|
|
|
#
|
2020-11-08 03:56:30 +00:00
|
|
|
# Then from the test code, the harness can be accessed via `.current` like so:
|
2018-10-09 20:25:39 +00:00
|
|
|
# ```
|
2019-09-26 21:11:54 +00:00
|
|
|
# harness = ::Spectator::Harness.current
|
2018-10-09 20:25:39 +00:00
|
|
|
# # Do something with the harness.
|
|
|
|
# ```
|
2021-01-16 18:03:05 +00:00
|
|
|
#
|
2018-10-09 20:25:39 +00:00
|
|
|
# Of course, the end-user shouldn't see this or work directly with the harness.
|
2021-01-16 18:03:05 +00:00
|
|
|
# Instead, methods the test calls can access it.
|
|
|
|
# For instance, an expectation reporting a result.
|
|
|
|
class Harness
|
2021-01-21 07:03:57 +00:00
|
|
|
Log = ::Spectator::Log.for(self)
|
|
|
|
|
2018-10-09 20:25:39 +00:00
|
|
|
# Retrieves the harness for the current running example.
|
|
|
|
class_getter! current : self
|
|
|
|
|
2020-11-08 03:56:30 +00:00
|
|
|
# Wraps an example with a harness and runs test code.
|
|
|
|
# A block provided to this method is considered to be the test code.
|
|
|
|
# The value of `.current` is set to the harness for the duration of the test.
|
|
|
|
# It will be reset after the test regardless of the outcome.
|
|
|
|
# The result of running the test code will be returned.
|
2023-01-27 00:19:31 +00:00
|
|
|
def self.run(&) : Result
|
2021-05-07 04:10:40 +00:00
|
|
|
with_harness do |harness|
|
|
|
|
harness.run { yield }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-11-28 22:45:17 +00:00
|
|
|
# Instantiates a new harness and yields it.
|
2021-05-07 04:10:40 +00:00
|
|
|
# The `.current` harness is set to the new harness for the duration of the block.
|
|
|
|
# `.current` is reset to the previous value (probably nil) afterwards, even if the block raises.
|
|
|
|
# The result of the block is returned.
|
2023-01-27 00:19:31 +00:00
|
|
|
private def self.with_harness(&)
|
2020-11-08 03:56:30 +00:00
|
|
|
previous = @@current
|
2021-05-07 04:10:40 +00:00
|
|
|
begin
|
2021-05-29 23:13:14 +00:00
|
|
|
@@current = harness = new
|
2021-05-07 04:10:40 +00:00
|
|
|
yield harness
|
|
|
|
ensure
|
|
|
|
@@current = previous
|
|
|
|
end
|
2018-10-09 20:25:39 +00:00
|
|
|
end
|
|
|
|
|
2020-11-08 03:56:30 +00:00
|
|
|
@deferred = Deque(->).new
|
2022-07-13 02:40:27 +00:00
|
|
|
@cleanup = Deque(->).new
|
2021-01-31 03:07:45 +00:00
|
|
|
@expectations = [] of Expectation
|
2021-07-31 17:56:53 +00:00
|
|
|
@aggregate : Array(Expectation)? = nil
|
2018-10-09 20:25:39 +00:00
|
|
|
|
2020-11-08 03:56:30 +00:00
|
|
|
# Runs test code and produces a result based on the outcome.
|
|
|
|
# The test code should be called from within the block given to this method.
|
2023-01-27 00:19:31 +00:00
|
|
|
def run(&) : Result
|
2021-01-31 03:53:09 +00:00
|
|
|
elapsed, error = capture { yield }
|
2021-05-07 04:10:40 +00:00
|
|
|
elapsed2, error2 = capture { run_deferred }
|
2022-07-13 02:40:27 +00:00
|
|
|
run_cleanup
|
2021-01-31 03:53:09 +00:00
|
|
|
translate(elapsed + elapsed2, error || error2)
|
2019-08-30 20:46:24 +00:00
|
|
|
end
|
|
|
|
|
2021-07-31 17:56:53 +00:00
|
|
|
def report(expectation : Expectation) : Bool
|
2021-01-21 07:03:57 +00:00
|
|
|
Log.debug { "Reporting expectation #{expectation}" }
|
2021-01-31 03:07:45 +00:00
|
|
|
@expectations << expectation
|
2021-05-07 04:10:40 +00:00
|
|
|
|
|
|
|
# TODO: Move this out of harness, maybe to `Example`.
|
2021-01-31 04:00:01 +00:00
|
|
|
Example.current.name = expectation.description unless Example.current.name?
|
2021-05-07 04:10:40 +00:00
|
|
|
|
2021-07-31 17:56:53 +00:00
|
|
|
if expectation.failed?
|
|
|
|
raise ExpectationFailed.new(expectation, expectation.failure_message) unless (aggregate = @aggregate)
|
|
|
|
aggregate << expectation
|
|
|
|
false
|
|
|
|
else
|
|
|
|
true
|
|
|
|
end
|
2021-01-16 18:03:05 +00:00
|
|
|
end
|
|
|
|
|
2020-11-08 03:56:30 +00:00
|
|
|
# Stores a block of code to be executed later.
|
|
|
|
# All deferred blocks run just before the `#run` method completes.
|
|
|
|
def defer(&block) : Nil
|
|
|
|
@deferred << block
|
2018-10-09 20:41:22 +00:00
|
|
|
end
|
|
|
|
|
2022-07-13 02:40:27 +00:00
|
|
|
# Stores a block of code to be executed at cleanup.
|
|
|
|
# Cleanup is run after everything else, even deferred blocks.
|
|
|
|
# Each cleanup step is wrapped in error handling so that one failure doesn't block the next ones.
|
|
|
|
def cleanup(&block) : Nil
|
|
|
|
@cleanup << block
|
|
|
|
end
|
|
|
|
|
2023-01-27 00:19:31 +00:00
|
|
|
def aggregate_failures(label = nil, &)
|
2021-07-31 17:56:53 +00:00
|
|
|
previous = @aggregate
|
|
|
|
@aggregate = aggregate = [] of Expectation
|
|
|
|
begin
|
|
|
|
yield.tap do
|
|
|
|
# If there's an nested aggregate (for some reason), allow the top-level one to handle things.
|
2021-07-31 18:04:43 +00:00
|
|
|
check_aggregate(aggregate, label) unless previous
|
2021-07-31 17:56:53 +00:00
|
|
|
end
|
|
|
|
ensure
|
|
|
|
@aggregate = previous
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-07-31 18:04:43 +00:00
|
|
|
private def check_aggregate(aggregate, label)
|
2021-07-31 17:56:53 +00:00
|
|
|
failures = aggregate.select(&.failed?)
|
|
|
|
case failures.size
|
|
|
|
when 0 then return
|
|
|
|
when 1
|
|
|
|
expectation = failures.first
|
|
|
|
raise ExpectationFailed.new(expectation, expectation.failure_message)
|
|
|
|
else
|
2021-07-31 18:04:43 +00:00
|
|
|
message = "Got #{failures.size} failures from failure aggregation block"
|
|
|
|
message += " \"#{label}\"" if label
|
|
|
|
raise MultipleExpectationsFailed.new(failures, message)
|
2021-07-31 17:56:53 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-11-08 03:56:30 +00:00
|
|
|
# Yields to run the test code and returns information about the outcome.
|
|
|
|
# Returns a tuple with the elapsed time and an error if one occurred (otherwise nil).
|
2023-01-27 00:19:31 +00:00
|
|
|
private def capture(&) : Tuple(Time::Span, Exception?)
|
2021-05-13 03:41:56 +00:00
|
|
|
error = nil
|
2020-11-08 03:56:30 +00:00
|
|
|
elapsed = Time.measure do
|
2021-05-13 03:40:48 +00:00
|
|
|
error = catch { yield }
|
2020-11-08 03:56:30 +00:00
|
|
|
end
|
|
|
|
{elapsed, error}
|
2018-10-09 20:41:22 +00:00
|
|
|
end
|
2018-10-09 20:25:39 +00:00
|
|
|
|
2021-05-07 04:10:40 +00:00
|
|
|
# Yields to run a block of code and captures exceptions.
|
|
|
|
# If the block of code raises an error, the error is caught and returned.
|
|
|
|
# If the block doesn't raise an error, then nil is returned.
|
2023-01-27 00:19:31 +00:00
|
|
|
private def catch(&) : Exception?
|
2021-05-07 04:10:40 +00:00
|
|
|
yield
|
|
|
|
rescue e
|
|
|
|
e
|
|
|
|
else
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
2020-11-08 03:56:30 +00:00
|
|
|
# Translates the outcome of running a test to a result.
|
|
|
|
# Takes the *elapsed* time and a possible *error* from the test.
|
|
|
|
# Returns a type of `Result`.
|
|
|
|
private def translate(elapsed, error) : Result
|
2021-01-21 07:03:57 +00:00
|
|
|
case error
|
|
|
|
when nil
|
2021-04-27 00:47:11 +00:00
|
|
|
PassResult.new(elapsed, @expectations)
|
2021-07-10 09:32:55 +00:00
|
|
|
when ExampleFailed
|
2021-04-27 00:47:11 +00:00
|
|
|
FailResult.new(elapsed, error, @expectations)
|
2021-06-10 03:57:17 +00:00
|
|
|
when ExamplePending
|
2021-07-17 22:25:32 +00:00
|
|
|
PendingResult.new(error.message || PendingResult::DEFAULT_REASON, error.location, elapsed, @expectations)
|
2021-01-21 07:03:57 +00:00
|
|
|
else
|
2021-04-27 00:47:11 +00:00
|
|
|
ErrorResult.new(elapsed, error, @expectations)
|
2020-11-08 03:56:30 +00:00
|
|
|
end
|
2019-11-15 01:30:48 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
# Runs all deferred blocks.
|
2021-05-07 04:10:40 +00:00
|
|
|
# This method executes code from tests and may raise an error.
|
|
|
|
# It should be wrapped in a call to `#capture`.
|
2021-01-31 03:53:09 +00:00
|
|
|
private def run_deferred
|
2021-01-31 04:07:42 +00:00
|
|
|
Log.debug { "Running deferred operations" }
|
2021-05-07 04:10:40 +00:00
|
|
|
@deferred.each(&.call)
|
2019-11-15 01:30:48 +00:00
|
|
|
end
|
2022-07-13 02:40:27 +00:00
|
|
|
|
|
|
|
# Invokes all cleanup callbacks.
|
|
|
|
# Each callback is wrapped with error handling.
|
|
|
|
private def run_cleanup
|
|
|
|
Log.debug { "Running cleanup" }
|
|
|
|
@cleanup.each do |callback|
|
|
|
|
callback.call
|
|
|
|
rescue e
|
|
|
|
Log.error(exception: e) { "Encountered error during cleanup" }
|
|
|
|
end
|
|
|
|
end
|
2018-10-09 20:25:39 +00:00
|
|
|
end
|
|
|
|
end
|