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,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
|