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.
@@ -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