1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
|
require 'riot/context_options'
require 'riot/context_helpers'
module Riot
RootContext = Struct.new(:setups, :teardowns, :detailed_description, :option_set)
# Defines the classes {Riot::Context} will use when creating new assertions and situations.
module ContextClassOverrides
# Returns the default class used for generating new {Riot::Assertion Assertion} instances. Defaults to
# {Riot::Assertion}.
#
# @return [Class]
def assertion_class; Assertion; end
# Returns the default class used for generating new {Riot::Situation Situation} instances. Defaults to
# {Riot::Situation}.
#
# @return [Class]
def situation_class; Situation; end
end # ContextClassOverrides
# An {Riot::Assertion} is declared within a Context. The context stores setup and teardown
# blocks, and allows for nesting and refactoring. Extension developers may also configure
# {Riot::ContextMiddleware Middleware} objects in order to extend the functionality of a Context.
class Context
include ContextClassOverrides
include ContextOptions
include ContextHelpers
# The set of middleware helpers configured for the current test space.
#
# @return [Array<Riot::ContextMiddleware>]
def self.middlewares; @middlewares ||= []; end
# The partial description of just this context.
#
# @return [String]
attr_reader :description
# The parent context.
#
# @return [Riot::Context, nil] a context or nil
attr_reader :parent
# Creates a new Context
#
# @param [String] description a partial description of this context
# @param [Riot::Context, nil] parent a parent context or nothing
# @param [lambda] definition the body of this context
def initialize(description, parent=nil, &definition)
@parent = parent || RootContext.new([],[], "", {})
@description = description
@contexts, @setups, @assertions, @teardowns = [], [], [], []
@context_error = nil
@options = @parent.option_set.dup
prepare_middleware(&definition)
rescue Exception => e
@context_error = e
end
# Create a new test context.
#
# @param [String] description
# @return [Riot::Context] the newly created context
def context(description, &definition)
new_context(description, self.class, &definition)
end
alias_method :describe, :context
# @private
# Returns an ordered list of the setup blocks for the context.
#
# @return [Array<Riot::RunnableBlock>]
def setups
@parent.setups + @setups
end
# @private
# Returns an ordered list of the teardown blocks for the context.
#
# @return [Array<Riot::RunnableBlock>]
def teardowns
@parent.teardowns + @teardowns
end
# Executes the setups, hookups, assertions, and teardowns and passes results on to a given
# {Riot::Reporter Reporter}. Sub-contexts will also be executed and provided the given reporter. A new
# {Riot::Situation Situation} will be created from the specified {#situation_class Situation class}.
#
# @param [Riot::Reporter] reporter the reporter to report results to
# @return [Riot::Reporter] the given reporter
def run(reporter)
reporter.describe_context(self) unless @assertions.empty?
if @context_error
reporter.report("context preparation", [:context_error, @context_error])
else
local_run(reporter, situation_class.new)
run_sub_contexts(reporter)
end
reporter
end
# @private
# Used mostly for testing purposes; this method does the actual running of just this context.
# @param [Riot::Reporter] reporter the reporter to report results to
# @param [Riot::Situation] situation the situation to use for executing the context.
def local_run(reporter, situation)
runnables.each do |runnable|
code, response = *runnable.run(situation)
reporter.report(runnable.to_s, [code, response])
break if code == :setup_error
end
end
# Prints the full description from the context tree, grabbing the description from the parent and
# appending the description given to this context.
#
# @return [String] the full description for this context
def detailed_description
"#{parent.detailed_description} #{description}".strip
end
private
# Iterative over the registered middlewares and let them configure this context instance if they so
# choose. {Riot::AllImportantMiddleware} will always be the last in the chain.
def prepare_middleware(&context_definition)
last_middleware = AllImportantMiddleware.new(&context_definition)
Context.middlewares.inject(last_middleware) do |previous_middleware, middleware|
middleware.new(previous_middleware)
end.call(self)
end
# The collection of things that are {Riot::RunnableBlock} instances in this context.
#
# @return [Array<Riot::RunnableBlock>]
def runnables
setups + @assertions + teardowns
end
# Execute each sub context.
#
# @param [Riot::Reporter] reporter the reporter instance to use
# @return [nil]
def run_sub_contexts(reporter)
@contexts.each { |ctx| ctx.run(reporter) }
end
# Creates a new context instance and appends it to the set of immediate children sub-contexts.
#
# @param [String] description a partial description
# @param [Class] klass the context class that a sub-context will be generated from
# @param [lambda] definition the body of the sub-context
# @return [#run] something that hopefully responds to run and is context-like
def new_context(description, klass, &definition)
(@contexts << klass.new(description, self, &definition)).last
end
end # Context
end # Riot
|