diff --git a/src/spectator/dsl/builder.cr b/src/spectator/dsl/builder.cr index 9ebedaf..e0b724d 100644 --- a/src/spectator/dsl/builder.cr +++ b/src/spectator/dsl/builder.cr @@ -1,7 +1,7 @@ require "../example_group_hook" require "../example_hook" require "../example_procsy_hook" -require "../spec/builder" +require "../spec_builder" module Spectator::DSL # Incrementally builds up a test spec from the DSL. @@ -10,7 +10,7 @@ module Spectator::DSL extend self # Underlying spec builder. - @@builder = Spec::Builder.new + @@builder = SpecBuilder.new # Defines a new example group and pushes it onto the group stack. # Examples and groups defined after calling this method will be nested under the new group. diff --git a/src/spectator/includes.cr b/src/spectator/includes.cr index c611475..7f87649 100644 --- a/src/spectator/includes.cr +++ b/src/spectator/includes.cr @@ -47,6 +47,9 @@ require "./pending_result" require "./profile" require "./report" require "./result" +require "./runner_events" +require "./runner" +require "./spec_builder" require "./spec" require "./test_context" require "./value" diff --git a/src/spectator/runner.cr b/src/spectator/runner.cr new file mode 100644 index 0000000..49b6065 --- /dev/null +++ b/src/spectator/runner.cr @@ -0,0 +1,98 @@ +require "./example" +require "./formatting/formatter" +require "./profile" +require "./report" +require "./run_flags" +require "./runner_events" + +module Spectator + # Logic for executing examples and collecting results. + struct Runner + include RunnerEvents + + # Formatter to send events to. + private getter formatter : Formatting::Formatter + + # Creates the runner. + # The collection of *examples* should be pre-filtered and shuffled. + # This runner will run each example in the order provided. + # The *formatter* will be called for various events. + def initialize(@examples : Array(Example), @formatter : Formatting::Formatter, + @run_flags = RunFlags::None, @random_seed : UInt64? = nil) + end + + # Runs the spec. + # This will run the provided examples + # and invoke the reporters to communicate results. + # True will be returned if the spec ran successfully, + # or false if there was at least one failure. + def run : Bool + start + elapsed = Time.measure { run_examples } + stop + + report = Report.generate(@examples, elapsed, @random_seed) + profile = Profile.generate(@examples) if @run_flags.profile? && report.counts.run > 0 + summarize(report, profile) + + report.counts.fail.zero? + ensure + close + end + + # Attempts to run all examples. + # Returns a list of examples that ran. + private def run_examples + @examples.each do |example| + result = run_example(example) + + # Bail out if the example failed + # and configured to stop after the first failure. + break fail_fast if fail_fast? && result.fail? + end + end + + # Runs a single example and returns the result. + # The formatter is given the example and result information. + private def run_example(example) + example_started(example) + result = if dry_run? + # TODO: Pending examples return a pending result instead of pass in RSpec dry-run. + dry_run_result + else + example.run + end + example_finished(example) + result + end + + # Creates a fake result. + private def dry_run_result + expectations = [] of Expectation + PassResult.new(Time::Span.zero, expectations) + end + + # Generates and returns a profile if one should be displayed. + private def profile(report) + Profile.generate(report) if @config.profile? + end + + # Indicates whether examples should be simulated, but not run. + private def dry_run? + @run_flags.dry_run? + end + + # Indicates whether test execution should stop after the first failure. + private def fail_fast? + @run_flags.fail_fast? + end + + private def fail_fast : Nil + end + + # Number of examples configured to run. + private def example_count + @examples.size + end + end +end diff --git a/src/spectator/runner_events.cr b/src/spectator/runner_events.cr new file mode 100644 index 0000000..8f4e97e --- /dev/null +++ b/src/spectator/runner_events.cr @@ -0,0 +1,93 @@ +require "./formatting/formatter" +require "./formatting/notifications" + +module Spectator + # Mix-in for announcing events from a `Runner`. + # All events invoke their corresponding method on the formatter. + module RunnerEvents + # Triggers the 'start' event. + # See `Formatting::Formatter#start` + private def start + notification = Formatting::StartNotification.new(example_count) + formatter.start(notification) + end + + # Triggers the 'example started' event. + # Must be passed the *example* about to run. + # See `Formatting::Formatter#example_started` + private def example_started(example) + notification = Formatting::ExampleNotification.new(example) + formatter.example_started(notification) + end + + # Triggers the 'example started' event. + # Also triggers the example result event corresponding to the example's outcome. + # Must be passed the completed *example*. + # See `Formatting::Formatter#example_finished` + private def example_finished(example) + notification = Formatting::ExampleNotification.new(example) + visitor = ResultVisitor.new(formatter, notification) + formatter.example_finished(notification) + example.result.accept(visitor) + end + + # Triggers the 'stop' event. + # See `Formatting::Formatter#stop` + private def stop + formatter.stop + end + + # Triggers the 'dump' events. + private def summarize(report, profile) + formatter.start_dump + + notification = Formatting::ExampleSummaryNotification.new(report.pending) + formatter.dump_pending(notification) + + notification = Formatting::ExampleSummaryNotification.new(report.failures) + formatter.dump_failures(notification) + + if profile + notification = Formatting::ProfileNotification.new(profile) + formatter.dump_profile(notification) + end + + notification = Formatting::SummaryNotification.new(report) + formatter.dump_summary(notification) + end + + # Triggers the 'close' event. + # See `Formatting::Formatter#close` + private def close + formatter.close + end + + # Provides methods for the various result types. + private struct ResultVisitor + # Creates the visitor. + # Requires the *formatter* to notify and the *notification* to send it. + def initialize(@formatter : Formatting::Formatter, @notification : Formatting::ExampleNotification) + end + + # Invokes the example passed method. + def pass(_result) + @formatter.example_passed(@notification) + end + + # Invokes the example failed method. + def fail(_result) + @formatter.example_failed(@notification) + end + + # Invokes the example error method. + def error(_result) + @formatter.example_error(@notification) + end + + # Invokes the example pending method. + def pending(_result) + @formatter.example_pending(@notification) + end + end + end +end diff --git a/src/spectator/spec.cr b/src/spectator/spec.cr index a3894f4..9aec3a0 100644 --- a/src/spectator/spec.cr +++ b/src/spectator/spec.cr @@ -1,6 +1,6 @@ require "./config" require "./example_group" -require "./spec/*" +require "./runner" module Spectator # Contains examples to be tested and configuration for running them. diff --git a/src/spectator/spec/builder.cr b/src/spectator/spec/builder.cr deleted file mode 100644 index fe4eacd..0000000 --- a/src/spectator/spec/builder.cr +++ /dev/null @@ -1,255 +0,0 @@ -require "../config" -require "../example" -require "../example_context_method" -require "../example_group" -require "../iterative_example_group" -require "../spec" -require "../metadata" - -module Spectator - class Spec - # Progressively builds a test spec. - # - # A stack is used to track the current example group. - # Adding an example or group will nest it under the group at the top of the stack. - class Builder - Log = ::Spectator::Log.for(self) - - # Stack tracking the current group. - # The bottom of the stack (first element) is the root group. - # The root group should never be removed. - # The top of the stack (last element) is the current group. - # New examples should be added to the current group. - @group_stack : Deque(ExampleGroup) - - # Configuration for the spec. - @config : Config? - - # Creates a new spec builder. - # A root group is pushed onto the group stack. - def initialize - root_group = ExampleGroup.new - @group_stack = Deque(ExampleGroup).new - @group_stack.push(root_group) - end - - # Constructs the test spec. - # Returns the spec instance. - # - # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. - # This would indicate a logical error somewhere in Spectator or an extension of it. - def build : Spec - raise "Mismatched start and end groups" unless root? - - Spec.new(root_group, config) - end - - # Defines a new example group and pushes it onto the group stack. - # Examples and groups defined after calling this method will be nested under the new group. - # The group will be finished and popped off the stack when `#end_example` is called. - # - # The *name* is the name or brief description of the group. - # This should be a symbol when describing a type - the type name is represented as a symbol. - # Otherwise, a string should be used. - # - # The *location* optionally defined where the group originates in source code. - # - # A set of *metadata* can be used for filtering and modifying example behavior. - # For instance, adding a "pending" tag will mark tests as pending and skip execution. - # - # The newly created group is returned. - # It shouldn't be used outside of this class until a matching `#end_group` is called. - def start_group(name, location = nil, metadata = Metadata.new) : ExampleGroup - Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{metadata}" } - ExampleGroup.new(name, location, current_group, metadata).tap do |group| - @group_stack << group - end - end - - # Defines a new iterative example group and pushes it onto the group stack. - # Examples and groups defined after calling this method will be nested under the new group. - # The group will be finished and popped off the stack when `#end_example` is called. - # - # The *collection* is the set of items to iterate over. - # Child nodes in this group will be executed once for every item in the collection. - # - # The *location* optionally defined where the group originates in source code. - # - # A set of *metadata* can be used for filtering and modifying example behavior. - # For instance, adding a "pending" tag will mark tests as pending and skip execution. - # - # The newly created group is returned. - # It shouldn't be used outside of this class until a matching `#end_group` is called. - def start_iterative_group(collection, location = nil, metadata = Metadata.new) : ExampleGroup - Log.trace { "Start iterative group: #{typeof(collection)} @ #{location}; metadata: #{metadata}" } - IterativeExampleGroup.new(collection, location, current_group, metadata).tap do |group| - @group_stack << group - end - end - - # Completes a previously defined example group and pops it off the group stack. - # Be sure to call `#start_group` and `#end_group` symmetically. - # - # The completed group will be returned. - # At this point, it is safe to use the group. - # All of its examples and sub-groups have been populated. - def end_group : ExampleGroup - Log.trace { "End group: #{current_group}" } - raise "Can't pop root group" if root? - - @group_stack.pop - end - - # Defines a new example. - # The example is added to the group currently on the top of the stack. - # - # The *name* is the name or brief description of the example. - # This should be a string or nil. - # When nil, the example's name will be populated by the first expectation run inside of the test code. - # - # The *location* optionally defined where the example originates in source code. - # - # The *context_builder* is a proc that creates a context the test code should run in. - # See `Context` for more information. - # - # A set of *metadata* can be used for filtering and modifying example behavior. - # For instance, adding a "pending" tag will mark the test as pending and skip execution. - # - # A block must be provided. - # It will be yielded two arguments - the example created by this method, and the *context* argument. - # The return value of the block is ignored. - # It is expected that the test code runs when the block is called. - def add_example(name, location, context_builder, metadata = Metadata.new, &block : Example -> _) - Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" } - current_group.create_child do |group| - context = context_builder.call - Example.new(context, block, name, location, group, metadata) - end - end - - # Defines a new pending example. - # The example is added to the group currently on the top of the stack. - # - # The *name* is the name or brief description of the example. - # This should be a string or nil. - # When nil, the example's name will be an anonymous example reference. - # - # The *location* optionally defined where the example originates in source code. - # - # A set of *metadata* can be used for filtering and modifying example behavior. - # For instance, adding a "pending" tag will mark the test as pending and skip execution. - # A default *reason* can be given in case the user didn't provide one. - # - # The newly created example is returned. - def add_pending_example(name, location, metadata = Metadata.new, reason = nil) : Example - Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" } - current_group.create_child do |group| - Example.pending(name, location, group, metadata, reason) - end - end - - # Attaches a hook to be invoked before any and all examples in the current group. - def before_all(hook) - Log.trace { "Add before_all hook #{hook}" } - current_group.add_before_all_hook(hook) - 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 - - # Attaches a hook to be invoked before every example in the current group. - # The current example is provided as a block argument. - def before_each(hook) - Log.trace { "Add before_each hook #{hook}" } - current_group.add_before_each_hook(hook) - 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 - - # Attaches a hook to be invoked after any and all examples in the current group. - def after_all(hook) - Log.trace { "Add after_all hook #{hook}" } - current_group.add_after_all_hook(hook) - 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 - - # Attaches a hook to be invoked after every example in the current group. - # The current example is provided as a block argument. - def after_each(hook) - Log.trace { "Add after_each hook #{hook}" } - current_group.add_after_each_hook(hook) - 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 - - # Attaches a hook to be invoked around every example in the current group. - # The current example in procsy form is provided as a block argument. - def around_each(hook) - Log.trace { "Add around_each hook #{hook}" } - current_group.add_around_each_hook(hook) - end - - # Defines a block of code to execute around every example in the current group. - # The current example in procsy form is provided as a block argument. - def around_each(&block : Example -> _) - Log.trace { "Add around_each hook" } - current_group.around_each(&block) - end - - # Builds the configuration to use for the spec. - # A `Config::Builder` is yielded to the block provided to this method. - # That builder will be used to create the configuration. - def configure(& : Config::Builder -> _) : Nil - builder = Config::Builder.new - yield builder - @config = builder.build - end - - # Sets the configuration of the spec. - # This configuration controls how examples run. - def config=(config) - @config = config - end - - # Checks if the current group is the root group. - private def root? - @group_stack.size == 1 - end - - # Retrieves the root group. - private def root_group - @group_stack.first - end - - # Retrieves the current group, which is at the top of the stack. - # This is the group that new examples should be added to. - private def current_group - @group_stack.last - end - - # Retrieves the configuration. - # If one wasn't previously set, a default configuration is used. - private def config : Config - @config || Config.default - end - end - end -end diff --git a/src/spectator/spec/events.cr b/src/spectator/spec/events.cr deleted file mode 100644 index d6ddb8f..0000000 --- a/src/spectator/spec/events.cr +++ /dev/null @@ -1,92 +0,0 @@ -module Spectator - class Spec - # Mix-in for announcing events from a `Runner`. - # All events invoke their corresponding method on the formatter. - module Events - # Triggers the 'start' event. - # See `Formatting::Formatter#start` - private def start - notification = Formatting::StartNotification.new(example_count) - formatter.start(notification) - end - - # Triggers the 'example started' event. - # Must be passed the *example* about to run. - # See `Formatting::Formatter#example_started` - private def example_started(example) - notification = Formatting::ExampleNotification.new(example) - formatter.example_started(notification) - end - - # Triggers the 'example started' event. - # Also triggers the example result event corresponding to the example's outcome. - # Must be passed the completed *example*. - # See `Formatting::Formatter#example_finished` - private def example_finished(example) - notification = Formatting::ExampleNotification.new(example) - visitor = ResultVisitor.new(formatter, notification) - formatter.example_finished(notification) - example.result.accept(visitor) - end - - # Triggers the 'stop' event. - # See `Formatting::Formatter#stop` - private def stop - formatter.stop - end - - # Triggers the 'dump' events. - private def summarize(report, profile) - formatter.start_dump - - notification = Formatting::ExampleSummaryNotification.new(report.pending) - formatter.dump_pending(notification) - - notification = Formatting::ExampleSummaryNotification.new(report.failures) - formatter.dump_failures(notification) - - if profile - notification = Formatting::ProfileNotification.new(profile) - formatter.dump_profile(notification) - end - - notification = Formatting::SummaryNotification.new(report) - formatter.dump_summary(notification) - end - - # Triggers the 'close' event. - # See `Formatting::Formatter#close` - private def close - formatter.close - end - - # Provides methods for the various result types. - private struct ResultVisitor - # Creates the visitor. - # Requires the *formatter* to notify and the *notification* to send it. - def initialize(@formatter : Formatting::Formatter, @notification : Formatting::ExampleNotification) - end - - # Invokes the example passed method. - def pass(_result) - @formatter.example_passed(@notification) - end - - # Invokes the example failed method. - def fail(_result) - @formatter.example_failed(@notification) - end - - # Invokes the example error method. - def error(_result) - @formatter.example_error(@notification) - end - - # Invokes the example pending method. - def pending(_result) - @formatter.example_pending(@notification) - end - end - end - end -end diff --git a/src/spectator/spec/runner.cr b/src/spectator/spec/runner.cr deleted file mode 100644 index 81e86c2..0000000 --- a/src/spectator/spec/runner.cr +++ /dev/null @@ -1,98 +0,0 @@ -require "../example" -require "../report" -require "../run_flags" -require "./events" - -module Spectator - class Spec - # Logic for executing examples and collecting results. - private struct Runner - include Events - - # Formatter to send events to. - private getter formatter : Formatting::Formatter - - # Creates the runner. - # The collection of *examples* should be pre-filtered and shuffled. - # This runner will run each example in the order provided. - # The *formatter* will be called for various events. - def initialize(@examples : Array(Example), @formatter : Formatting::Formatter, - @run_flags = RunFlags::None, @random_seed : UInt64? = nil) - end - - # Runs the spec. - # This will run the provided examples - # and invoke the reporters to communicate results. - # True will be returned if the spec ran successfully, - # or false if there was at least one failure. - def run : Bool - start - elapsed = Time.measure { run_examples } - stop - - report = Report.generate(@examples, elapsed, @random_seed) - profile = Profile.generate(@examples) if @run_flags.profile? && report.counts.run > 0 - summarize(report, profile) - - report.counts.fail.zero? - ensure - close - end - - # Attempts to run all examples. - # Returns a list of examples that ran. - private def run_examples - @examples.each do |example| - result = run_example(example) - - # Bail out if the example failed - # and configured to stop after the first failure. - break fail_fast if fail_fast? && result.fail? - end - end - - # Runs a single example and returns the result. - # The formatter is given the example and result information. - private def run_example(example) - example_started(example) - result = if dry_run? - # TODO: Pending examples return a pending result instead of pass in RSpec dry-run. - dry_run_result - else - example.run - end - example_finished(example) - result - end - - # Creates a fake result. - private def dry_run_result - expectations = [] of Expectation - PassResult.new(Time::Span.zero, expectations) - end - - # Generates and returns a profile if one should be displayed. - private def profile(report) - Profile.generate(report) if @config.profile? - end - - # Indicates whether examples should be simulated, but not run. - private def dry_run? - @run_flags.dry_run? - end - - # Indicates whether test execution should stop after the first failure. - private def fail_fast? - @run_flags.fail_fast? - end - - private def fail_fast : Nil - end - - # Number of examples configured to run. - private def example_count - @examples.size - end - end - end -end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr new file mode 100644 index 0000000..76797e2 --- /dev/null +++ b/src/spectator/spec_builder.cr @@ -0,0 +1,253 @@ +require "./config" +require "./example" +require "./example_context_method" +require "./example_group" +require "./iterative_example_group" +require "./spec" +require "./metadata" + +module Spectator + # Progressively builds a test spec. + # + # A stack is used to track the current example group. + # Adding an example or group will nest it under the group at the top of the stack. + class SpecBuilder + Log = ::Spectator::Log.for(self) + + # Stack tracking the current group. + # The bottom of the stack (first element) is the root group. + # The root group should never be removed. + # The top of the stack (last element) is the current group. + # New examples should be added to the current group. + @group_stack : Deque(ExampleGroup) + + # Configuration for the spec. + @config : Config? + + # Creates a new spec builder. + # A root group is pushed onto the group stack. + def initialize + root_group = ExampleGroup.new + @group_stack = Deque(ExampleGroup).new + @group_stack.push(root_group) + end + + # Constructs the test spec. + # Returns the spec instance. + # + # Raises an error if there were not symmetrical calls to `#start_group` and `#end_group`. + # This would indicate a logical error somewhere in Spectator or an extension of it. + def build : Spec + raise "Mismatched start and end groups" unless root? + + Spec.new(root_group, config) + end + + # Defines a new example group and pushes it onto the group stack. + # Examples and groups defined after calling this method will be nested under the new group. + # The group will be finished and popped off the stack when `#end_example` is called. + # + # The *name* is the name or brief description of the group. + # This should be a symbol when describing a type - the type name is represented as a symbol. + # Otherwise, a string should be used. + # + # The *location* optionally defined where the group originates in source code. + # + # A set of *metadata* can be used for filtering and modifying example behavior. + # For instance, adding a "pending" tag will mark tests as pending and skip execution. + # + # The newly created group is returned. + # It shouldn't be used outside of this class until a matching `#end_group` is called. + def start_group(name, location = nil, metadata = Metadata.new) : ExampleGroup + Log.trace { "Start group: #{name.inspect} @ #{location}; metadata: #{metadata}" } + ExampleGroup.new(name, location, current_group, metadata).tap do |group| + @group_stack << group + end + end + + # Defines a new iterative example group and pushes it onto the group stack. + # Examples and groups defined after calling this method will be nested under the new group. + # The group will be finished and popped off the stack when `#end_example` is called. + # + # The *collection* is the set of items to iterate over. + # Child nodes in this group will be executed once for every item in the collection. + # + # The *location* optionally defined where the group originates in source code. + # + # A set of *metadata* can be used for filtering and modifying example behavior. + # For instance, adding a "pending" tag will mark tests as pending and skip execution. + # + # The newly created group is returned. + # It shouldn't be used outside of this class until a matching `#end_group` is called. + def start_iterative_group(collection, location = nil, metadata = Metadata.new) : ExampleGroup + Log.trace { "Start iterative group: #{typeof(collection)} @ #{location}; metadata: #{metadata}" } + IterativeExampleGroup.new(collection, location, current_group, metadata).tap do |group| + @group_stack << group + end + end + + # Completes a previously defined example group and pops it off the group stack. + # Be sure to call `#start_group` and `#end_group` symmetically. + # + # The completed group will be returned. + # At this point, it is safe to use the group. + # All of its examples and sub-groups have been populated. + def end_group : ExampleGroup + Log.trace { "End group: #{current_group}" } + raise "Can't pop root group" if root? + + @group_stack.pop + end + + # Defines a new example. + # The example is added to the group currently on the top of the stack. + # + # The *name* is the name or brief description of the example. + # This should be a string or nil. + # When nil, the example's name will be populated by the first expectation run inside of the test code. + # + # The *location* optionally defined where the example originates in source code. + # + # The *context_builder* is a proc that creates a context the test code should run in. + # See `Context` for more information. + # + # A set of *metadata* can be used for filtering and modifying example behavior. + # For instance, adding a "pending" tag will mark the test as pending and skip execution. + # + # A block must be provided. + # It will be yielded two arguments - the example created by this method, and the *context* argument. + # The return value of the block is ignored. + # It is expected that the test code runs when the block is called. + def add_example(name, location, context_builder, metadata = Metadata.new, &block : Example -> _) + Log.trace { "Add example: #{name} @ #{location}; metadata: #{metadata}" } + current_group.create_child do |group| + context = context_builder.call + Example.new(context, block, name, location, group, metadata) + end + end + + # Defines a new pending example. + # The example is added to the group currently on the top of the stack. + # + # The *name* is the name or brief description of the example. + # This should be a string or nil. + # When nil, the example's name will be an anonymous example reference. + # + # The *location* optionally defined where the example originates in source code. + # + # A set of *metadata* can be used for filtering and modifying example behavior. + # For instance, adding a "pending" tag will mark the test as pending and skip execution. + # A default *reason* can be given in case the user didn't provide one. + # + # The newly created example is returned. + def add_pending_example(name, location, metadata = Metadata.new, reason = nil) : Example + Log.trace { "Add pending example: #{name} @ #{location}; metadata: #{metadata}" } + current_group.create_child do |group| + Example.pending(name, location, group, metadata, reason) + end + end + + # Attaches a hook to be invoked before any and all examples in the current group. + def before_all(hook) + Log.trace { "Add before_all hook #{hook}" } + current_group.add_before_all_hook(hook) + 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 + + # Attaches a hook to be invoked before every example in the current group. + # The current example is provided as a block argument. + def before_each(hook) + Log.trace { "Add before_each hook #{hook}" } + current_group.add_before_each_hook(hook) + 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 + + # Attaches a hook to be invoked after any and all examples in the current group. + def after_all(hook) + Log.trace { "Add after_all hook #{hook}" } + current_group.add_after_all_hook(hook) + 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 + + # Attaches a hook to be invoked after every example in the current group. + # The current example is provided as a block argument. + def after_each(hook) + Log.trace { "Add after_each hook #{hook}" } + current_group.add_after_each_hook(hook) + 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 + + # Attaches a hook to be invoked around every example in the current group. + # The current example in procsy form is provided as a block argument. + def around_each(hook) + Log.trace { "Add around_each hook #{hook}" } + current_group.add_around_each_hook(hook) + end + + # Defines a block of code to execute around every example in the current group. + # The current example in procsy form is provided as a block argument. + def around_each(&block : Example -> _) + Log.trace { "Add around_each hook" } + current_group.around_each(&block) + end + + # Builds the configuration to use for the spec. + # A `Config::Builder` is yielded to the block provided to this method. + # That builder will be used to create the configuration. + def configure(& : Config::Builder -> _) : Nil + builder = Config::Builder.new + yield builder + @config = builder.build + end + + # Sets the configuration of the spec. + # This configuration controls how examples run. + def config=(config) + @config = config + end + + # Checks if the current group is the root group. + private def root? + @group_stack.size == 1 + end + + # Retrieves the root group. + private def root_group + @group_stack.first + end + + # Retrieves the current group, which is at the top of the stack. + # This is the group that new examples should be added to. + private def current_group + @group_stack.last + end + + # Retrieves the configuration. + # If one wasn't previously set, a default configuration is used. + private def config : Config + @config || Config.default + end + end +end