handshake 0.1.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,737 @@
1
+ # handshake.rb
2
+ # Copyright (c) 2007 Brian Guthrie
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require 'handshake/inheritable_attributes'
24
+ require 'test/unit/assertions'
25
+
26
+ class Class # :nodoc:
27
+ # Redefines each of the given methods as a call to self#send. This assumes
28
+ # that self#send knows what do with them.
29
+ def proxy_self(*meths)
30
+ meths.each do |meth|
31
+ class_eval <<-EOS
32
+ def #{meth}(*args, &block)
33
+ self.send(:#{meth}, *args, &block)
34
+ end
35
+ EOS
36
+ end
37
+ nil
38
+ end
39
+ end
40
+
41
+ # A module for defining class and method contracts (as in design-by-contract
42
+ # programming). To use it in your code, include this module in a class.
43
+ # Note that when you do so, that class's +new+ method will be replaced.
44
+ # There are three different types of contracts you can specify on a class.
45
+ # See Handshake::ClassMethods for more documentation.
46
+ module Handshake
47
+
48
+ # Catches any thrown :contract exception raised within the given block,
49
+ # appends the given message to the violation message, and re-raises the
50
+ # exception.
51
+ def Handshake.catch_contract(mesg=nil, &block) # :nodoc:
52
+ violation = catch(:contract, &block)
53
+ if violation.is_a?(Exception)
54
+ # Re-raise the violation with the given message, ensuring that the
55
+ # callback stack begins with the caller of this method rather than
56
+ # this method.
57
+ message = ( mesg.nil? ? "" : ( mesg + ": " ) ) + violation.message
58
+ raise violation.class, message, caller
59
+ end
60
+ end
61
+
62
+ # When Handshake is included in a class, that class's +new+ method is
63
+ # overridden to provide custom functionality. A proxy object, returned
64
+ # in place of the real object, filters all external method calls through
65
+ # any contracts that have been defined.
66
+ # <b>N.B.:<b> Handshake is designed to act as a barrier between an object and
67
+ # its callers. However, anything that takes place within that barrier
68
+ # is not checked. This means that Handshake is, at the moment, unable
69
+ # to enforce contracts on methods called only internally, notably private
70
+ # methods.
71
+ def Handshake.included(base)
72
+ base.extend(ClassMethods)
73
+ base.extend(ClauseMethods)
74
+
75
+ base.send(:include, Test::Unit::Assertions)
76
+ base.send(:include, Handshake::InstanceMethods)
77
+
78
+ base.class_inheritable_array :invariants
79
+ base.write_inheritable_array :invariants, []
80
+
81
+ base.class_inheritable_hash :method_contracts
82
+ base.write_inheritable_hash :method_contracts, {}
83
+
84
+ class << base
85
+ alias :instantiate :new
86
+ # Override the class-level new method of every class that includes
87
+ # Contract and cause it to return a proxy object for the original.
88
+ def new(*args, &block)
89
+ if @non_instantiable
90
+ raise ContractViolation, "This class has been marked as abstract and cannot be instantiated."
91
+ end
92
+ o = nil
93
+
94
+ # Special case: check invariants for constructor.
95
+ Handshake.catch_contract("Contract violated in call to constructor of class #{self}") do
96
+ if contract_defined? :initialize
97
+ method_contracts[:initialize].check_accepts!(*args, &block)
98
+ end
99
+ end
100
+
101
+ o = self.instantiate(*args, &block)
102
+
103
+ Handshake.catch_contract("Invariant violated by constructor of class #{self}") do
104
+ o.check_invariants!
105
+ end
106
+
107
+ raise ContractError, "Could not instantiate object" if o.nil?
108
+ Proxy.new( o )
109
+ end
110
+ end
111
+ end
112
+
113
+ # This module contains methods that are mixed into any class that includes
114
+ # Handshake. They allow you to define constraints on that class and its
115
+ # methods. Subclasses will inherit the contracts and invariants of its
116
+ # superclass, but Handshake contracts currently can't be mixed-in via a
117
+ # module.
118
+ #
119
+ # This module defines three kinds of contracts: class invariants, method
120
+ # signature constraints, and more general method pre- and post-conditions.
121
+ # Invariants accept a block which should return a boolean. Pre- and post-
122
+ # conditions expect you to use assertions (all of Test::Unit's standard
123
+ # assertions are available) and will pass unless an assertion fails.
124
+ # Method signature contracts map inputs clauses to output clauses. A
125
+ # "clause" is defined as any object that implements the === method.
126
+ #
127
+ # All method contracts are defined on the method defined immediately after
128
+ # their declaration unless a method name is specified. For example,
129
+ #
130
+ # contract :foo, String => Integer
131
+ #
132
+ # is equivalent to
133
+ #
134
+ # contract String => Integer
135
+ # def foo ...
136
+ #
137
+ # in the sense that the contract will be applied to all calls to the "foo"
138
+ # method either way.
139
+ #
140
+ # ===Method signature contracts
141
+ # contract String => Integer
142
+ # contract [ String, 1..5, [ Integer ], Block ] => [ String, String ]
143
+ # contract clause("must equal 'foo'") { |o| o == "foo" } => anything
144
+ #
145
+ # A method signature contract is defined as a mapping from valid inputs to
146
+ # to valid outputs. A clause here is any object which implements the
147
+ # <tt>===</tt> method. Classes, ranges, regexes, and other useful objects
148
+ # are thus all valid values for a method signature contract.
149
+ #
150
+ # Multiple arguments are specified as an array. To specify that a method
151
+ # accepts varargs, define a nested array as the last or second-to-last
152
+ # item in the array. To specify that a method accepts a block, place
153
+ # the Block constant as the last item in the array. Expect this to change
154
+ # in the future to allow for block contracts.
155
+ #
156
+ # New clauses may be created easily with the Handshake::ClauseMethods#clause
157
+ # method. Handshake::ClauseMethods also provides a number of useful contract
158
+ # combinators for specifying rich input and output contracts.
159
+ #
160
+ # ===Contract-checked accessors
161
+ # contract_reader :foo => String, :bar => Integer
162
+ # contract_writer ...
163
+ # contract_accessor ...
164
+ # Defines contract-checked accessors. Method names and clauses are specified
165
+ # in a hash. Hash values are any valid clause.
166
+ #
167
+ # ===Invariants
168
+ # invariant(optional_message) { returns true }
169
+ # Aliased as +always+. Has access to instance variables and methods of object
170
+ # but calls to same are unchecked.
171
+ #
172
+ # ===Pre/post-conditions
173
+ # before(optional_message) { |arg1, ...| assert condition }
174
+ # after(optional_message) { |arg1, ..., returned| assert condition }
175
+ # around(optional_message) { |arg1, ...| assert condition }
176
+ # Check a set of conditions, using assertions, before and after method
177
+ # invocation. +before+ and +after+ are aliased as +requires+ and +ensures+
178
+ # respectively. +around+ currently throws a block argument warning; this
179
+ # should be fixed soon. Same scope rules as invariants, so you can check
180
+ # instance variables and local methods. All Test::Unit::Assertions are available
181
+ # for use, but any such AssertionFailed errors encountered are re-raised
182
+ # by Handshake as Handshake::AssertionFailed errors to avoid confusion
183
+ # with test case execution.
184
+ #
185
+ # ===Abstract class decorator
186
+ # class SuperDuperContract
187
+ # include Handshake; abstract!
188
+ # ...
189
+ # end
190
+ #
191
+ # To define a class as non-instantiable and have Handshake raise a
192
+ # ContractViolation if a caller attempts to do so, call <tt>abstract!</tt>
193
+ # at the top of the class definition. This attribute is not inherited
194
+ # by subclasses, but is useful if you would like to define a pure-contract
195
+ # superclass that isn't intended to be instantiated directly.
196
+ module ClassMethods
197
+ # Define this class as non-instantiable. Subclasses do not inherit this
198
+ # attribute.
199
+ def abstract!
200
+ @non_instantiable = true
201
+ end
202
+
203
+ # Specify an invariant, with a block and an optional error message.
204
+ def invariant(mesg=nil, &block) # :yields:
205
+ write_inheritable_array(:invariants, [ Invariant.new(mesg, &block) ] )
206
+ nil
207
+ end
208
+ alias :always :invariant
209
+
210
+ # In order for contract clauses to work in conjunction with Handshake
211
+ # proxy objects, the === method must be redefined in terms of is_a?.
212
+ def ===(other)
213
+ other.is_a? self
214
+ end
215
+
216
+ # Specify an argument contract, with argument clauses on one side of the
217
+ # hash arrow and returned values on the other. Each clause must implement
218
+ # the === method or have been created with the assert method. This
219
+ # method should generally not be called directly.
220
+ def contract(meth_or_hash, contract_hash=nil)
221
+ if meth_or_hash.is_a? Hash
222
+ defer :contract, meth_or_hash
223
+ else
224
+ define_contract(meth_or_hash, contract_hash)
225
+ end
226
+ end
227
+
228
+ # Specify a precondition.
229
+ def before(meth_or_mesg=nil, mesg=nil, &block)
230
+ condition(:before, meth_or_mesg, mesg, &block)
231
+ end
232
+ alias :requires :before
233
+
234
+ # Specify a postcondition.
235
+ def after(meth_or_mesg=nil, mesg=nil, &block)
236
+ condition(:after, meth_or_mesg, mesg, &block)
237
+ end
238
+ alias :ensures :after
239
+
240
+ # Specify a bothcondition.
241
+ def around(meth_or_mesg=nil, mesg=nil, &block)
242
+ condition(:around, meth_or_mesg, mesg, &block)
243
+ end
244
+
245
+ # Returns the MethodContract for the given method name. Side effect:
246
+ # creates one if none defined.
247
+ def contract_for(method)
248
+ if contract_defined?(method)
249
+ method_contracts[method]
250
+ else
251
+ contract = MethodContract.new("#{self}##{method}")
252
+ write_inheritable_hash :method_contracts, { method => contract }
253
+ contract
254
+ end
255
+ end
256
+
257
+ # Returns true if a contract is defined for the named method.
258
+ def contract_defined?(method)
259
+ method_contracts.has_key?(method)
260
+ end
261
+
262
+ # Defines contract-checked attribute readers with the given hash of method
263
+ # name to clause.
264
+ def contract_reader(meth_to_clause)
265
+ attr_reader *(meth_to_clause.keys)
266
+ meth_to_clause.each do |meth, cls|
267
+ contract meth, nil => cls
268
+ end
269
+ end
270
+
271
+ # Defines contract-checked attribute writers with the given hash of method
272
+ # name to clause.
273
+ def contract_writer(meth_to_clause)
274
+ attr_writer *(meth_to_clause.keys)
275
+ meth_to_clause.each do |meth, cls|
276
+ contract "#{meth}=".to_sym, cls => anything
277
+ end
278
+ end
279
+
280
+ # Defines contract-checked attribute accessors for the given hash of method
281
+ # name to clause.
282
+ def contract_accessor(meth_to_clause)
283
+ contract_reader meth_to_clause
284
+ contract_writer meth_to_clause
285
+ end
286
+
287
+ # Callback from method add event. If a previous method contract
288
+ # declaration was deferred, complete it now with the name of the newly-
289
+ # added method.
290
+ def method_added(meth_name)
291
+ @deferred ||= {}
292
+ unless @deferred.empty?
293
+ @deferred.each do |k, v|
294
+ case k
295
+ when :before, :after, :around
296
+ define_condition meth_name, k, v
297
+ when :contract
298
+ define_contract meth_name, v
299
+ end
300
+ end
301
+ @deferred.clear
302
+ end
303
+ end
304
+
305
+ private
306
+
307
+ def define_contract(method, contract_hash)
308
+ raise ArgumentError unless contract_hash.length == 1
309
+ accepts, returns = [ contract_hash.keys.first, contract_hash.values.first ].map {|v| arrayify v}
310
+ contract = contract_for(method).dup
311
+ contract.accepts = accepts
312
+ contract.returns = returns
313
+ write_inheritable_hash :method_contracts, { method => contract }
314
+ end
315
+
316
+ def define_condition(method, type, condition)
317
+ defined_before = [ :before, :around ].include? type
318
+ defined_after = [ :after, :around ].include? type
319
+ contract = contract_for(method).dup
320
+ contract.preconditions << condition if defined_before
321
+ contract.postconditions << condition if defined_after
322
+ write_inheritable_hash :method_contracts, { method => contract }
323
+ end
324
+
325
+ def condition(type, meth_or_mesg=nil, mesg=nil, &block)
326
+ method_specified = meth_or_mesg.is_a?(Symbol)
327
+ message = method_specified ? mesg : meth_or_mesg
328
+ condition = MethodCondition.new(message, &block)
329
+ if method_specified
330
+ define_condition(type, meth_or_mesg, condition)
331
+ else
332
+ defer type, condition
333
+ end
334
+ end
335
+
336
+ def arrayify(value_or_array)
337
+ value_or_array.is_a?(Array) ? value_or_array : [ value_or_array ]
338
+ end
339
+
340
+ def defer(type, value)
341
+ ( @deferred ||= {} )[type] = value
342
+ end
343
+
344
+ end
345
+
346
+ module InstanceMethods
347
+ # Checks the invariants defined on this class against +self+, raising a
348
+ # ContractViolation if any of them fail.
349
+ def check_invariants!
350
+ self.class.invariants.each do |invar|
351
+ unless invar.holds?(self)
352
+ mesg = invar.mesg || "Invariant check failed"
353
+ throw :contract, ContractViolation.new(mesg)
354
+ end
355
+ end
356
+ end
357
+ end
358
+
359
+ # Class representing method contracts. Not for external use.
360
+ class MethodContract # :nodoc:
361
+ attr_accessor :preconditions, :postconditions, :returns
362
+ attr_reader :accepts
363
+
364
+ def initialize(method_name)
365
+ @method_name = method_name
366
+ @preconditions, @postconditions, @accepts, @returns = [], [], [], []
367
+ end
368
+
369
+ # Returns true only if this MethodContract has been set up to check
370
+ # for one or more contract conditions.
371
+ def defined?
372
+ [ @preconditions, @postconditions, @accepts, @returns ].all? do |ary|
373
+ ary.empty?
374
+ end
375
+ end
376
+
377
+ # Checks the postconditions of this contract against the given object
378
+ # and return values. Any assertions thrown are re-raised as
379
+ # Handshake::AssertionViolation errors.
380
+ def check_post!(o, *args)
381
+ check_conditions!(o, args, @postconditions)
382
+ end
383
+
384
+ # Checks the preconditions of this contract against the given object
385
+ # and arugment values. Any assertions thrown are re-raised as
386
+ # Handshake::AssertionFailed errors.
387
+ def check_pre!(o, *args)
388
+ check_conditions!(o, args, @preconditions)
389
+ end
390
+
391
+ def check_conditions!(o, args, conditions)
392
+ conditions.each do |condition|
393
+ o.class.instance_eval do
394
+ define_method(:bound_condition_passes?, &(condition.block))
395
+ end
396
+ begin
397
+ o.bound_condition_passes?(*args)
398
+ rescue Test::Unit::AssertionFailedError => afe
399
+ throw :contract, AssertionFailed.new(afe.message)
400
+ rescue Exception => e
401
+ throw :contract, e
402
+ end
403
+ o.class.send(:remove_method, :bound_condition_passes?)
404
+ end
405
+ end
406
+
407
+ def accepts=(args)
408
+ # If the last argument is a Block, handle it as a special case. We
409
+ # do this to ensure that there's no conflict with any real arguments
410
+ # which may accept Procs.
411
+ @block = args.pop if args.last == Block
412
+
413
+ if args.find_all {|o| o.is_a? Array}.length > 1
414
+ raise ContractError, "Cannot define more than one expected variable argument"
415
+ end
416
+ @accepts = args
417
+ end
418
+
419
+ def expects_block?
420
+ not @block.nil?
421
+ end
422
+
423
+ def accepts_varargs?
424
+ @accepts.last.is_a? Array
425
+ end
426
+
427
+ def check_accepts!(*args, &block)
428
+ @accepts.each_with_index do |expected_arg, i|
429
+ # Varargs: consume all remaining arguments.
430
+ if expected_arg.is_a? Array
431
+ check_varargs!(args, expected_arg.first, i) and break
432
+ end
433
+ check_equivalence!(args[i], expected_arg)
434
+ end
435
+ if expects_block?
436
+ check_equivalence!(block, @block)
437
+ end
438
+ end
439
+
440
+ def check_returns!(*args)
441
+ @returns.each_with_index do |expected, i|
442
+ check_equivalence!(args[i], expected)
443
+ end
444
+ end
445
+
446
+ def check_varargs!(given_args, expected, index)
447
+ given_args[index..-1].each {|arg| check_equivalence!(arg, expected)}
448
+ end
449
+
450
+ def check_equivalence!(given, expected)
451
+ unless expected === given
452
+ mesg = "expected #{expected.inspect}, received #{given.inspect}"
453
+ throw :contract, ContractViolation.new(mesg)
454
+ end
455
+ end
456
+ end
457
+
458
+ # Specifies a condition on a method. Not for external use.
459
+ class MethodCondition # :nodoc:
460
+ attr_accessor :message, :block
461
+ def initialize(message=nil, &block)
462
+ @message, @block = message, block
463
+ end
464
+ end
465
+
466
+ # This class defines a class invariant, which has a block and an optional
467
+ # method. Not for external use.
468
+ class Invariant # :nodoc:
469
+ def initialize(mesg=nil, &block)
470
+ @mesg = mesg
471
+ @block = block
472
+ end
473
+ # Any -> Boolean
474
+ # Evaluates this class's block in the binding of the given object.
475
+ def holds?(o)
476
+ block = @block
477
+ o.instance_eval &block
478
+ end
479
+ def mesg
480
+ @mesg || "Invariant check failed"
481
+ end
482
+ end
483
+
484
+ # This class filters all method calls to its proxied object through any
485
+ # contracts defined on that object's class. It attempts to look and act
486
+ # like its proxied object for all intents and purposes, although it notably
487
+ # does not proxy +__id__+, +__send__+, or +class+.
488
+ class Proxy
489
+ NOT_PROXIED = [ "__id__", "__send__" ]
490
+ SELF_PROXIED = Object.instance_methods - NOT_PROXIED
491
+
492
+ # Redefine language-level methods inherited from Object, ensuring that
493
+ # they are forwarded to the proxy object.
494
+ proxy_self *SELF_PROXIED
495
+
496
+ # Accepts an object to be proxied.
497
+ def initialize(proxied)
498
+ @proxied = proxied
499
+ end
500
+
501
+ # Returns the wrapped object. Method calls made against this object
502
+ # will not be checked.
503
+ def unchecked!
504
+ @proxied
505
+ end
506
+
507
+ # Returns the class of the proxied object.
508
+ def proxied_class
509
+ @proxied.class
510
+ end
511
+
512
+ # Override the send method, and alias method_missing to same.
513
+ # This method intercepts all method calls and runs them through the
514
+ # contract filter. The order of contract checks is as follows:
515
+ # * Before: invariants, method signature, precondition
516
+ # * Method is called
517
+ # * After: method signature, postcondition, invariants
518
+ def send(meth_name, *args, &block)
519
+ meth_string = "#{@proxied.class}##{meth_name}"
520
+ contract = @proxied.class.contract_for(meth_name)
521
+ return_val = nil
522
+ # Use throw/catch rather than raise/rescue in order to pull exceptions
523
+ # once and only once from within the stack trace.
524
+ Handshake.catch_contract("Contract violated in call to #{meth_string}") do
525
+ @proxied.check_invariants!
526
+ contract.check_accepts! *args, &block
527
+ contract.check_pre! @proxied, *args
528
+ end
529
+
530
+ # make actual call
531
+ return_val = @proxied.send meth_name, *args, &block
532
+
533
+ Handshake.catch_contract("Contract violated by #{meth_string}") do
534
+ contract.check_returns! return_val
535
+ contract.check_post! @proxied, *(args << return_val)
536
+ @proxied.check_invariants!
537
+ end
538
+
539
+ return return_val
540
+ end
541
+ alias :method_missing :send
542
+
543
+
544
+ end
545
+
546
+ # For block-checking, we need a class which is_a? Proc for instance checking
547
+ # purposes but isn't the same so as not to prevent the user from passing in
548
+ # explicitly defined procs as arguments. Expect this to be replaced at
549
+ # some point in the future with a +block_contract+ construct.
550
+ class Block
551
+ def Block.===(o); Proc === o; end
552
+ end
553
+
554
+ # Transforms the given block into a contract clause. Clause fails if
555
+ # the given block returns false or nil, passes otherwise. See
556
+ # Handshake::ClauseMethods for more examples of its use. This object may
557
+ # be instantiated directly but calling Handshake::ClauseMethods#clause is
558
+ # generally preferable.
559
+ class Clause
560
+ # Defines a new Clause object with a block and a message.
561
+ # The block should return a boolean value. The message is optional but
562
+ # strongly recommended for human-readable contract violation errors.
563
+ def initialize(mesg=nil, &block) # :yields: argument
564
+ @mesg, @block = mesg, block
565
+ end
566
+ # Returns true if the block passed to the constructor returns true when
567
+ # called with the given argument.
568
+ def ===(o)
569
+ @block.call(o)
570
+ end
571
+ # Returns the message defined for this Clause, or "undocumented clause"
572
+ # if none is defined.
573
+ def inspect; @mesg || "undocumented clause"; end
574
+ def ==(other)
575
+ other.class == self.class && other.mesg == @mesg && other.block == @block
576
+ end
577
+ end
578
+
579
+ # A collection of methods for defining constraints on method arguments.
580
+ module ClauseMethods
581
+ # Passes if the given block returns true when passed the argument.
582
+ def clause(mesg=nil, &block) # :yields: argument
583
+ Clause.new(mesg, &block)
584
+ end
585
+
586
+ # Passes if the subclause does not pass on the argument.
587
+ def not?(clause)
588
+ clause("not #{clause.inspect}") { |o| not ( clause === o ) }
589
+ end
590
+
591
+ # Always passes.
592
+ def anything
593
+ Clause.new("anything") { true }
594
+ end
595
+
596
+ # Passes if argument is true or false.
597
+ # contract self => boolean?
598
+ # def ==(other)
599
+ # ...
600
+ # end
601
+ def boolean?
602
+ #clause("true or false") { |o| ( o == true ) || ( o == false ) }
603
+ any?(TrueClass, FalseClass)
604
+ end
605
+
606
+ # Passes if any of the subclauses pass on the argument.
607
+ # contract any?(String, Symbol) => anything
608
+ def any?(*clauses)
609
+ clause("any of #{clauses.inspect}") { |o| clauses.any? {|c| c === o} }
610
+ end
611
+ alias :or? :any?
612
+
613
+ # Passes only if all of the subclauses pass on the argument.
614
+ # contract all?(Integer, nonzero?)
615
+ def all?(*clauses)
616
+ clause("all of #{clauses.inspect}") { |o| clauses.all? {|c| c === o} }
617
+ end
618
+ alias :and? :all?
619
+
620
+ # Passes if argument is numeric and nonzero.
621
+ def nonzero?
622
+ all? Numeric, clause("nonzero") {|o| o != 0}
623
+ end
624
+
625
+ # Passes if argument is Enumerable and the subclause passes on all of
626
+ # its objects.
627
+ #
628
+ # class StringArray < Array
629
+ # include Handshake
630
+ # contract :+, many?(String) => self
631
+ # end
632
+ def many?(clause)
633
+ many_with_map?(clause) { |o| o }
634
+ end
635
+
636
+ # Passes if argument is Enumerable and the subclause passes on all of
637
+ # its objects, mapped over the given block.
638
+ # contract many_with_map?(nonzero?, "person age") { |person| person.age } => anything
639
+ def many_with_map?(clause, mesg=nil, &block) # :yields: argument
640
+ map_mesg = ( mesg.nil? ? "" : " after map #{mesg}" )
641
+ many_with_map = clause("many of #{clause.inspect}#{map_mesg}") do |o|
642
+ o.map(&block).all? { |p| clause === p }
643
+ end
644
+ all? Enumerable, many_with_map
645
+ end
646
+
647
+ # Passes if argument is a Hash and if the key and value clauses pass all
648
+ # of its keys and values, respectively.
649
+ # E.g. <tt>hash_of?(Symbol, String)</tt>:
650
+ #
651
+ # :foo => "bar", :baz => "qux" # passes
652
+ # :foo => "bar", "baz" => 3 # fails
653
+ def hash_of?(key_clause, value_clause)
654
+ all_keys = many_with_map?(key_clause, "all keys") { |kv| kv[0] }
655
+ all_values = many_with_map?(value_clause, "all values") { |kv| kv[1] }
656
+ all? Hash, all_keys, all_values
657
+ end
658
+
659
+ # Passes only if argument is a hash and does not contain any keys except
660
+ # those given.
661
+ # E.g. <tt>hash_with_keys(:foo, :bar, :baz)</tt>:
662
+ #
663
+ # :foo => 3 # passes
664
+ # :foo => 10, :bar => "foo" # passes
665
+ # :foo => "eight", :chunky_bacon => "delicious" # fails
666
+ def hash_with_keys(*keys)
667
+ key_assertion = clause("contains keys #{keys.inspect}") do |o|
668
+ ( o.keys - keys ).empty?
669
+ end
670
+ all? Hash, key_assertion
671
+ end
672
+ alias :hash_with_options :hash_with_keys
673
+
674
+ # Passes if:
675
+ # * argument is a hash, and
676
+ # * argument contains only the keys explicitly specified in the given
677
+ # hash, and
678
+ # * every value contract in the given hash passes every applicable value
679
+ # in the argument hash
680
+ # E.g. <tt>hash_contract(:foo => String, :bar => Integer)</tt>
681
+ #
682
+ # :foo => "foo" # passes
683
+ # :bar => 3 # passes
684
+ # :foo => "bar", :bar => 42 # passes
685
+ # :foo => 88, :bar => "none" # fails
686
+ def hash_contract(hash)
687
+ value_assertions = hash.keys.map do |k|
688
+ clause("key #{k} requires #{hash[k].inspect}") do |o|
689
+ o.has_key?(k) ? hash[k] === o[k] : true
690
+ end
691
+ end
692
+ all? hash_with_keys(*hash.keys), *value_assertions
693
+ end
694
+
695
+ # Passes if argument responds to all of the given methods.
696
+ def responds_to?(*methods)
697
+ respond_assertions = methods.map do |m|
698
+ clause("responds to #{m}") { |o| o.respond_to? m }
699
+ end
700
+ all? *respond_assertions
701
+ end
702
+
703
+ # Allows you to check whether the argument is_a? of the given symbol.
704
+ # For example, is?(:String). Useful for situations where you want
705
+ # to check for a class type that hasn't been defined yet when Ruby
706
+ # evaluates the contract but will have been by the time the code runs.
707
+ # Note that <tt>String => anything</tt> is equivalent to
708
+ # <tt>is?(:String) => anything</tt>.
709
+ def is?(class_symbol)
710
+ clause(class_symbol.to_s) { |o|
711
+ Object.const_defined?(class_symbol) && o.is_a?(Object.const_get(class_symbol))
712
+ }
713
+ end
714
+
715
+ end
716
+
717
+ class ContractViolation < RuntimeError; end
718
+ class AssertionFailed < ContractViolation; end
719
+ class ContractError < RuntimeError; end
720
+ end
721
+
722
+
723
+ module Test # :nodoc:
724
+ module Unit # :nodoc:
725
+ module Assertions
726
+ # Asserts that the given block violates the contract by raising an
727
+ # instance of Handshake::ContractViolation.
728
+ def assert_violation(&block)
729
+ assert_raise(Handshake::ContractViolation, Handshake::AssertionFailed, &block)
730
+ end
731
+
732
+ def assert_passes(&block)
733
+ assert_nothing_raised(&block)
734
+ end
735
+ end
736
+ end
737
+ end