whitestone 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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