handshake 0.1.0 → 0.2.0

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