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.
- data/MIT-LICENSE +20 -0
- data/Manifest.txt +10 -0
- data/README +33 -0
- data/Rakefile +54 -0
- data/lib/handshake/handshake.rb +737 -0
- data/lib/handshake/inheritable_attributes.rb +132 -0
- data/lib/handshake/version.rb +8 -0
- data/lib/handshake.rb +1 -0
- data/test/tc_handshake.rb +494 -0
- metadata +54 -0
@@ -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
|