From 4230ec70a023d372ccd1435242c1fd4a13c7d7fc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 7 Nov 2020 20:56:30 -0700 Subject: [PATCH] Move test handling code to Harness --- src/spectator/example.cr | 32 ++++------- src/spectator/harness.cr | 116 ++++++++++++++++++++++----------------- 2 files changed, 76 insertions(+), 72 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 67fdfff..b8c1207 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -1,7 +1,7 @@ require "./example_context_delegate" require "./example_group" require "./example_node" -require "./pass_result" +require "./harness" require "./pending_result" require "./result" require "./source" @@ -9,6 +9,9 @@ require "./source" module Spectator # Standard example that runs a test case. class Example < ExampleNode + # Currently running example. + class_getter! current : Example + # Indicates whether the example already ran. getter? finished : Bool = false @@ -41,9 +44,13 @@ module Spectator # Returns the result of the execution. # The result will also be stored in `#result`. def run : Result - runner = Runner.new(self, @delegate) + @@current = self + Log.debug { "Running example #{self}" } + Log.warn { "Example #{self} running more than once" } if @finished + @result = Harness.run { @delegate.call(self) } + ensure + @@current = nil @finished = true - @result = runner.run end # Exposes information about the example useful for debugging. @@ -61,24 +68,5 @@ module Spectator io << result end - - # Logic dedicated to running an example and necessary hooks. - # This type does not directly modify or mutate state in the `Example` class. - private struct Runner - # Creates the runner. - # *example* is the example being tested. - # The *delegate* is the entrypoint of the example's test code. - def initialize(@example : Example, @delegate : ExampleContextDelegate) - end - - # Executes the example's test code and produces a result. - def run : Result - Log.debug { "Running example #{@example}" } - elapsed = Time.measure do - @delegate.call(@example) - end - PassResult.new(elapsed) - end - end end end diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 5268edc..37bf1eb 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -1,78 +1,94 @@ -require "./mocks/registry" +require "./error_result" +require "./pass_result" +require "./result" module Spectator - # Helper class that acts as a gateway between example code and the test framework. - # Every example must be invoked by passing it to `#run`. - # This sets up the harness so that the example code can use it. - # The test framework does the following: + # Helper class that acts as a gateway between test code and the framework. + # This is essentially an "example runner." + # + # 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: # ``` - # result = Harness.run(example) + # result = Harness.run { delegate.call(example) } # # Do something with the result. # ``` - # Then from the example code, the harness can be accessed via `#current` like so: + # Then from the test code, the harness can be accessed via `.current` like so: # ``` # harness = ::Spectator::Harness.current # # Do something with the harness. # ``` # Of course, the end-user shouldn't see this or work directly with the harness. # Instead, methods the user calls can access it. - # For instance, an expectation reporting a result. - class Harness + # For instance, an assertion reporting a result. + private class Harness # Retrieves the harness for the current running example. class_getter! current : self - # Wraps an example with a harness and runs the example. - # The `#current` harness will be set - # prior to running the example, and reset after. - # The *example* argument will be the example to run. - # The result returned from `Example#run` will be returned. - def self.run(example : Example) : Result - @@current = new(example) - example.run - ensure - @@current = nil + # 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. + def self.run : Result + harness = new + previous = @@current + @@current = harness + result = harness.run { yield } + @@current = previous + result end - # Retrieves the current running example. - getter example : Example + @deferred = Deque(->).new - getter mocks : Mocks::Registry - - # Retrieves the group for the current running example. - def group - example.group + # 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. + def run : Result + outcome = capture { yield } + run_deferred # TODO: Handle errors in deferred blocks. + translate(*outcome) end - # Reports the outcome of an expectation. - # An exception will be raised when a failing result is given. - def report_expectation(expectation : Expectations::Expectation) : Nil - @example.description = expectation.description unless @example.test_wrapper.description? - @reporter.report(expectation) - end - - # Generates the reported expectations from the example. - # This should be run after the example has finished. - def expectations : Expectations::ExampleExpectations - @reporter.expectations - end - - # Marks a block of code to run later. - def defer(&block : ->) : Nil + # 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 end + # 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). + private def capture + error = nil.as(Exception?) + elapsed = Time.measure do + begin + yield + rescue e + error = e + end + end + {elapsed, error} + end + + # 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 + if error + ErrorResult.new(elapsed, error) + else + PassResult.new(elapsed) + end + end + # Runs all deferred blocks. - def run_deferred : Nil + private def run_deferred : Nil @deferred.each(&.call) @deferred.clear end - - # Creates a new harness. - # The example the harness is for should be passed in. - private def initialize(@example) - @reporter = Expectations::ExpectationReporter.new - @mocks = Mocks::Registry.new(@example.group.context) - @deferred = Deque(->).new - end end end