Remove Spec namespace

This commit is contained in:
Michael Miller 2021-07-05 11:49:16 -06:00
parent 6a01ab3531
commit 3e4079d408
No known key found for this signature in database
GPG key ID: FB9F12F7C646A4AD
9 changed files with 450 additions and 448 deletions

View file

@ -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.

View file

@ -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"

98
src/spectator/runner.cr Normal file
View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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