xspec 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ # # Autorun
2
+
3
+ # A convenience file that sets up an auto-running common case DSL in the
4
+ # top-level namespace to get you going fast.
5
+ require 'xspec'
6
+
7
+ extend XSpec.dsl
8
+ autorun!
@@ -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
@@ -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