From f433405ece3c97c5960a6a33fde431238b151b72 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Nov 2020 15:06:49 -0700 Subject: [PATCH] Scratch work Trying to implement hooks. Ran into a problem with contexts. --- src/spectator/dsl/builder.cr | 28 ++++++++++ src/spectator/dsl/hooks.cr | 9 +--- src/spectator/events.cr | 62 +++++++++++++++++++++++ src/spectator/example.cr | 26 ++++++++-- src/spectator/example_context_delegate.cr | 3 ++ src/spectator/example_group.cr | 7 +++ src/spectator/spec_builder.cr | 40 +++++++++++++++ 7 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 src/spectator/events.cr diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index dc4647c..2b791da 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -35,6 +35,34 @@ module Spectator::DSL @@builder.add_example(*args, &block) end + # Defines a block of code to execute before any and all examples in the current group. + # + # See `Spec::Builder#before_all` for usage details. + def before_all(&block) + @@builder.before_all(&block) + end + + # Defines a block of code to execute before every example in the current group + # + # See `Spec::Builder#before_each` for usage details. + def before_each(&block : Example, Context -> _) + @@builder.before_each(&block) + end + + # Defines a block of code to execute after any and all examples in the current group. + # + # See `Spec::Builder#after_all` for usage details. + def after_all(&block) + @@builder.after_all(&block) + end + + # Defines a block of code to execute after every example in the current group. + # + # See `Spec::Builder#after_each` for usage details. + def after_each(&block : Example, Context ->) + @@builder.after_each(&block) + end + # Sets the configuration of the spec. # # See `Spec::Builder#config=` for usage details. diff --git a/src/spectator/dsl/hooks.cr b/src/spectator/dsl/hooks.cr index c6c6154..00eb251 100644 --- a/src/spectator/dsl/hooks.cr +++ b/src/spectator/dsl/hooks.cr @@ -9,9 +9,7 @@ module Spectator::DSL {{block.body}} end - ::Spectator::DSL::Builder.add_hook( - :before - ) { {{@type.name}.%hook } + ::Spectator::DSL::Builder.before_all { {{@type.name}.%hook } end macro before_each(&block) @@ -21,10 +19,7 @@ module Spectator::DSL {{block.body}} end - ::Spectator::DSL::Builder.add_context_hook( - :before, - {{@type.name}} - ) { |context| context.as({{@type.name}).%hook } + ::Spectator::DSL::Builder.before_each { |context| context.as({{@type.name}).%hook } end macro after_all(&block) diff --git a/src/spectator/events.cr b/src/spectator/events.cr new file mode 100644 index 0000000..ae8bd60 --- /dev/null +++ b/src/spectator/events.cr @@ -0,0 +1,62 @@ +require "./example_context_delegate" + +module Spectator + # Mix-in for managing events and hooks. + # This module is intended to be included by `ExampleGroup`. + module Events + # Defines an event for an example group. + # This event typically runs before or after an example group finishes. + # No contextual information (or example) is provided to the hooks. + # The *name* defines the name of the event. + # This must be unique across all events. + # Two methods are defined - one to add a hook and the other to trigger the event which calls every hook. + private macro group_event(name) + @{{name.id}}_hooks = Deque(->).new + + # Defines a hook for the *{{name.id}}* event. + # The block of code given to this method is invoked when the event occurs. + def {{name.id}}(&block) : Nil + @{{name.id}}_hooks << block + end + + # Signals that the *{{name.id}}* event has occurred. + # All hooks associated with the event will be called. + def call_{{name.id}} : Nil + @{{name.id}}_hooks.each(&.call) + end + end + + # Defines an event for an example. + # This event typically runs before or after an example finishes. + # The current example is provided to the hooks. + # The *name* defines the name of the event. + # This must be unique across all events. + # Three methods are defined - two to add a hook and the other to trigger the event which calls every hook. + # A hook can be added with an `ExampleContextDelegate` or a block that accepts an example (no context). + private macro example_event(name) + @{{name.id}}_hooks = Deque(ExampleContextMethod).new + + # Defines a hook for the *{{name.id}}* event. + # The *delegate* will be called when the event occurs. + # The current example is provided to the delegate. + def {{name.id}}(delegate : ExampleContextDelegate) : Nil + @{{name.id}}_hooks << delegate + end + + # Defines a hook for the *{{name.id}}* event. + # The block of code given to this method is invoked when the event occurs. + # The current example is provided as a block argument. + def {{name.id}}(&block : Example -> _) : Nil + delegate = ExampleContextDelegate.null(&block) + @{{name.id}}_hooks << delegate + end + + # Signals that the *{{name.id}}* event has occurred. + # All hooks associated with the event will be called. + # The *example* should be the current example. + def call_{{name.id}}(example) : Nil + @{{name.id}}_hooks.each(&.call(example)) + end + end + end +end diff --git a/src/spectator/example.cr b/src/spectator/example.cr index b8c1207..27e67d6 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -19,12 +19,13 @@ module Spectator getter result : Result = PendingResult.new # Creates the example. - # The *delegate* contains the test context and method that runs the test case. + # 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 `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 : ExampleContextDelegate, + def initialize(@context : Context, @entrypoint : ExampleContextMethod, name : String? = nil, source : Source? = nil, group : ExampleGroup? = nil) super(name, source, group) end @@ -37,7 +38,8 @@ module Spectator # 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? = nil, source : Source? = nil, group : ExampleGroup? = nil, &block : Example -> _) - @delegate = ExampleContextDelegate.null(&block) + @context = NullContext.new + @entrypoint = block end # Executes the test case. @@ -46,13 +48,27 @@ module Spectator def run : Result @@current = self Log.debug { "Running example #{self}" } - Log.warn { "Example #{self} running more than once" } if @finished - @result = Harness.run { @delegate.call(self) } + Log.warn { "Example #{self} already ran" } if @finished + @result = Harness.run { @entrypoint.call(self, @context) } ensure @@current = nil @finished = true 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. + def with_context(klass) + context = klass.cast(@delegate.context) + with context yield + end + # Exposes information about the example useful for debugging. def inspect(io) # Full example name. diff --git a/src/spectator/example_context_delegate.cr b/src/spectator/example_context_delegate.cr index cd8f6bb..0b61f57 100644 --- a/src/spectator/example_context_delegate.cr +++ b/src/spectator/example_context_delegate.cr @@ -6,6 +6,9 @@ module Spectator # Stores a test context and a method to call within it. # This is a variant of `ContextDelegate` that accepts the current running example. struct ExampleContextDelegate + # Retrieves the underlying context. + protected getter context : Context + # Creates the delegate. # The *context* is the instance of the test context. # The *method* is proc that downcasts *context* and calls a method on it. diff --git a/src/spectator/example_group.cr b/src/spectator/example_group.cr index d6d5f84..cd5bb71 100644 --- a/src/spectator/example_group.cr +++ b/src/spectator/example_group.cr @@ -1,11 +1,18 @@ +require "./events" require "./example_node" module Spectator # Collection of examples and sub-groups. class ExampleGroup < ExampleNode include Enumerable(ExampleNode) + include Events include Iterable(ExampleNode) + group_event before_all + group_event after_all + example_event before_each + example_event after_each + @nodes = [] of ExampleNode # Removes the specified *node* from the group. diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 4c54b17..d6be9f2 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -98,6 +98,46 @@ module Spectator # The example is added to the current group by `Example` initializer. end + # Defines a block of code to execute before any and all examples in the current group. + def before_all(&block) + Log.trace { "Add before_all hook" } + current_group.before_all(&block) + end + + # Defines a delegate to call before every example in the current group. + # The current example is provided to the delegate. + def before_each(delegate : ExampleContextDelegate) + Log.trace { "Add before_each hook delegate" } + current_group.before_each(delegate) + end + + # Defines a block of code to execute before every example in the current group. + # The current example is provided as a block argument. + def before_each(&block : Example -> _) + Log.trace { "Add before_each hook block" } + current_group.before_each(&block) + end + + # Defines a block of code to execute after any and all examples in the current group. + def after_all(&block) + Log.trace { "Add after_all hook" } + current_group.after_all(&block) + end + + # Defines a delegate to call after every example in the current group. + # The current example is provided to the delegate. + def after_each(delegate : ExampleContextDelegate) + Log.trace { "Add after_each hook delegate" } + current_group.after_each(delegate) + end + + # Defines a block of code to execute after every example in the current group. + # The current example is provided as a block argument. + def after_each(&block : Example -> _) + Log.trace { "Add after_each hook" } + current_group.after_each(&block) + end + # Builds the configuration to use for the spec. # A `ConfigBuilder` is yielded to the block provided to this method. # That builder will be used to create the configuration.