xspec 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +141 -0
- data/bin/xspec +24 -0
- data/lib/xspec.rb +61 -0
- data/lib/xspec/assertion_contexts.rb +360 -0
- data/lib/xspec/autorun.rb +8 -0
- data/lib/xspec/data_structures.rb +181 -0
- data/lib/xspec/defaults.rb +30 -0
- data/lib/xspec/dsl.rb +26 -0
- data/lib/xspec/evaluators.rb +40 -0
- data/lib/xspec/notifiers.rb +310 -0
- data/spec/all_specs.rb +9 -0
- data/spec/integration/rspec_expectations_spec.rb +21 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/unit/assertion_spec.rb +67 -0
- data/spec/unit/doubles_spec.rb +199 -0
- data/spec/unit/let_spec.rb +46 -0
- data/spec/unit/notifiers_spec.rb +189 -0
- data/spec/unit/skeleton_spec.rb +7 -0
- data/xspec.gemspec +27 -0
- metadata +73 -0
@@ -0,0 +1,181 @@
|
|
1
|
+
# # Data Structures
|
2
|
+
|
3
|
+
# XSpec data structures are very dumb. They:
|
4
|
+
#
|
5
|
+
# * Only contain iteration and creation logic.
|
6
|
+
# * Do not store recursive references ("everything flows downhill").
|
7
|
+
module XSpec
|
8
|
+
# A unit of work, usually created by the `it` DSL method, is a labeled,
|
9
|
+
# indivisible code block that expresses an assertion about a property of the
|
10
|
+
# system under test. They are run by an evaluator.
|
11
|
+
UnitOfWork = Struct.new(:name, :block)
|
12
|
+
|
13
|
+
|
14
|
+
# A context is a recursively nested structure, usually created with the
|
15
|
+
# `describe` DSL method, that contains other contexts and units of work. Most
|
16
|
+
# of the logic for a context happens at the class level rather than instance,
|
17
|
+
# which is unusual but required for method inheritance to work correctly. It
|
18
|
+
# currently violates the logic rule specified above, more work is required to
|
19
|
+
# decouple it.
|
20
|
+
require 'xspec/dsl'
|
21
|
+
class Context
|
22
|
+
class << self
|
23
|
+
attr_reader :name, :children, :units_of_work, :assertion_context
|
24
|
+
|
25
|
+
# A context includes the same DSL methods as the root level module, which
|
26
|
+
# enables the recursive creation.
|
27
|
+
def __xspec_context; self; end
|
28
|
+
include ::XSpec::DSL
|
29
|
+
|
30
|
+
# Each nested context creates a new class that inherits from the parent.
|
31
|
+
# Methods can be added to this class as per normal, and are correctly
|
32
|
+
# inherited by children. When it comes time to run tests, the evaluator
|
33
|
+
# will create a new instance of the context (a class) for each test,
|
34
|
+
# making the defined methods available and also ensuring that there is no
|
35
|
+
# state pollution between tests.
|
36
|
+
def make(name, assertion_context, &block)
|
37
|
+
x = Class.new(self)
|
38
|
+
x.initialize!(name, assertion_context)
|
39
|
+
x.class_eval(&block) if block
|
40
|
+
x.apply_assertion_context!
|
41
|
+
x
|
42
|
+
end
|
43
|
+
|
44
|
+
# A class cannot have an implicit initializer, but some variable
|
45
|
+
# inititialization is required so the `initialize!` method is called
|
46
|
+
# explicitly when ever a dynamic subclass is created.
|
47
|
+
def initialize!(name, assertion_context)
|
48
|
+
@children = []
|
49
|
+
@units_of_work = []
|
50
|
+
@name = name
|
51
|
+
@assertion_context = assertion_context
|
52
|
+
end
|
53
|
+
|
54
|
+
# The assertion context should be applied after the user has had a chance
|
55
|
+
# to add their own methods. It needs to be last so that users can't
|
56
|
+
# clobber the assertion methods.
|
57
|
+
def apply_assertion_context!
|
58
|
+
include(assertion_context)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Executing a unit of work creates a new instance and hands it off to the
|
62
|
+
# `call` method, which is defined by whichever assertion context is being
|
63
|
+
# used. By creating a new instance everytime, no state is preserved
|
64
|
+
# between executions.
|
65
|
+
def execute(unit_of_work)
|
66
|
+
new.call(unit_of_work)
|
67
|
+
end
|
68
|
+
|
69
|
+
# The root context is nothing special, and behaves the same as all the
|
70
|
+
# others.
|
71
|
+
def root(assertion_context)
|
72
|
+
make(nil, assertion_context)
|
73
|
+
end
|
74
|
+
|
75
|
+
# Child contexts and units of work are typically added by the `describe`
|
76
|
+
# and `it` DSL methods respectively.
|
77
|
+
def add_child_context(name = nil, opts = {}, &block)
|
78
|
+
self.children << make(name, assertion_context, &block)
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_unit_of_work(name = nil, opts = {}, &block)
|
82
|
+
self.units_of_work << UnitOfWork.new(name, block)
|
83
|
+
end
|
84
|
+
|
85
|
+
# A shared context is a floating context that isn't part of any context
|
86
|
+
# heirachy, so its units of work will not be visible to the root node. It
|
87
|
+
# can be brought into any point in the heirachy using `copy_into_tree`
|
88
|
+
# (aliased as `it_behaves_like_a` in the DSL), and this can be done
|
89
|
+
# multiple times, which allows definitions to be reused.
|
90
|
+
#
|
91
|
+
# This is leaky abstraction, since only units of work are copied from
|
92
|
+
# shared contexts. Methods and child contexts are ignored.
|
93
|
+
def create_shared_context(&block)
|
94
|
+
make(nil, assertion_context, &block)
|
95
|
+
end
|
96
|
+
|
97
|
+
def copy_into_tree(source_context)
|
98
|
+
target_context = make(
|
99
|
+
source_context.name,
|
100
|
+
source_context.assertion_context
|
101
|
+
)
|
102
|
+
source_context.nested_units_of_work.each do |x|
|
103
|
+
target_context.units_of_work << x.unit_of_work
|
104
|
+
end
|
105
|
+
self.children << target_context
|
106
|
+
target_context
|
107
|
+
end
|
108
|
+
|
109
|
+
# The most convenient way to access all units of work is this recursive
|
110
|
+
# iteration that returns all leaf-nodes as `NestedUnitOfWork` objects.
|
111
|
+
require 'enumerator'
|
112
|
+
def nested_units_of_work(&block)
|
113
|
+
enum = Enumerator.new do |y|
|
114
|
+
children.each do |child|
|
115
|
+
child.nested_units_of_work do |x|
|
116
|
+
y.yield x.nest_under(self)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
units_of_work.each do |x|
|
121
|
+
y.yield NestedUnitOfWork.new([self], x)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
if block
|
126
|
+
enum.each(&block)
|
127
|
+
else
|
128
|
+
enum
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Values of memoized methods are remembered only for the duration of a
|
133
|
+
# single unit of work. These are typically created using the `let` DSL
|
134
|
+
# method.
|
135
|
+
def add_memoized_method(name, &block)
|
136
|
+
define_method(name) do
|
137
|
+
memoized[block] ||= instance_eval(&block)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Dynamically generated classes are hard to identify in object graphs, so
|
142
|
+
# it is helpful for debugging to set an explicit name.
|
143
|
+
def to_s
|
144
|
+
"Context:'#{name}'"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def memoized
|
149
|
+
@memoized ||= {}
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Units of work can be nested inside contexts. This is the main object that
|
154
|
+
# other components of the system work with.
|
155
|
+
NestedUnitOfWork = Struct.new(:parents, :unit_of_work) do
|
156
|
+
def block; unit_of_work.block; end
|
157
|
+
def name; unit_of_work.name; end
|
158
|
+
|
159
|
+
def immediate_parent
|
160
|
+
parents.last
|
161
|
+
end
|
162
|
+
|
163
|
+
def nest_under(parent)
|
164
|
+
self.class.new([parent] + parents, unit_of_work)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# The result of executing a unit of work, including timing information. This
|
169
|
+
# is passed to notifiers for display or other processing.
|
170
|
+
ExecutedUnitOfWork = Struct.new(:nested_unit_of_work, :errors, :duration) do
|
171
|
+
def name; nested_unit_of_work.name end
|
172
|
+
def parents; nested_unit_of_work.parents end
|
173
|
+
end
|
174
|
+
|
175
|
+
# A test failure will be reported as a `Failure`, which includes contextual
|
176
|
+
# information about the failure useful for reporting to the user.
|
177
|
+
Failure = Struct.new(:unit_of_work, :message, :caller)
|
178
|
+
|
179
|
+
# An exception is mostly handled the same way as a failure.
|
180
|
+
CodeException = Class.new(Failure)
|
181
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# # Defaults
|
2
|
+
|
3
|
+
# These are the defaults used by `XSpec.dsl`, but feel free to specify your own
|
4
|
+
# instead. They are set up in such a way that if you can override a component
|
5
|
+
# down in the bowels without having to provide an entire top level evaluator.
|
6
|
+
require 'xspec/evaluators'
|
7
|
+
require 'xspec/assertion_contexts'
|
8
|
+
require 'xspec/notifiers'
|
9
|
+
|
10
|
+
module XSpec
|
11
|
+
def add_defaults(options = {})
|
12
|
+
# A notifier makes it possible to observe the state of the system, be that
|
13
|
+
# progress or details of failing tests.
|
14
|
+
options[:notifier] ||= XSpec::Notifier::DEFAULT
|
15
|
+
|
16
|
+
# A unit of work will run as an instance method on the context it is
|
17
|
+
# defined in, but in addition an assertion context will be added as well.
|
18
|
+
# This is a module that is included as the final step in constructing a
|
19
|
+
# context. Allows for different matchers and expectation frameworks to be
|
20
|
+
# used.
|
21
|
+
options[:assertion_context] ||= AssertionContext::DEFAULT
|
22
|
+
|
23
|
+
# An evaluator is responsible for scheduling units of work and handing them
|
24
|
+
# off to the assertion context. Any logic regarding threads, remote
|
25
|
+
# execution or the like belongs in an evaluator.
|
26
|
+
options[:evaluator] ||= Evaluator::DEFAULT.new(options)
|
27
|
+
options
|
28
|
+
end
|
29
|
+
module_function :add_defaults
|
30
|
+
end
|
data/lib/xspec/dsl.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Common DSL functions are provided as a module so that they can be used in
|
2
|
+
# both top-level and nested contexts. The method names are modeled after
|
3
|
+
# rspec, and should behave roughly the same.
|
4
|
+
module XSpec
|
5
|
+
module DSL
|
6
|
+
def it(*args, &block)
|
7
|
+
__xspec_context.add_unit_of_work(*args, &block)
|
8
|
+
end
|
9
|
+
|
10
|
+
def describe(*args, &block)
|
11
|
+
__xspec_context.add_child_context(*args, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def let(*args, &block)
|
15
|
+
__xspec_context.add_memoized_method(*args, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def shared_context(*args, &block)
|
19
|
+
__xspec_context.create_shared_context(*args, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def it_behaves_like_a(context)
|
23
|
+
__xspec_context.copy_into_tree(context)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# # Evaluators
|
2
|
+
|
3
|
+
# Evaluators are responsible for collecting all units of work to be run and
|
4
|
+
# scheduling them.
|
5
|
+
module XSpec
|
6
|
+
module Evaluator
|
7
|
+
# The serial evaluator, unsurprisingly, runs all units of works serially in
|
8
|
+
# a loop. It is about as simple an evaluator as you can imagine. Parents
|
9
|
+
# are responsible for actually executing the work.
|
10
|
+
class Serial
|
11
|
+
def initialize(opts)
|
12
|
+
@notifier = opts.fetch(:notifier)
|
13
|
+
@clock = opts.fetch(:clock, ->{ Time.now.to_f })
|
14
|
+
end
|
15
|
+
|
16
|
+
def run(context)
|
17
|
+
notifier.run_start
|
18
|
+
|
19
|
+
context.nested_units_of_work.each do |x|
|
20
|
+
notifier.evaluate_start(x)
|
21
|
+
|
22
|
+
start_time = clock.()
|
23
|
+
errors = x.immediate_parent.execute(x)
|
24
|
+
finish_time = clock.()
|
25
|
+
|
26
|
+
result = ExecutedUnitOfWork.new(x, errors, finish_time - start_time)
|
27
|
+
notifier.evaluate_finish(result)
|
28
|
+
end
|
29
|
+
|
30
|
+
notifier.run_finish
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
attr_reader :notifier, :clock
|
36
|
+
end
|
37
|
+
|
38
|
+
DEFAULT = Serial
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,310 @@
|
|
1
|
+
# # Notifiers
|
2
|
+
|
3
|
+
# Without a notifier, there is no way for XSpec to interact with the outside
|
4
|
+
# world. A notifier handles progress updates as tests are executed, and
|
5
|
+
# summarizing the run when it finished.
|
6
|
+
module XSpec
|
7
|
+
module Notifier
|
8
|
+
# Many notifiers play nice with others, and can be composed together in a
|
9
|
+
# way that each notifier will have its callback run in turn.
|
10
|
+
module Composable
|
11
|
+
def +(other)
|
12
|
+
Composite.new(self, other)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Composite
|
17
|
+
include Composable
|
18
|
+
|
19
|
+
def initialize(*notifiers)
|
20
|
+
@notifiers = notifiers
|
21
|
+
end
|
22
|
+
|
23
|
+
def run_start
|
24
|
+
notifiers.each(&:run_start)
|
25
|
+
end
|
26
|
+
|
27
|
+
def evaluate_start(*args)
|
28
|
+
notifiers.each {|x| x.evaluate_start(*args) }
|
29
|
+
end
|
30
|
+
|
31
|
+
def evaluate_finish(*args)
|
32
|
+
notifiers.each {|x| x.evaluate_finish(*args) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_finish
|
36
|
+
notifiers.map(&:run_finish).all?
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
attr_reader :notifiers
|
42
|
+
end
|
43
|
+
|
44
|
+
# Outputs a single character for each executed unit of work representing
|
45
|
+
# the result.
|
46
|
+
class Character
|
47
|
+
include Composable
|
48
|
+
|
49
|
+
def initialize(out = $stdout)
|
50
|
+
@out = out
|
51
|
+
end
|
52
|
+
|
53
|
+
def run_start; end
|
54
|
+
|
55
|
+
def evaluate_start(*_); end
|
56
|
+
|
57
|
+
def evaluate_finish(result)
|
58
|
+
@out.print label_for_failure(result.errors[0])
|
59
|
+
@failed ||= result.errors.any?
|
60
|
+
end
|
61
|
+
|
62
|
+
def run_finish
|
63
|
+
@out.puts
|
64
|
+
!@failed
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
|
69
|
+
def label_for_failure(f)
|
70
|
+
case f
|
71
|
+
when CodeException then 'E'
|
72
|
+
when Failure then 'F'
|
73
|
+
else '.'
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
# Renders a histogram of test durations after the entire run is complete.
|
80
|
+
class TimingsAtEnd
|
81
|
+
include Composable
|
82
|
+
|
83
|
+
DEFAULT_SPLITS = [0.001, 0.005, 0.01, 0.1, 1.0, Float::INFINITY]
|
84
|
+
|
85
|
+
def initialize(out: $stdout,
|
86
|
+
splits: DEFAULT_SPLITS,
|
87
|
+
width: 20)
|
88
|
+
|
89
|
+
@timings = {}
|
90
|
+
@splits = splits
|
91
|
+
@width = width
|
92
|
+
@out = out
|
93
|
+
end
|
94
|
+
|
95
|
+
def run_start(*_); end
|
96
|
+
|
97
|
+
def evaluate_start(_)
|
98
|
+
end
|
99
|
+
|
100
|
+
def evaluate_finish(result)
|
101
|
+
timings[result] = result.duration
|
102
|
+
end
|
103
|
+
|
104
|
+
def run_finish
|
105
|
+
buckets = bucket_from_splits(timings, splits)
|
106
|
+
max = buckets.values.max
|
107
|
+
|
108
|
+
out.puts " Timings:"
|
109
|
+
buckets.each do |(split, count)|
|
110
|
+
label = split.infinite? ? "∞" : split
|
111
|
+
|
112
|
+
out.puts " %6s %-#{width}s %i" % [
|
113
|
+
label,
|
114
|
+
'#' * (count / max.to_f * width.to_f).ceil,
|
115
|
+
count
|
116
|
+
]
|
117
|
+
end
|
118
|
+
out.puts
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
attr_reader :timings, :splits, :width, :out
|
124
|
+
|
125
|
+
def bucket_from_splits(timings, splits)
|
126
|
+
initial_buckets = splits.each_with_object({}) do |b, a|
|
127
|
+
a[b] = 0
|
128
|
+
end
|
129
|
+
|
130
|
+
buckets = timings.each_with_object(initial_buckets) do |(_, d), a|
|
131
|
+
split = splits.detect {|x| d < x }
|
132
|
+
a[split] += 1
|
133
|
+
end
|
134
|
+
|
135
|
+
remove_trailing_zero_counts(buckets)
|
136
|
+
end
|
137
|
+
|
138
|
+
def remove_trailing_zero_counts(buckets)
|
139
|
+
Hash[
|
140
|
+
buckets
|
141
|
+
.to_a
|
142
|
+
.reverse
|
143
|
+
.drop_while {|_, x| x == 0 }
|
144
|
+
.reverse
|
145
|
+
]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Outputs error messages and backtraces after the entire run is complete.
|
150
|
+
class FailuresAtEnd
|
151
|
+
include Composable
|
152
|
+
|
153
|
+
def initialize(out = $stdout)
|
154
|
+
@errors = []
|
155
|
+
@out = out
|
156
|
+
end
|
157
|
+
|
158
|
+
def run_start; end
|
159
|
+
|
160
|
+
def evaluate_start(*_); end
|
161
|
+
|
162
|
+
def evaluate_finish(result)
|
163
|
+
self.errors += result.errors
|
164
|
+
end
|
165
|
+
|
166
|
+
def run_finish
|
167
|
+
return true if errors.empty?
|
168
|
+
|
169
|
+
out.puts
|
170
|
+
errors.each do |error|
|
171
|
+
out.puts "%s:\n%s\n\n" % [full_name(error.unit_of_work), error.message.lines.map {|x| " #{x}"}.join("")]
|
172
|
+
clean_backtrace(error.caller).each do |line|
|
173
|
+
out.puts " %s" % line
|
174
|
+
end
|
175
|
+
out.puts
|
176
|
+
end
|
177
|
+
|
178
|
+
false
|
179
|
+
end
|
180
|
+
|
181
|
+
def full_name(unit_of_work)
|
182
|
+
(unit_of_work.parents + [unit_of_work]).map(&:name).compact.join(' ')
|
183
|
+
end
|
184
|
+
|
185
|
+
# A standard backtrace contains many entries for XSpec itself which are
|
186
|
+
# not useful for debugging your tests, so they are stripped out.
|
187
|
+
def clean_backtrace(backtrace)
|
188
|
+
lib_dir = File.dirname(File.expand_path('..', __FILE__))
|
189
|
+
|
190
|
+
backtrace.reject {|x|
|
191
|
+
File.dirname(x).start_with?(lib_dir)
|
192
|
+
}
|
193
|
+
end
|
194
|
+
|
195
|
+
protected
|
196
|
+
|
197
|
+
attr_accessor :out, :errors
|
198
|
+
end
|
199
|
+
|
200
|
+
# Includes nicely formatted names and durations of each test in the output,
|
201
|
+
# with color.
|
202
|
+
class ColoredDocumentation
|
203
|
+
require 'set'
|
204
|
+
|
205
|
+
include Composable
|
206
|
+
|
207
|
+
VT100_COLORS = {
|
208
|
+
:black => 30,
|
209
|
+
:red => 31,
|
210
|
+
:green => 32,
|
211
|
+
:yellow => 33,
|
212
|
+
:blue => 34,
|
213
|
+
:magenta => 35,
|
214
|
+
:cyan => 36,
|
215
|
+
:white => 37
|
216
|
+
}
|
217
|
+
|
218
|
+
def color_code_for(color)
|
219
|
+
VT100_COLORS.fetch(color)
|
220
|
+
end
|
221
|
+
|
222
|
+
def colorize(text, color)
|
223
|
+
"\e[#{color_code_for(color)}m#{text}\e[0m"
|
224
|
+
end
|
225
|
+
|
226
|
+
def decorate(result)
|
227
|
+
name = result.name
|
228
|
+
out = if result.errors.any?
|
229
|
+
colorize(append_failed(name), :red)
|
230
|
+
else
|
231
|
+
colorize(name , :green)
|
232
|
+
end
|
233
|
+
"%.3fs " % result.duration + out
|
234
|
+
end
|
235
|
+
|
236
|
+
def initialize(out = $stdout)
|
237
|
+
self.indent = 2
|
238
|
+
self.last_seen_names = []
|
239
|
+
self.failed = false
|
240
|
+
self.out = out
|
241
|
+
end
|
242
|
+
|
243
|
+
def run_start; end
|
244
|
+
|
245
|
+
def evaluate_start(*_); end
|
246
|
+
|
247
|
+
def evaluate_finish(result)
|
248
|
+
output_context_header! result.parents.map(&:name).compact
|
249
|
+
|
250
|
+
spaces = ' ' * (last_seen_names.size * indent)
|
251
|
+
|
252
|
+
self.failed ||= result.errors.any?
|
253
|
+
|
254
|
+
out.puts "%s%s" % [spaces, decorate(result)]
|
255
|
+
end
|
256
|
+
|
257
|
+
def run_finish
|
258
|
+
out.puts
|
259
|
+
!failed
|
260
|
+
end
|
261
|
+
|
262
|
+
protected
|
263
|
+
|
264
|
+
attr_accessor :last_seen_names, :indent, :failed, :out
|
265
|
+
|
266
|
+
def output_context_header!(parent_names)
|
267
|
+
if parent_names != last_seen_names
|
268
|
+
tail = parent_names - last_seen_names
|
269
|
+
|
270
|
+
out.puts
|
271
|
+
if tail.any?
|
272
|
+
existing_indent = parent_names.size - tail.size
|
273
|
+
tail.each_with_index do |name, i|
|
274
|
+
out.puts '%s%s' % [' ' * ((existing_indent + i) * indent), name]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
self.last_seen_names = parent_names
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def append_failed(name)
|
283
|
+
[name, "FAILED"].compact.join(' - ')
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
# Includes nicely formatted names and durations of each test in the output.
|
288
|
+
class Documentation < ColoredDocumentation
|
289
|
+
def colorize(name, _)
|
290
|
+
name
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
# A notifier that does not do anything and always returns successful.
|
295
|
+
# Useful as a parent class for other notifiers or for testing.
|
296
|
+
class Null
|
297
|
+
include Composable
|
298
|
+
|
299
|
+
def run_start; end
|
300
|
+
def evaluate_start(*_); end
|
301
|
+
def evaluate_finish(*_); end
|
302
|
+
def run_finish; true; end
|
303
|
+
end
|
304
|
+
|
305
|
+
DEFAULT =
|
306
|
+
ColoredDocumentation.new +
|
307
|
+
TimingsAtEnd.new +
|
308
|
+
FailuresAtEnd.new
|
309
|
+
end
|
310
|
+
end
|