whitestone 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +4 -0
- data/LICENSE +16 -0
- data/README.txt +78 -0
- data/Rakefile +1 -0
- data/bin/whitestone +263 -0
- data/doc/README-snippets.rdoc +58 -0
- data/doc/whitestone.markdown +806 -0
- data/etc/aliases +5 -0
- data/etc/examples/example_1.rb +51 -0
- data/etc/examples/example_2.rb +51 -0
- data/etc/extra_tests/basic.rb +56 -0
- data/etc/extra_tests/error_should_not_also_fail.rb +17 -0
- data/etc/extra_tests/output_examples.rb +108 -0
- data/etc/extra_tests/output_examples_code.rb +38 -0
- data/etc/extra_tests/raise.rb +4 -0
- data/etc/extra_tests/realistic_example.rb +94 -0
- data/etc/extra_tests/specification_error.rb +8 -0
- data/etc/extra_tests/stop.rb +16 -0
- data/etc/extra_tests/terminate_suite.rb +56 -0
- data/etc/run-output-examples +1 -0
- data/etc/run-unit-tests +1 -0
- data/etc/ws +1 -0
- data/lib/whitestone.rb +710 -0
- data/lib/whitestone/assertion_classes.rb +418 -0
- data/lib/whitestone/auto.rb +20 -0
- data/lib/whitestone/custom_assertions.rb +252 -0
- data/lib/whitestone/include.rb +14 -0
- data/lib/whitestone/output.rb +335 -0
- data/lib/whitestone/support.rb +29 -0
- data/lib/whitestone/version.rb +3 -0
- data/test/_setup.rb +5 -0
- data/test/custom_assertions.rb +120 -0
- data/test/insulation.rb +202 -0
- data/test/whitestone_test.rb +616 -0
- data/whitestone.gemspec +31 -0
- metadata +125 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
D "Outer" do
|
2
|
+
T { 1 + 1 == 2 }
|
3
|
+
|
4
|
+
D "Inner" do
|
5
|
+
F false
|
6
|
+
N "foo".gsub!(/x/,'y')
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
D "Fail fast on error (direct execution)" do
|
11
|
+
T { 1 + 1 == 2 } # will pass
|
12
|
+
T "foo".frobnosticate? # will cause error and should cause suite to aboure
|
13
|
+
Eq "whitestone".length, 10 # would pass if it ran
|
14
|
+
Eq "whitestone".length, 17 # would fail, but shouldn't get to this point
|
15
|
+
|
16
|
+
D "Won't get here" do
|
17
|
+
Eq "won't get here".size, 30 # shouldn't see a failure for this
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
D "Fail fast on error (indirect execution)" do
|
22
|
+
T { 1 + 1 == 2 } # will pass
|
23
|
+
T { "foo".frobnosticate? } # will cause error and should cause suite to aboure
|
24
|
+
T false # shouldn't see failure for this
|
25
|
+
end
|
26
|
+
|
27
|
+
# Not implemented at the time this code was committed.
|
28
|
+
D "Fail fast on assertion failure" do
|
29
|
+
T { 1 + 1 == 2 } # will pass
|
30
|
+
Eq 5.succ, 8 # will fail and thereby cause suite to abort
|
31
|
+
Eq "whitestone".length, 10 # would pass if it ran
|
32
|
+
Eq "whitestone".length, 17 # would fail, but shouldn't get to this point
|
33
|
+
|
34
|
+
D "Won't get here" do
|
35
|
+
Eq "won't get here".size, 30 # shouldn't see a failure for this
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
D "Sibling suites unaffected by error or failure" do
|
40
|
+
D "suite 1 pass" do
|
41
|
+
T true
|
42
|
+
end
|
43
|
+
D "suite 2 fail" do
|
44
|
+
T nil
|
45
|
+
end
|
46
|
+
D "suite 3 pass (unaffected by suite 2's failure)" do
|
47
|
+
T true
|
48
|
+
end
|
49
|
+
D "suite 4 error" do
|
50
|
+
T { "foo".frobnosticate? }
|
51
|
+
end
|
52
|
+
D "suite 5 pass (unaffected by suite 4's error)" do
|
53
|
+
T true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
ruby -e 'puts "\n" * 100'; ruby -rubygems -Ilib test/output_examples.rb
|
data/etc/run-unit-tests
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby -rubygems -Ilib test/whitestone_test.rb
|
data/etc/ws
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby -Ilib -rubygems -I../col/lib bin/whitestone $*
|
data/lib/whitestone.rb
ADDED
@@ -0,0 +1,710 @@
|
|
1
|
+
require 'whitestone/support' # String enhancements
|
2
|
+
require 'whitestone/version'
|
3
|
+
require 'col' # ANSI colours
|
4
|
+
|
5
|
+
# =================== T A B L E O F C O N T E N T S ==================== #
|
6
|
+
# #
|
7
|
+
# * Exceptions; Test and Scope classes #
|
8
|
+
# * Accessory methods: stats, current_test, caught_value, exception #
|
9
|
+
# * D, D!, <, >, <<, >>, S, S!, S? #
|
10
|
+
# * Assertions: T F N Eq Mt Ko Ft E C + custom assertions + 'action' #
|
11
|
+
# * run, stop, execute, call #
|
12
|
+
# * Instance variables: @stats, @current_scope, @current_test, etc. #
|
13
|
+
# * Code for mixing in: D = ::Whitestone; T, F, Eq, Etc. #
|
14
|
+
# #
|
15
|
+
# ============================================================================ #
|
16
|
+
|
17
|
+
|
18
|
+
module Whitestone
|
19
|
+
|
20
|
+
# --------------------------------------------------------------section---- #
|
21
|
+
# #
|
22
|
+
# Exception classes #
|
23
|
+
# Test and Scope classes #
|
24
|
+
# #
|
25
|
+
# ------------------------------------------------------------------------- #
|
26
|
+
|
27
|
+
class ErrorOccurred < StandardError; end
|
28
|
+
class FailureOccurred < StandardError
|
29
|
+
def initialize(context, message, backtrace)
|
30
|
+
@context = context
|
31
|
+
@message = message
|
32
|
+
@backtrace = backtrace
|
33
|
+
end
|
34
|
+
attr_reader :context, :message, :backtrace
|
35
|
+
end
|
36
|
+
class AssertionSpecificationError < StandardError; end
|
37
|
+
|
38
|
+
##
|
39
|
+
# A Test object is what results when the following code is executed:
|
40
|
+
#
|
41
|
+
# D "civil" do
|
42
|
+
# Eq @d.year, 1972
|
43
|
+
# Eq @d.month, 5
|
44
|
+
# Eq @d.day, 13
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# Test objects gather in a tree structure, useful for reporting.
|
48
|
+
class Test
|
49
|
+
attr_accessor :description, :block, :sandbox
|
50
|
+
attr_accessor :result
|
51
|
+
attr_accessor :error
|
52
|
+
attr_accessor :parent
|
53
|
+
attr_reader :children
|
54
|
+
def initialize(description, block, sandbox)
|
55
|
+
@description, @block, @sandbox = description, block, sandbox
|
56
|
+
@result = :blank # A 'blank' result until an assertion is run.
|
57
|
+
@error = nil # The exception object, if any.
|
58
|
+
@parent = nil # The test object in whose scope this test is defined.
|
59
|
+
@children = [] # The children of this test.
|
60
|
+
end
|
61
|
+
def parent=(test)
|
62
|
+
@parent = test
|
63
|
+
if @parent
|
64
|
+
@parent.children << self
|
65
|
+
end
|
66
|
+
end
|
67
|
+
def passed?; @result == :pass; end
|
68
|
+
def failed?; @result == :fail; end
|
69
|
+
def error?; @result == :error; end
|
70
|
+
def blank?; @result == :blank; end
|
71
|
+
end # class Test
|
72
|
+
|
73
|
+
##
|
74
|
+
# A Scope object contains a group of Test objects and the setup and teardown
|
75
|
+
# information for that group. A 'D' method opens a new scope.
|
76
|
+
class Scope
|
77
|
+
attr_reader :tests, :before_each, :after_each, :before_all, :after_all
|
78
|
+
def initialize
|
79
|
+
@tests = []
|
80
|
+
@before_each = []
|
81
|
+
@after_each = []
|
82
|
+
@before_all = []
|
83
|
+
@after_all = []
|
84
|
+
end
|
85
|
+
def filter(regex)
|
86
|
+
@tests = @tests.select { |t| t.description =~ regex }
|
87
|
+
end
|
88
|
+
end # class Scope
|
89
|
+
|
90
|
+
|
91
|
+
class << Whitestone
|
92
|
+
|
93
|
+
# ------------------------------------------------------------section---- #
|
94
|
+
# #
|
95
|
+
# Accessory methods: stats, current_test, caught_value, exception #
|
96
|
+
# #
|
97
|
+
# ----------------------------------------------------------------------- #
|
98
|
+
|
99
|
+
##
|
100
|
+
# 'stats' is a hash with the following keys:
|
101
|
+
# :pass :fail :error :assertions :time
|
102
|
+
attr_reader :stats
|
103
|
+
|
104
|
+
##
|
105
|
+
# The _description_ of the currently-running test. Very useful for
|
106
|
+
# conditional breakpoints in library code. E.g.
|
107
|
+
# debugger if Whitestone.current_test =~ /something.../
|
108
|
+
def current_test
|
109
|
+
(@current_test.nil?) ? "(toplevel)" : @current_test.description
|
110
|
+
end
|
111
|
+
|
112
|
+
##
|
113
|
+
# When a C assertion is run (i.e. that the expected symbol will be thrown),
|
114
|
+
# the value that is thrown along with the symbol will be stored in
|
115
|
+
# Whitestone.caught_value in case it needs to be tested. If no value is
|
116
|
+
# thrown, this accessor will contain nil.
|
117
|
+
attr_accessor :caught_value
|
118
|
+
|
119
|
+
##
|
120
|
+
# When an E assertion is run (i.e. that the expected error will be raised),
|
121
|
+
# the exception that is rescued will be stored in Whitestone.exception in case
|
122
|
+
# it needs to be tested.
|
123
|
+
attr_accessor :exception
|
124
|
+
|
125
|
+
|
126
|
+
# ------------------------------------------------------------section---- #
|
127
|
+
# #
|
128
|
+
# D, D!, <, >, <<, >>, S, S!, S? #
|
129
|
+
# #
|
130
|
+
# ----------------------------------------------------------------------- #
|
131
|
+
|
132
|
+
##
|
133
|
+
# Defines a new test composed of the given
|
134
|
+
# description and the given block to execute.
|
135
|
+
#
|
136
|
+
# This test may contain nested tests.
|
137
|
+
#
|
138
|
+
# Tests at the outer-most level are automatically
|
139
|
+
# insulated from the top-level Ruby environment.
|
140
|
+
def D *description, &block
|
141
|
+
create_test @tests.empty?, *description, &block
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Defines a new test that is explicitly insulated from the tests
|
146
|
+
# that contain it and also from the top-level Ruby environment.
|
147
|
+
#
|
148
|
+
# This test may contain nested tests.
|
149
|
+
def D! *description, &block
|
150
|
+
create_test true, *description, &block
|
151
|
+
end
|
152
|
+
|
153
|
+
def create_test insulate, *description, &block
|
154
|
+
raise ArgumentError, 'block must be given' unless block
|
155
|
+
description = description.join(' ')
|
156
|
+
sandbox = Object.new if insulate
|
157
|
+
new_test = Whitestone::Test.new(description, block, sandbox)
|
158
|
+
new_test.parent = @tests.last
|
159
|
+
@current_scope.tests << new_test
|
160
|
+
end
|
161
|
+
private :create_test
|
162
|
+
|
163
|
+
# Registers the given block to be executed
|
164
|
+
# before each nested test inside this test.
|
165
|
+
def <(*args, &block)
|
166
|
+
if args.empty?
|
167
|
+
raise ArgumentError, 'block must be given' unless block
|
168
|
+
@current_scope.before_each << block
|
169
|
+
else
|
170
|
+
# the < method is being used as a check for inheritance
|
171
|
+
super
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Registers the given block to be executed
|
176
|
+
# after each nested test inside this test.
|
177
|
+
def > &block
|
178
|
+
raise ArgumentError, 'block must be given' unless block
|
179
|
+
@current_scope.after_each << block
|
180
|
+
end
|
181
|
+
|
182
|
+
# Registers the given block to be executed
|
183
|
+
# before all nested tests inside this test.
|
184
|
+
def << &block
|
185
|
+
raise ArgumentError, 'block must be given' unless block
|
186
|
+
@current_scope.before_all << block
|
187
|
+
end
|
188
|
+
|
189
|
+
# Registers the given block to be executed
|
190
|
+
# after all nested tests inside this test.
|
191
|
+
def >> &block
|
192
|
+
raise ArgumentError, 'block must be given' unless block
|
193
|
+
@current_scope.after_all << block
|
194
|
+
end
|
195
|
+
|
196
|
+
# Mechanism for sharing code between tests.
|
197
|
+
#
|
198
|
+
# S :values do
|
199
|
+
# @values = [8,9,10]
|
200
|
+
# end
|
201
|
+
#
|
202
|
+
# D "some test" do
|
203
|
+
# S :values
|
204
|
+
# Eq @values.last, 10
|
205
|
+
# end
|
206
|
+
#
|
207
|
+
def S identifier, &block
|
208
|
+
if block_given?
|
209
|
+
if already_shared = @share[identifier]
|
210
|
+
msg = "A code block #{already_shared.inspect} has already " \
|
211
|
+
"been shared under the identifier #{identifier.inspect}."
|
212
|
+
raise ArgumentError, msg
|
213
|
+
end
|
214
|
+
@share[identifier] = block
|
215
|
+
|
216
|
+
elsif block = @share[identifier]
|
217
|
+
if @tests.empty?
|
218
|
+
msg = "Cannot inject code block #{block.inspect} shared under " \
|
219
|
+
"identifier #{identifier.inspect} outside of a Whitestone test."
|
220
|
+
raise
|
221
|
+
else
|
222
|
+
# Find the closest insulated parent test; this should always
|
223
|
+
# succeed because root-level tests are insulated by default.
|
224
|
+
test = @tests.reverse.find { |t| t.sandbox }
|
225
|
+
test.sandbox.instance_eval(&block)
|
226
|
+
end
|
227
|
+
|
228
|
+
else
|
229
|
+
raise ArgumentError, "No code block is shared under " \
|
230
|
+
"identifier #{identifier.inspect}."
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Shares the given code block AND inserts it in-place.
|
235
|
+
# (Well, by in-place, I mean the closest insulated block.)
|
236
|
+
def S! identifier, &block
|
237
|
+
raise 'block must be given' unless block_given?
|
238
|
+
S identifier, &block
|
239
|
+
S identifier
|
240
|
+
end
|
241
|
+
|
242
|
+
# Checks whether any code has been shared under the given identifier.
|
243
|
+
def S? identifier
|
244
|
+
@share.key? identifier
|
245
|
+
end
|
246
|
+
|
247
|
+
|
248
|
+
# ------------------------------------------------------------section---- #
|
249
|
+
# #
|
250
|
+
# Assertions: T F N Eq Mt Ko Ft E C #
|
251
|
+
# + custom assertions #
|
252
|
+
# + the 'action' method #
|
253
|
+
# #
|
254
|
+
# ----------------------------------------------------------------------- #
|
255
|
+
|
256
|
+
require 'whitestone/assertion_classes'
|
257
|
+
# ^^^ Assertion::True, Assertion::False, Assertion::Equality, etc.
|
258
|
+
require 'whitestone/custom_assertions'
|
259
|
+
# ^^^ Assertion::Custom
|
260
|
+
|
261
|
+
ASSERTION_CLASSES = {
|
262
|
+
:T => Assertion::True, :F => Assertion::False, :N => Assertion::Nil,
|
263
|
+
:Eq => Assertion::Equality, :Mt => Assertion::Match, :Ko => Assertion::KindOf,
|
264
|
+
:Ft => Assertion::FloatEqual, :Id => Assertion::Identity,
|
265
|
+
:E => Assertion::ExpectError, :C => Assertion::Catch,
|
266
|
+
:custom => Assertion::Custom
|
267
|
+
}
|
268
|
+
|
269
|
+
# Dynamically define the primitive assertion methods.
|
270
|
+
|
271
|
+
%w{T F N Eq Mt Ko Ft Id E C}.each do |base|
|
272
|
+
assert_method = base
|
273
|
+
negate_method = base + "!"
|
274
|
+
query_method = base + "?"
|
275
|
+
|
276
|
+
lineno = __LINE__
|
277
|
+
code = %{
|
278
|
+
def #{assert_method}(*args, &block)
|
279
|
+
action :#{base}, :assert, *args, &block
|
280
|
+
end
|
281
|
+
|
282
|
+
def #{negate_method}(*args, &block)
|
283
|
+
action :#{base}, :negate, *args, &block
|
284
|
+
end
|
285
|
+
|
286
|
+
def #{query_method}(*args, &block)
|
287
|
+
action :#{base}, :query, *args, &block
|
288
|
+
end
|
289
|
+
}
|
290
|
+
module_eval code, __FILE__, lineno+2
|
291
|
+
end
|
292
|
+
|
293
|
+
# === Whitestone.action
|
294
|
+
#
|
295
|
+
# This is an absolutely key method. It implements T, F, Eq, T!, F?, Eq?, etc.
|
296
|
+
# After some sanity checking, it creates an assertion object, runs it, and
|
297
|
+
# sees whether it passed or failed.
|
298
|
+
#
|
299
|
+
# If the assertion fails, we raise FailureOccurred, with the necessary
|
300
|
+
# information about the failure. If an error happens while the assertion is
|
301
|
+
# run, we don't catch it. Both the error and the failure are handled
|
302
|
+
# upstream, in Whitestone.call.
|
303
|
+
#
|
304
|
+
# It's worth noting that errors can occur while tests are run that are
|
305
|
+
# unconnected to this method. Consider these two examples:
|
306
|
+
# T { "foo".frobnosticate? } -- error occurs on our watch
|
307
|
+
# T "foo".frobnosticate? -- error occurs before T() is called
|
308
|
+
#
|
309
|
+
# By letting errors from here escape, the two cases can be dealt with
|
310
|
+
# together.
|
311
|
+
#
|
312
|
+
# T and F are special cases: they can be called with custom assertions.
|
313
|
+
#
|
314
|
+
# T :circle, c, [4,1, 10, :H]
|
315
|
+
# -> run_custom_test(:circle, :assert, [4,1,10,:H])
|
316
|
+
#
|
317
|
+
def action(base, assert_negate_query, *args, &block)
|
318
|
+
mode = assert_negate_query # :assert, :negate or :query
|
319
|
+
|
320
|
+
# Sanity checks: these should never fail!
|
321
|
+
unless [:assert, :negate, :query].include? mode
|
322
|
+
raise AssertionSpecificationError, "Invalid mode: #{mode.inspect}"
|
323
|
+
end
|
324
|
+
unless ASSERTION_CLASSES.key? base
|
325
|
+
raise AssertionSpecificationError, "Invalid base: #{base.inspect}"
|
326
|
+
end
|
327
|
+
|
328
|
+
# Special case: T may be used to invoke custom assertions.
|
329
|
+
# We catch the use of F as well, even though it's disallowed, so that
|
330
|
+
# we can give an appropriate error message.
|
331
|
+
if base == :T or base == :F and args.size > 1 and args.first.is_a? Symbol
|
332
|
+
if base == :T and mode == :assert
|
333
|
+
# Run a custom assertion.
|
334
|
+
inside_custom_assertion do
|
335
|
+
action(:custom, :assert, *args)
|
336
|
+
end
|
337
|
+
return nil
|
338
|
+
else
|
339
|
+
message = "You are attempting to run a custom assertion.\n"
|
340
|
+
message << "These can only be run with T, not F, T?, T!, F? etc."
|
341
|
+
raise AssertionSpecificationError, message
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
assertion = ASSERTION_CLASSES[base].new(mode, *args, &block)
|
346
|
+
# e.g. assertion = Assertion::Equality(:assert, 4, 4) # no block
|
347
|
+
# assertion = Assertion::Nil(:query) { names.find "Tobias" }
|
348
|
+
# assertion = Assertion::Custom(...)
|
349
|
+
|
350
|
+
stats[:assertions] += 1 unless @inside_custom_assertion
|
351
|
+
|
352
|
+
# We run the assertion (returns true for pass and false for fail).
|
353
|
+
passed = assertion.run
|
354
|
+
|
355
|
+
# We negate the result if neccesary...
|
356
|
+
case mode
|
357
|
+
when :negate then passed = ! passed
|
358
|
+
when :query then return passed
|
359
|
+
end
|
360
|
+
# ...and report a failure if necessary.
|
361
|
+
if passed
|
362
|
+
# We do this here because we only want the test to pass if it actually
|
363
|
+
# runs an assertion; otherwise its result is 'blank'. If a later
|
364
|
+
# assertion in the test fails or errors, the result will be rewritten.
|
365
|
+
@current_test.result = :pass if @current_test
|
366
|
+
else
|
367
|
+
calling_context = assertion.block || @calls.last
|
368
|
+
backtrace = caller
|
369
|
+
raise FailureOccurred.new(calling_context, assertion.message, backtrace)
|
370
|
+
end
|
371
|
+
end # action
|
372
|
+
private :action
|
373
|
+
|
374
|
+
##
|
375
|
+
# {inside_custom_assertion} allows us (via {yield}) to run a custom
|
376
|
+
# assertion without racking up the assertion count for each of the
|
377
|
+
# assertions therein.
|
378
|
+
# Todo: consider making it a stack so that custom assertions can be nested.
|
379
|
+
def inside_custom_assertion
|
380
|
+
@inside_custom_assertion = true
|
381
|
+
stats[:assertions] += 1
|
382
|
+
yield
|
383
|
+
ensure
|
384
|
+
@inside_custom_assertion = false
|
385
|
+
end
|
386
|
+
private :inside_custom_assertion
|
387
|
+
|
388
|
+
##
|
389
|
+
# Whitestone.custom _defines_ a custom assertion.
|
390
|
+
#
|
391
|
+
# Example usage:
|
392
|
+
# Whitestone.custom :circle, {
|
393
|
+
# :description => "Circle equality",
|
394
|
+
# :parameters => [ [:circle, Circle], [:values, Array] ],
|
395
|
+
# :run => lambda { |circle, values|
|
396
|
+
# x, y, r, label = values
|
397
|
+
# test('x') { Ft x, circle.centre.x }
|
398
|
+
# test('y') { Ft y, circle.centre.y }
|
399
|
+
# test('r') { Ft r, circle.radius }
|
400
|
+
# test('label') { Eq Label[label], circle.label }
|
401
|
+
# }
|
402
|
+
# }
|
403
|
+
def custom(name, definition)
|
404
|
+
define_custom_assertion(name, definition)
|
405
|
+
end
|
406
|
+
|
407
|
+
def define_custom_assertion(name, definition)
|
408
|
+
legitimate_keys = Set[:description, :parameters, :check, :run]
|
409
|
+
unless Symbol === name and Hash === definition and
|
410
|
+
(definition.keys + [:check]).all? { |key| legitimate_keys.include? key }
|
411
|
+
message = %{
|
412
|
+
#
|
413
|
+
#Usage:
|
414
|
+
# Whitestone.custom(name, definition)
|
415
|
+
# where name is a symbol
|
416
|
+
# and definition is a hash with keys :description, :parameters, :run
|
417
|
+
# and optionally :check
|
418
|
+
}.___margin
|
419
|
+
raise AssertionSpecificationError, Col[message].yb
|
420
|
+
end
|
421
|
+
Assertion::Custom.define(name, definition)
|
422
|
+
end
|
423
|
+
private :define_custom_assertion
|
424
|
+
|
425
|
+
|
426
|
+
# ------------------------------------------------------------section---- #
|
427
|
+
# #
|
428
|
+
# run, stop, execute, call #
|
429
|
+
# #
|
430
|
+
# Only 'run' and 'stop' are public, but 'execute' and 'call' are #
|
431
|
+
# fundamentally important methods for the operation of whitestone. #
|
432
|
+
# #
|
433
|
+
# ----------------------------------------------------------------------- #
|
434
|
+
|
435
|
+
#
|
436
|
+
# === Whitestone.run
|
437
|
+
#
|
438
|
+
# Executes all tests defined thus far. Tests are defined by 'D' blocks.
|
439
|
+
# Test objects live in a Scope. @current_scope is the top-level scope, but
|
440
|
+
# this variable is changed during execution to point to nested scopes as
|
441
|
+
# needed (and then changed back again).
|
442
|
+
#
|
443
|
+
# This method should therefore be run _after_ all the tests have been
|
444
|
+
# defined, e.g. in an at_exit clause. Requiring 'whitestone/auto' does that for
|
445
|
+
# you.
|
446
|
+
#
|
447
|
+
# Argument: options hash
|
448
|
+
# * {:filter} is a Regex. Only top-level tests whose descriptions
|
449
|
+
# match that regex will be run.
|
450
|
+
# * {:full_backtrace} is true or false: do you want the backtraces
|
451
|
+
# reported in event of failure or error to be filtered or not? Most of the
|
452
|
+
# time you would want them to be filtered (therefore _false_).
|
453
|
+
#
|
454
|
+
def run(options={})
|
455
|
+
test_filter_pattern = options[:filter]
|
456
|
+
@output.set_full_backtrace if options[:full_backtrace]
|
457
|
+
# Clear previous results.
|
458
|
+
@stats.clear
|
459
|
+
@tests.clear
|
460
|
+
|
461
|
+
# Filter the tests if asked to.
|
462
|
+
if test_filter_pattern
|
463
|
+
@top_level.filter(test_filter_pattern)
|
464
|
+
if @top_level.tests.empty?
|
465
|
+
msg = "!! Applied filter #{test_filter_pattern.inspect}, which left no tests to be run!"
|
466
|
+
STDERR.puts Col[msg].yb
|
467
|
+
exit
|
468
|
+
end
|
469
|
+
end
|
470
|
+
|
471
|
+
# Execute the tests.
|
472
|
+
@stats[:time] = record_execution_time do
|
473
|
+
catch(:stop_dfect_execution) do
|
474
|
+
execute # <-- This is where the real action takes place.
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
# Display reports.
|
479
|
+
@output.display_test_by_test_result(@top_level)
|
480
|
+
@output.display_details_of_failures_and_errors
|
481
|
+
@output.display_results_npass_nfail_nerror_etc(@stats)
|
482
|
+
|
483
|
+
@top_level = @current_scope = Whitestone::Scope.new
|
484
|
+
# ^^^ In case 'run' gets called again; we don't want to re-run the old tests.
|
485
|
+
end
|
486
|
+
|
487
|
+
#
|
488
|
+
# === Whitestone.stop
|
489
|
+
#
|
490
|
+
# Stops the execution of the {Whitestone.run} method or raises
|
491
|
+
# an exception if that method is not currently executing.
|
492
|
+
#
|
493
|
+
def stop
|
494
|
+
throw :stop_dfect_execution
|
495
|
+
end
|
496
|
+
|
497
|
+
# Record the elapsed time to execute the given block.
|
498
|
+
def record_execution_time
|
499
|
+
start = Time.now
|
500
|
+
yield
|
501
|
+
finish = Time.now
|
502
|
+
finish - start
|
503
|
+
end
|
504
|
+
private :record_execution_time
|
505
|
+
|
506
|
+
#
|
507
|
+
# === Whitestone.execute
|
508
|
+
#
|
509
|
+
# Executes the current test scope recursively. A SCOPE is a collection of D
|
510
|
+
# blocks, and the contents of each D block is a TEST, comprising a
|
511
|
+
# description and a block of code. Because a test block may contain D
|
512
|
+
# statements within it, when a test block is run @current_scope is set to
|
513
|
+
# Scope.new so that newly-encountered tests can be added to it. That scope
|
514
|
+
# is then executed recursively. The invariant is this: @current_scope is
|
515
|
+
# the CURRENT scope to which tests may be added. At the end of 'execute',
|
516
|
+
# @current_scope is restored to its previous value.
|
517
|
+
#
|
518
|
+
# The per-test guts of this method have been extracted to {execute_test} so
|
519
|
+
# that the structure of {execute} is easier to see. {execute_test} contains
|
520
|
+
# lots of exception handling and comments.
|
521
|
+
def execute
|
522
|
+
@current_scope.before_all.each {|b| call b } # Run pre-test setup
|
523
|
+
@current_scope.tests.each do |test| # Loop through tests
|
524
|
+
@current_scope.before_each.each {|b| call b } # Run per-test setup
|
525
|
+
@tests.push test; @current_test = test
|
526
|
+
|
527
|
+
execute_test(test) # Run the test
|
528
|
+
|
529
|
+
@tests.pop; @current_test = @tests.last
|
530
|
+
@current_scope.after_each.each {|b| call b } # Run per-test teardown
|
531
|
+
end
|
532
|
+
@current_scope.after_all.each {|b| call b } # Run post-test teardown
|
533
|
+
end
|
534
|
+
private :execute
|
535
|
+
|
536
|
+
#
|
537
|
+
# === Whitestone.execute_test
|
538
|
+
#
|
539
|
+
# Executes a single test (block containing assertions). That wouldn't be so
|
540
|
+
# hard, except that there could be new tests defined within that block, so
|
541
|
+
# we need to create a new scope into which such tests may be placed [in
|
542
|
+
# {create_test} -- {@current_scope.tests << Test.new(...)}].
|
543
|
+
#
|
544
|
+
# The old scope is restored at the end of the method.
|
545
|
+
#
|
546
|
+
# The new scope is executed recursively in order to run any tests created
|
547
|
+
# therein.
|
548
|
+
#
|
549
|
+
# Exception (and failure) handling is straightforward here. The hard work
|
550
|
+
# is done in {call}; we just catch them and do nothing. The point is to
|
551
|
+
# avoid the recursive {execute}: fail fast.
|
552
|
+
#
|
553
|
+
def execute_test(test)
|
554
|
+
stored_scope = @current_scope
|
555
|
+
begin
|
556
|
+
# Create nested scope in case a 'D' is encountered while running the test.
|
557
|
+
@current_scope = Whitestone::Scope.new
|
558
|
+
|
559
|
+
# Run the test block, which may create new tests along the way (if the
|
560
|
+
# block includes any calls to 'D').
|
561
|
+
call test.block, test.sandbox
|
562
|
+
|
563
|
+
# Increment the pass count _if_ the current test passed, which it only
|
564
|
+
# does if at least one assertion was run.
|
565
|
+
@stats[:pass] += 1 if @current_test.passed?
|
566
|
+
|
567
|
+
# Execute the nested scope. Nothing will happen if there are no tests
|
568
|
+
# in the nested scope because before_all, tests and after_all will be
|
569
|
+
# empty.
|
570
|
+
execute
|
571
|
+
|
572
|
+
rescue FailureOccurred => f
|
573
|
+
# See method-level comment regarding exception handling.
|
574
|
+
:noop
|
575
|
+
rescue ErrorOccurred => e
|
576
|
+
:noop
|
577
|
+
rescue Exception => e
|
578
|
+
# We absolutely should not be receiving an exception here. Exceptions
|
579
|
+
# are caught up the line, dealt with, and ErrorOccurred is raised. If
|
580
|
+
# we get here, something is strange and we should exit.
|
581
|
+
STDERR.puts "Internal error: #{__FILE__}:#{__LINE__}; exiting"
|
582
|
+
puts e.inspect
|
583
|
+
puts e.backtrace
|
584
|
+
exit!
|
585
|
+
ensure
|
586
|
+
# Restore the previous values of @current_scope
|
587
|
+
@current_scope = stored_scope
|
588
|
+
end
|
589
|
+
end # execute_test
|
590
|
+
|
591
|
+
|
592
|
+
# === Whitestone.call
|
593
|
+
#
|
594
|
+
# Invokes the given block and debugs any exceptions that may arise as a result.
|
595
|
+
# The block can be from a Test object or a "before-each"-style block.
|
596
|
+
#
|
597
|
+
# If an assertion fails or an error occurs during the running of a test, it
|
598
|
+
# is dealt with in this method (update the stats, update the test object,
|
599
|
+
# re-raise so the upstream method {execute} can abort the current test/scope.
|
600
|
+
#
|
601
|
+
def call(block, sandbox = nil)
|
602
|
+
begin
|
603
|
+
@calls.push block
|
604
|
+
|
605
|
+
if sandbox
|
606
|
+
sandbox.instance_eval(&block)
|
607
|
+
else
|
608
|
+
block.call
|
609
|
+
end
|
610
|
+
|
611
|
+
rescue FailureOccurred => f
|
612
|
+
## A failure has occurred while running a test. We report the failure
|
613
|
+
## and re-raise the exception so that the calling code knows not to
|
614
|
+
## continue with this test.
|
615
|
+
@stats[:fail] += 1
|
616
|
+
@current_test.result = :fail
|
617
|
+
@output.report_failure( current_test, f.message, f.backtrace )
|
618
|
+
raise
|
619
|
+
|
620
|
+
rescue Exception, AssertionSpecificationError => e
|
621
|
+
## An error has occurred while running a test.
|
622
|
+
## OR
|
623
|
+
## An assertion was not properly specified.
|
624
|
+
##
|
625
|
+
## We record and report the error and then raise Whitestone::ErrorOccurred
|
626
|
+
## so that the code running the test knows an error occurred. It
|
627
|
+
## doesn't need to do anything with the error; it's just a signal.
|
628
|
+
@stats[:error] += 1
|
629
|
+
@current_test.result = :error
|
630
|
+
@current_test.error = e
|
631
|
+
if e.class == AssertionSpecificationError
|
632
|
+
@output.report_uncaught_exception( current_test, e, @calls, :filter )
|
633
|
+
else
|
634
|
+
@output.report_uncaught_exception( current_test, e, @calls )
|
635
|
+
end
|
636
|
+
raise ErrorOccurred
|
637
|
+
|
638
|
+
ensure
|
639
|
+
@calls.pop
|
640
|
+
end
|
641
|
+
end # call
|
642
|
+
private :call
|
643
|
+
|
644
|
+
end # class << Whitestone
|
645
|
+
|
646
|
+
|
647
|
+
# --------------------------------------------------------------section---- #
|
648
|
+
# #
|
649
|
+
# Instance variables: #
|
650
|
+
# @stats, @current_scope, @current_test, @share, and others #
|
651
|
+
# #
|
652
|
+
# ------------------------------------------------------------------------- #
|
653
|
+
|
654
|
+
# Here we are in 'module Whitestone', not 'module << Whitestone', as it were.
|
655
|
+
|
656
|
+
@stats = Hash.new { |h,k| h[k] = 0 }
|
657
|
+
|
658
|
+
@top_level = Whitestone::Scope.new
|
659
|
+
# We maintain a handle on the top-level scope so we can
|
660
|
+
# walk the tree and produce a report.
|
661
|
+
@current_scope = @top_level
|
662
|
+
# The current scope in which tests are defined. Scopes
|
663
|
+
# nest; this is handled by saving and restoring state
|
664
|
+
# in the recursive method 'execute'.
|
665
|
+
@tests = [] # Stack of the current tests in scope (as opposed to a list
|
666
|
+
# of the tests in the current scope).
|
667
|
+
@current_test = nil # Should be equal to @tests.last.
|
668
|
+
@share = {}
|
669
|
+
@calls = [] # Stack of blocks that are executed, allowing access to
|
670
|
+
# the outer context for error reporting.
|
671
|
+
require 'whitestone/output'
|
672
|
+
@output = Output.new # Handles output of reports to the console.
|
673
|
+
|
674
|
+
|
675
|
+
# --------------------------------------------------------------section---- #
|
676
|
+
# #
|
677
|
+
# D: alias for Whitestone to allow D.< etc. #
|
678
|
+
# Mixin methods T, F, Eq, ... #
|
679
|
+
# #
|
680
|
+
# ------------------------------------------------------------------------- #
|
681
|
+
|
682
|
+
# Allows before and after hooks to be specified via the
|
683
|
+
# following method syntax when this module is mixed-in:
|
684
|
+
#
|
685
|
+
# D .<< { puts "before all nested tests" }
|
686
|
+
# D .< { puts "before each nested test" }
|
687
|
+
# D .> { puts "after each nested test" }
|
688
|
+
# D .>> { puts "after all nested tests" }
|
689
|
+
#
|
690
|
+
D = ::Whitestone
|
691
|
+
|
692
|
+
# Provide mixin-able assertion methods. These are defined in the module
|
693
|
+
# Whitestone (instead of being directly executable methods like Whitestone.Eq)
|
694
|
+
# and as such can be mixed in to the top level with an `include Whitestone`.
|
695
|
+
methods(false).grep(/^(x?[A-Z][a-z]?)?[<>!?]*$/).each do |name|
|
696
|
+
#
|
697
|
+
# XXX: using eval() on a string because Ruby 1.8's
|
698
|
+
# define_method() cannot take a block parameter
|
699
|
+
#
|
700
|
+
module_eval "def #{name}(*a, &b) ::#{self.name}.#{name}(*a, &b) end",
|
701
|
+
__FILE__, __LINE__
|
702
|
+
unless name =~ /[<>]/
|
703
|
+
# Also define 'x' method that is a no-op; e.g. xD, xT, ...
|
704
|
+
module_eval "def x#{name}(*a, &b) :no_op end", __FILE__, __LINE__
|
705
|
+
module_eval "def Whitestone.x#{name}(*a, &b) :no_op end", __FILE__, __LINE__
|
706
|
+
end
|
707
|
+
end
|
708
|
+
|
709
|
+
end # module Whitestone
|
710
|
+
|