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,418 @@
|
|
1
|
+
|
2
|
+
require 'set'
|
3
|
+
require 'differ'
|
4
|
+
module BoldColor
|
5
|
+
class << self
|
6
|
+
def format(change)
|
7
|
+
(change.change? && as_change(change)) ||
|
8
|
+
(change.delete? && as_delete(change)) ||
|
9
|
+
(change.insert? && as_insert(change)) ||
|
10
|
+
''
|
11
|
+
end
|
12
|
+
private
|
13
|
+
def as_insert(change) Col[change.insert].green.bold.to_s end
|
14
|
+
def as_delete(change) Col[change.delete].red.bold.to_s end
|
15
|
+
def as_change(change) as_delete(change) << as_insert(change) end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
Differ.format = BoldColor
|
19
|
+
|
20
|
+
# --------------------------------------------------------------------------- #
|
21
|
+
|
22
|
+
module Whitestone
|
23
|
+
|
24
|
+
module Assertion
|
25
|
+
|
26
|
+
##
|
27
|
+
# Various methods to guard against invalid assertions. All of these raise
|
28
|
+
# AssertionSpecificationError if there is a problem.
|
29
|
+
#
|
30
|
+
module Guards
|
31
|
+
extend self # All methods here may be mixed in or called directly;
|
32
|
+
# e.g. Assertion::Guards.type_check("foo", String)
|
33
|
+
|
34
|
+
## Return a lambda that can be run. If the user specified a block, it's
|
35
|
+
## that. If not, it's the first argument. If both or neither, it's an
|
36
|
+
## error. If there's more arguments than necessary, it's an error.
|
37
|
+
def args_or_block_one_only(args, block)
|
38
|
+
if block and args.empty?
|
39
|
+
block
|
40
|
+
elsif !block and args.size == 1
|
41
|
+
lambda { args.first }
|
42
|
+
else
|
43
|
+
raise AssertionSpecificationError, "Improper arguments to T"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def one_argument(array)
|
48
|
+
unless array.size == 1
|
49
|
+
raise AssertionSpecificationError, "Exactly one argument required"
|
50
|
+
end
|
51
|
+
array.first
|
52
|
+
end
|
53
|
+
|
54
|
+
def two_arguments(array)
|
55
|
+
unless array.size == 2
|
56
|
+
raise AssertionSpecificationError, "Exactly two arguments required"
|
57
|
+
end
|
58
|
+
array
|
59
|
+
end
|
60
|
+
|
61
|
+
def two_or_three_arguments(array)
|
62
|
+
unless array.size == 2 or array.size == 3
|
63
|
+
raise AssertionSpecificationError, "Exactly two or three arguments required"
|
64
|
+
end
|
65
|
+
array
|
66
|
+
end
|
67
|
+
|
68
|
+
def no_block_allowed
|
69
|
+
if @block
|
70
|
+
raise AssertionSpecificationError, "This method doesn't take a block"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def block_required(block)
|
75
|
+
unless block
|
76
|
+
raise AssertionSpecificationError, "The method requires a block"
|
77
|
+
end
|
78
|
+
block
|
79
|
+
end
|
80
|
+
|
81
|
+
def type_check(args, types)
|
82
|
+
if Class === types
|
83
|
+
types = args.map { types }
|
84
|
+
end
|
85
|
+
if types.size != args.size
|
86
|
+
raise AssertionSpecificationError, "Incorrect number of types provided"
|
87
|
+
end
|
88
|
+
args.zip(types).each do |arg, type|
|
89
|
+
unless arg.is_a? type
|
90
|
+
msg = "Argument error: expected #{type}; "\
|
91
|
+
"got #{arg.inspect} (#{arg.class})"
|
92
|
+
raise AssertionSpecificationError, msg
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end # module Assertion::Guards
|
97
|
+
|
98
|
+
# ----------------------------------------------------------------------- #
|
99
|
+
|
100
|
+
##
|
101
|
+
# A class in the Assertion namespace meets the following criteria:
|
102
|
+
# def initialize(mode, *args, &block)
|
103
|
+
# def run # -> true or false (representing pass or fail)
|
104
|
+
#
|
105
|
+
# The idea is to support T, F, Eq, etc. The initialize method ensures the
|
106
|
+
# correct number, type and combination of arguments are provided (e.g. you
|
107
|
+
# can't provide and argument _and_ a block for T, F or N).
|
108
|
+
#
|
109
|
+
# Any Assertion::XYZ object answers to #block (to provide the context of a
|
110
|
+
# failure or error; may be nil) and #message (which returns the message the
|
111
|
+
# user sees).
|
112
|
+
#
|
113
|
+
# Every subclass must call *super* in its initialize method so that the mode
|
114
|
+
# and the block can be correctly stored.
|
115
|
+
#
|
116
|
+
class Base
|
117
|
+
include Assertion::Guards
|
118
|
+
def initialize(mode, *args, &block)
|
119
|
+
@mode = mode
|
120
|
+
@block = block
|
121
|
+
end
|
122
|
+
|
123
|
+
def block
|
124
|
+
@block
|
125
|
+
end
|
126
|
+
|
127
|
+
def message
|
128
|
+
"No message implemented for class #{self.class} yet."
|
129
|
+
end
|
130
|
+
end # class Assertion::Base
|
131
|
+
|
132
|
+
# ----------------------------------------------------------------------- #
|
133
|
+
|
134
|
+
class True < Base
|
135
|
+
def initialize(mode, *args, &block)
|
136
|
+
super
|
137
|
+
@test_lambda = args_or_block_one_only(args, block)
|
138
|
+
end
|
139
|
+
def run
|
140
|
+
@test_lambda.call ? true : false
|
141
|
+
end
|
142
|
+
def message
|
143
|
+
Col["Assertion failed"].yb
|
144
|
+
end
|
145
|
+
end # class Assertion::True
|
146
|
+
|
147
|
+
class False < True
|
148
|
+
def run
|
149
|
+
not super # False is the _opposite_ of True
|
150
|
+
end
|
151
|
+
end # class Assertion::False
|
152
|
+
|
153
|
+
class Nil < Base
|
154
|
+
def initialize(mode, *args, &block)
|
155
|
+
super
|
156
|
+
@test_lambda = args_or_block_one_only(args, block)
|
157
|
+
end
|
158
|
+
def run
|
159
|
+
@test_lambda.call.nil?
|
160
|
+
end
|
161
|
+
def message
|
162
|
+
msg = Col['Condition expected NOT to be nil'].yb
|
163
|
+
case @mode
|
164
|
+
when :assert then msg.sub(' NOT', '')
|
165
|
+
when :negate then msg
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end # class Assertion::Nil
|
169
|
+
|
170
|
+
class Equality < Base
|
171
|
+
def initialize(mode, *args, &block)
|
172
|
+
super
|
173
|
+
@actual, @expected = two_arguments(args)
|
174
|
+
no_block_allowed
|
175
|
+
end
|
176
|
+
def run
|
177
|
+
@expected == @actual
|
178
|
+
end
|
179
|
+
def message
|
180
|
+
case @mode
|
181
|
+
when :assert
|
182
|
+
String.new.tap { |str|
|
183
|
+
str << Col["Equality test failed"].yb
|
184
|
+
str << Col["\n Should be: ", @expected.inspect].fmt(:yb, :gb)
|
185
|
+
str << Col["\n Was: ", @actual.inspect].fmt(:rb, :rb)
|
186
|
+
if String === @actual and String === @expected \
|
187
|
+
and @expected.length > 40 and @actual.length > 40
|
188
|
+
diff = Differ.diff_by_char(@expected.inspect, @actual.inspect)
|
189
|
+
str << "\n" << " Dif: #{diff}"
|
190
|
+
end
|
191
|
+
}
|
192
|
+
when :negate
|
193
|
+
if @expected.inspect.length < 10
|
194
|
+
Col["Inequality test failed: object should not equal",
|
195
|
+
@expected.inspect].fmt [:yb, :rb]
|
196
|
+
else
|
197
|
+
Col.inline(
|
198
|
+
"Inequality test failed: the two objects were equal.\n", :yb,
|
199
|
+
" Value: ", :yb,
|
200
|
+
@expected.inspect, :rb
|
201
|
+
)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end # class Assertion::Equality
|
206
|
+
|
207
|
+
class Match < Base
|
208
|
+
def initialize(mode, *args, &block)
|
209
|
+
super
|
210
|
+
no_block_allowed
|
211
|
+
args = two_arguments(args)
|
212
|
+
unless args.map { |a| a.class }.to_set == Set[Regexp, String]
|
213
|
+
raise AssertionSpecificationError, "Expect a String and a Regexp (any order)"
|
214
|
+
end
|
215
|
+
@regexp, @string = args
|
216
|
+
if String === @regexp
|
217
|
+
@string, @regexp = @regexp, @string
|
218
|
+
end
|
219
|
+
@string = Col.uncolored(@string)
|
220
|
+
end
|
221
|
+
def run
|
222
|
+
@regexp =~ @string
|
223
|
+
end
|
224
|
+
def message
|
225
|
+
_not_ =
|
226
|
+
case @mode
|
227
|
+
when :assert then " "
|
228
|
+
when :negate then " NOT "
|
229
|
+
end
|
230
|
+
String.new.tap { |str|
|
231
|
+
string = Col.plain(@string).inspect.___truncate(200)
|
232
|
+
regexp = @regexp.inspect
|
233
|
+
str << Col["Match failure: string should#{_not_}match regex\n"].yb.to_s
|
234
|
+
str << Col[" String: ", string].fmt('yb,rb') << "\n"
|
235
|
+
str << Col[" Regexp: ", regexp].fmt('yb,gb')
|
236
|
+
}
|
237
|
+
end
|
238
|
+
end # class Assertion::Match
|
239
|
+
|
240
|
+
class KindOf < Base
|
241
|
+
def initialize(mode, *args, &block)
|
242
|
+
super
|
243
|
+
no_block_allowed
|
244
|
+
args = two_arguments(args)
|
245
|
+
type_check(args, [Object,Module])
|
246
|
+
@object, @klass = args
|
247
|
+
end
|
248
|
+
def run
|
249
|
+
@object.kind_of? @klass
|
250
|
+
end
|
251
|
+
def message
|
252
|
+
_not_ =
|
253
|
+
case @mode
|
254
|
+
when :assert then " "
|
255
|
+
when :negate then " NOT "
|
256
|
+
end
|
257
|
+
Col.inline(
|
258
|
+
"Type failure: object expected#{_not_}to be of type #{@klass}\n", :yb,
|
259
|
+
" Object's class is ", :yb,
|
260
|
+
@object.class, :rb
|
261
|
+
)
|
262
|
+
end
|
263
|
+
end # class Assertion::KindOf
|
264
|
+
|
265
|
+
class FloatEqual < Base
|
266
|
+
EPSILON = 0.000001
|
267
|
+
def initialize(mode, *args, &block)
|
268
|
+
super
|
269
|
+
no_block_allowed
|
270
|
+
type_check(args, Numeric)
|
271
|
+
@actual, @expected, @epsilon = two_or_three_arguments(args).map { |x| x.to_f }
|
272
|
+
@epsilon ||= EPSILON
|
273
|
+
end
|
274
|
+
def run
|
275
|
+
if @actual.zero? or @expected.zero?
|
276
|
+
# There's no scale, so we can only go on difference.
|
277
|
+
(@actual - @expected) < @epsilon
|
278
|
+
else
|
279
|
+
# We go by ratio. The ratio of two equal numbers is one, so the ratio
|
280
|
+
# of two practically-equal floats will be very nearly one.
|
281
|
+
@ratio = (@actual/@expected - 1).abs
|
282
|
+
@ratio < @epsilon
|
283
|
+
end
|
284
|
+
end
|
285
|
+
def message
|
286
|
+
String.new.tap { |str|
|
287
|
+
case @mode
|
288
|
+
when :assert
|
289
|
+
str << Col["Float equality test failed"].yb
|
290
|
+
str << "\n" << Col[" Should be: #{@expected.inspect}"].gb
|
291
|
+
str << "\n" << Col[" Was: #{@actual.inspect}"].rb
|
292
|
+
str << "\n" << " Epsilon: #{@epsilon}"
|
293
|
+
if @ratio
|
294
|
+
str << "\n" << " Ratio: #{@ratio}"
|
295
|
+
end
|
296
|
+
when :negate
|
297
|
+
line = "Float inequality test failed: the two values were essentially equal."
|
298
|
+
str << Col[line].yb
|
299
|
+
str << "\n" << Col[" Value 1: ", @actual.inspect ].fmt(:yb, :rb)
|
300
|
+
str << "\n" << Col[" Value 2: ", @expected.inspect].fmt(:yb, :rb)
|
301
|
+
str << "\n" << " Epsilon: #{@epsilon}"
|
302
|
+
if @ratio
|
303
|
+
str << "\n" << " Ratio: #{@ratio}"
|
304
|
+
end
|
305
|
+
end
|
306
|
+
}
|
307
|
+
end
|
308
|
+
end # class Assertion::FloatEqual
|
309
|
+
|
310
|
+
class Identity < Base
|
311
|
+
def initialize(mode, *args, &block)
|
312
|
+
super
|
313
|
+
@obj1, @obj2 = two_arguments(args)
|
314
|
+
no_block_allowed
|
315
|
+
end
|
316
|
+
def run
|
317
|
+
@obj1.object_id == @obj2.object_id
|
318
|
+
end
|
319
|
+
def message
|
320
|
+
String.new.tap { |str|
|
321
|
+
case @mode
|
322
|
+
when :assert
|
323
|
+
str << Col["Identity test failed -- the two objects are NOT the same"].yb
|
324
|
+
str << Col["\n Object 1 id: ", @obj1.object_id].fmt('yb,rb')
|
325
|
+
str << Col["\n Object 2 id: ", @obj2.object_id].fmt('yb,rb')
|
326
|
+
when :negate
|
327
|
+
str << Col["Identity test failed -- the two objects ARE the same"].yb
|
328
|
+
str << Col["\n Object id: ", @obj1.object_id].fmt('yb,rb')
|
329
|
+
end
|
330
|
+
}
|
331
|
+
end
|
332
|
+
end # class Assertion::Identity
|
333
|
+
|
334
|
+
class ExpectError < Base
|
335
|
+
def initialize(mode, *args, &block)
|
336
|
+
super
|
337
|
+
@exceptions = args.empty? ? [StandardError] : args
|
338
|
+
unless @exceptions.all? { |klass| klass.is_a? Class }
|
339
|
+
raise AssertionSpecificationError, "Invalid arguments: must all be classes"
|
340
|
+
end
|
341
|
+
@block = block_required(block)
|
342
|
+
end
|
343
|
+
def run
|
344
|
+
# Return true if the block raises an exception, false otherwise.
|
345
|
+
# Only the exceptions specified in @exceptions will be caught.
|
346
|
+
begin
|
347
|
+
@block.call
|
348
|
+
return false
|
349
|
+
rescue ::Exception => e
|
350
|
+
if @exceptions.any? { |klass| e.is_a? klass }
|
351
|
+
@exception_class = e.class
|
352
|
+
Whitestone.exception = e
|
353
|
+
return true
|
354
|
+
else
|
355
|
+
raise e # It's not one of the exceptions we wanted; re-raise it.
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
def message
|
360
|
+
_or_ = Col[' or '].yb
|
361
|
+
kinds_str = @exceptions.map { |ex| Col[ex].rb }.join(_or_)
|
362
|
+
klass = @exception_class
|
363
|
+
case @mode
|
364
|
+
when :assert
|
365
|
+
Col["Expected block to raise ", kinds_str, "; nothing raised"].fmt 'yb,_,yb'
|
366
|
+
when :negate
|
367
|
+
Col[ "Expected block NOT to raise ", kinds_str, "; ", klass, " raised"].
|
368
|
+
fmt :yb, :_, :yb, :rb, :yb
|
369
|
+
end
|
370
|
+
end
|
371
|
+
end # class Assertion::Exception
|
372
|
+
|
373
|
+
class Catch < Base
|
374
|
+
TOKEN = Object.new
|
375
|
+
def initialize(mode, *args, &block)
|
376
|
+
super
|
377
|
+
@symbol = one_argument(args)
|
378
|
+
@block = block_required(block)
|
379
|
+
end
|
380
|
+
def run
|
381
|
+
return_value =
|
382
|
+
catch(@symbol) do
|
383
|
+
begin
|
384
|
+
@block.call
|
385
|
+
rescue => e
|
386
|
+
raise e unless e.message =~ /\Auncaught throw (`.*?'|:.*)\z/
|
387
|
+
# ^ We don't want this exception to escape and terminate our
|
388
|
+
# tests. TODO: make sure I understand this and agree with
|
389
|
+
# what it does. Should we report an uncaught throw?
|
390
|
+
end
|
391
|
+
TOKEN # Special object to say we reached the end of the block,
|
392
|
+
# therefore nothing was thrown.
|
393
|
+
end
|
394
|
+
if return_value == TOKEN
|
395
|
+
# The symbol we were expecting was not thrown, so this test failed.
|
396
|
+
Whitestone.caught_value = nil
|
397
|
+
return false
|
398
|
+
else
|
399
|
+
Whitestone.caught_value = return_value
|
400
|
+
return true
|
401
|
+
end
|
402
|
+
end
|
403
|
+
def message
|
404
|
+
symbol = @symbol.to_sym.inspect
|
405
|
+
case @mode
|
406
|
+
when :assert
|
407
|
+
Col["Expected block to throw ", symbol, "; it didn't"].fmt 'yb,rb,yb'
|
408
|
+
when :negate
|
409
|
+
Col["Expected block NOT to throw ", symbol, "; it did"].fmt 'yb,rb,yb'
|
410
|
+
end
|
411
|
+
end
|
412
|
+
end # class Assertion::Catch
|
413
|
+
|
414
|
+
# ----------------------------------------------------------------------- #
|
415
|
+
|
416
|
+
end # module Assertion
|
417
|
+
end # module Whitestone
|
418
|
+
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Provides painless, automatic configuration of Whitestone.
|
2
|
+
#
|
3
|
+
# Simply require() this file and Whitestone will be available for use anywhere
|
4
|
+
# in your program and will execute all tests before your program exits.
|
5
|
+
|
6
|
+
require 'whitestone'
|
7
|
+
|
8
|
+
class Object
|
9
|
+
include Whitestone
|
10
|
+
end
|
11
|
+
|
12
|
+
at_exit do
|
13
|
+
Whitestone.run
|
14
|
+
|
15
|
+
# reflect number of failures in exit status
|
16
|
+
stats = Whitestone.stats
|
17
|
+
fails = stats[:fail] + stats[:error]
|
18
|
+
|
19
|
+
exit [fails, 255].min
|
20
|
+
end
|
@@ -0,0 +1,252 @@
|
|
1
|
+
module Whitestone
|
2
|
+
|
3
|
+
# ==============================================================section==== #
|
4
|
+
# #
|
5
|
+
# Custom assertions #
|
6
|
+
# #
|
7
|
+
# Assertion::Custom < Assertion::Base (below) #
|
8
|
+
# - responsible for creating and running custom assertions #
|
9
|
+
# #
|
10
|
+
# Assertion::Custom::CustomTestContext (next section) #
|
11
|
+
# - provides a context in which custom assertions can run #
|
12
|
+
# #
|
13
|
+
# ========================================================================= #
|
14
|
+
|
15
|
+
#
|
16
|
+
# Whitestone::Assertion::Custom -- custom assertions
|
17
|
+
#
|
18
|
+
# This class is responsible for _creating_ and _running_ custom assertions.
|
19
|
+
#
|
20
|
+
# Creating:
|
21
|
+
# Whitestone.custom :circle, {
|
22
|
+
# :description => "Circle equality",
|
23
|
+
# :parameters => [ [:circle, Circle], [:values, Array] ],
|
24
|
+
# :run => lambda {
|
25
|
+
# x, y, r, label = values
|
26
|
+
# test('x') { Ft circle.centre.x, x }
|
27
|
+
# test('y') { Ft circle.centre.y, y }
|
28
|
+
# test('r') { Ft circle.radius, r }
|
29
|
+
# test('label') { Eq circle.label, Label[label] }
|
30
|
+
# }
|
31
|
+
# }
|
32
|
+
# * (Whitestone.custom passes its arguments straight through to Custom.define,
|
33
|
+
# which is surprisingly a very lightweight method.)
|
34
|
+
#
|
35
|
+
# Running:
|
36
|
+
# T :circle, circle, [4,1, 10, nil]
|
37
|
+
# --> assertion = Custom.new(:custom, :assert, :circle, circle, [4,1, 10, nil]
|
38
|
+
# --> assertion.run
|
39
|
+
#
|
40
|
+
# Custom _is_ an assertion (Assertion::Base) object, just like True,
|
41
|
+
# Equality, Catch, etc. It follows the same methods and life-cycle:
|
42
|
+
# * initialize: check arguments are sound; store instance variables for later
|
43
|
+
# * run: use the instance variables to perform the necessary assertion
|
44
|
+
# * message: return a message to be displayed upon failure
|
45
|
+
#
|
46
|
+
# _run_ is a lot more complicated than a normal assertion because all the
|
47
|
+
# logic is in the Config object (compare Equality#run: {@object == @expected}).
|
48
|
+
# The block that is specified (the _lambda_ above) needs to be run in a
|
49
|
+
# special context for those {test} calls to work.
|
50
|
+
#
|
51
|
+
class Assertion::Custom < Assertion::Base
|
52
|
+
|
53
|
+
# Whitestone::Assertion::Custom::Config
|
54
|
+
#
|
55
|
+
# The Config object is what makes each custom assertion different.
|
56
|
+
# For example (same as the example given in Custom):
|
57
|
+
# name = :circle
|
58
|
+
# description = "Circle equality"
|
59
|
+
# parameters = [ [:circle, Circle], [:values, Array] ]
|
60
|
+
# run_block = lambda { ... }
|
61
|
+
#
|
62
|
+
class Config
|
63
|
+
attr_reader :name, :description, :parameters, :run_block
|
64
|
+
def initialize(name, hash)
|
65
|
+
@name = name
|
66
|
+
@description = hash[:description]
|
67
|
+
@parameters = hash[:parameters]
|
68
|
+
@run_block = hash[:run]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
@@config = Hash.new # { :circle => Config.new(...), :square => Config.new(...) }
|
73
|
+
|
74
|
+
# Custom.define
|
75
|
+
#
|
76
|
+
# Defines a new custom assertion -- just stores the configuration away for
|
77
|
+
# retrieval when the assertion is run.
|
78
|
+
def self.define(name, definition)
|
79
|
+
@@config[name] = Config.new(name, definition)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Custom#initialize
|
83
|
+
#
|
84
|
+
# Retrieves the config for the named custom assertion and checks the
|
85
|
+
# arguments against the configured parameters.
|
86
|
+
#
|
87
|
+
# Sets up a context (CustomTestContext) for running the assertion when #run
|
88
|
+
# is called.
|
89
|
+
def initialize(mode, *args, &block)
|
90
|
+
name = args.shift
|
91
|
+
super(mode, *args, &block)
|
92
|
+
no_block_allowed
|
93
|
+
@config = @@config[name]
|
94
|
+
if @config.nil?
|
95
|
+
message = "Non-existent custom assertion: #{name.inspect}"
|
96
|
+
raise AssertionSpecificationError, message
|
97
|
+
end
|
98
|
+
check_args_against_parameters(args)
|
99
|
+
@context = CustomTestContext.new(@config.parameters, args)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Custom#run
|
103
|
+
#
|
104
|
+
# Returns true or false for pass or fail, just like other assertions.
|
105
|
+
#
|
106
|
+
# The Config object provides the block to run, while @context provides the
|
107
|
+
# context in which to run it.
|
108
|
+
#
|
109
|
+
# We trap FailureOccurred errors because as a _custom_ assertion we need to
|
110
|
+
# take responsibility for the errors, and wrap some information around the
|
111
|
+
# error message.
|
112
|
+
def run
|
113
|
+
test_code = @config.run_block
|
114
|
+
@context.instance_eval &test_code
|
115
|
+
# ^^^ This gives the test code access to the 'test' method that is so
|
116
|
+
# important for running a custom assertion.
|
117
|
+
# See the notes on CustomTestContext for an example.
|
118
|
+
return true # the custom test passed
|
119
|
+
rescue FailureOccurred => f
|
120
|
+
# We are here because an assertion failed. That means _this_ (custom)
|
121
|
+
# assertion has failed. We need to build an error message and raise
|
122
|
+
# FailureOccurred ourselves.
|
123
|
+
@message = String.new.tap { |str|
|
124
|
+
str << Col["#{@config.description} test failed: "].yb
|
125
|
+
str << Col[@context.context_label].cb
|
126
|
+
str << Col[" (details below)\n", f.message.___indent(4)].fmt(:yb, :yb)
|
127
|
+
}
|
128
|
+
return false
|
129
|
+
rescue AssertionSpecificationError => e
|
130
|
+
# While running the test block, we got an AssertionSpecificationError.
|
131
|
+
# This probably means some bad data was put in, like
|
132
|
+
# T :circle, c, [4,1, "radius", nil]
|
133
|
+
# (The radius needs to be a number, not a string.)
|
134
|
+
# We will still raise the AssertionSpecificationError but we want it to
|
135
|
+
# look like it comes from the _custom_ assertion, not the _primitive_
|
136
|
+
# one. Essentially, we are acting like it's a failure: constructing the
|
137
|
+
# message that includes the context label (in this case, 'r' for
|
138
|
+
# radius).
|
139
|
+
message = String.new.tap { |str|
|
140
|
+
str << Col["#{@config.description} test -- error: "].yb
|
141
|
+
str << Col[@context.context_label].cb
|
142
|
+
str << Col[" details below\n", e.message.___indent(4)].fmt(:yb, :yb)
|
143
|
+
}
|
144
|
+
raise AssertionSpecificationError, message
|
145
|
+
end
|
146
|
+
|
147
|
+
# Custom#message
|
148
|
+
#
|
149
|
+
# If a failure occurred, a failure message was prepared when the exception
|
150
|
+
# was caught in #run.
|
151
|
+
def message
|
152
|
+
@message
|
153
|
+
end
|
154
|
+
|
155
|
+
# e.g. parameters = [ [:circle, Circle], [:values, Array] ]
|
156
|
+
# args = [ some_circle, [3,1,10,:X] ]
|
157
|
+
# That's a match.
|
158
|
+
# For this method, we're not interested in the names of the parameters.
|
159
|
+
def check_args_against_parameters(args)
|
160
|
+
parameters = @config.parameters
|
161
|
+
parameter_types = parameters.map { |name, type| type }
|
162
|
+
if args.size != parameter_types.size
|
163
|
+
msg = "Expect #{parameter_types.size} arguments after " \
|
164
|
+
"#{@config.name.inspect}; got #{args.size}"
|
165
|
+
raise AssertionSpecificationError, msg
|
166
|
+
end
|
167
|
+
args.zip(parameter_types).each do |arg, type|
|
168
|
+
unless arg.is_a? type
|
169
|
+
msg = "Argument error: expected #{type}; "\
|
170
|
+
"got #{arg.inspect} (#{arg.class})"
|
171
|
+
raise AssertionSpecificationError, msg
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
private :check_args_against_parameters
|
176
|
+
|
177
|
+
end # class Assertion::Custom
|
178
|
+
|
179
|
+
|
180
|
+
# ------------------------------------------------------------section---- #
|
181
|
+
# #
|
182
|
+
# CustomTestContext #
|
183
|
+
# #
|
184
|
+
# ----------------------------------------------------------------------- #
|
185
|
+
|
186
|
+
|
187
|
+
##
|
188
|
+
# CustomTestContext -- an environment in which a custom text can run
|
189
|
+
# and have access to its parameters.
|
190
|
+
#
|
191
|
+
# Example usage (test writer's point of view):
|
192
|
+
#
|
193
|
+
# Whitestone.custom :circle, {
|
194
|
+
# :description => "Circle equality",
|
195
|
+
# :parameters => [ [:circle, Circle], [:values, Array] ],
|
196
|
+
# :run => lambda {
|
197
|
+
# x, y, r, label = values
|
198
|
+
# test('x') { Ft x, circle.centre.x }
|
199
|
+
# test('y') { Ft y, circle.centre.y }
|
200
|
+
# test('r') { Ft r, circle.radius }
|
201
|
+
# test('label') { Eq Label[label], circle.label }
|
202
|
+
# }
|
203
|
+
# }
|
204
|
+
#
|
205
|
+
# That _lambda_ on Line 4 gets evaluated in a CustomTestContext object, which
|
206
|
+
# gives it access to the method 'test' and the parameters 'circle' and
|
207
|
+
# 'values', which are dynamically-defined methods on the context object.
|
208
|
+
#
|
209
|
+
# Example usage (CustomTestContext user's point of view):
|
210
|
+
#
|
211
|
+
# context = CustomTestContext.new(parameters, arguments)
|
212
|
+
# context.instance_eval(block)
|
213
|
+
#
|
214
|
+
class Assertion::Custom::CustomTestContext
|
215
|
+
# The label associated with the current assertion (see #test).
|
216
|
+
attr_reader :context_label
|
217
|
+
|
218
|
+
# Example:
|
219
|
+
# parameters: [ [:circle, Circle], [:values, Array] ],
|
220
|
+
# values: [ circle_object, [4,1,5,:X] ]
|
221
|
+
# Result of calling method:
|
222
|
+
# def circle() circle_object end
|
223
|
+
# def values() [4,1,5,:X] end
|
224
|
+
# Effect:
|
225
|
+
# * code run in this context (i.e. with this object as 'self') can access
|
226
|
+
# the methods 'circle' and 'values', as well as the method 'test'.
|
227
|
+
def initialize(parameters, values)
|
228
|
+
parameters = parameters.map { |name, type| name }
|
229
|
+
parameters.zip(values).each do |param, value|
|
230
|
+
metaclass = class << self; self; end
|
231
|
+
metaclass.module_eval do
|
232
|
+
define_method(param) { value }
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
# See the example usage above. The block is expected to have a single
|
238
|
+
# assertion in it (but of course we can't control or even check that).
|
239
|
+
#
|
240
|
+
# If the assertion fails, we use the label as part of the error message
|
241
|
+
# so it's easy to see what went wrong.
|
242
|
+
#
|
243
|
+
# Therefore we save the label so the test runner that is using this
|
244
|
+
# context can access it. In the example above, the value of 'context_label'
|
245
|
+
# at different times throughout the lambda is 'x', 'y', 'r' and 'label'.
|
246
|
+
def test(label, &assertion)
|
247
|
+
@context_label = label
|
248
|
+
assertion.call
|
249
|
+
end
|
250
|
+
end # class Assertion::Custom::CustomTestContext
|
251
|
+
|
252
|
+
end # module Whitestone
|