mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Remove Spec namespace
This commit is contained in:
parent
6a01ab3531
commit
3e4079d408
9 changed files with 450 additions and 448 deletions
|
@ -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.
|
||||
|
|
|
@ -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
98
src/spectator/runner.cr
Normal 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
|
93
src/spectator/runner_events.cr
Normal file
93
src/spectator/runner_events.cr
Normal 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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
253
src/spectator/spec_builder.cr
Normal file
253
src/spectator/spec_builder.cr
Normal 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
|
Loading…
Reference in a new issue