whitestone 1.0.0
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.
- 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
|
+
|