handshake 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest.txt +4 -2
- data/README.txt +110 -0
- data/Rakefile +2 -1
- data/lib/handshake/block_contract.rb +53 -0
- data/lib/handshake/clause_methods.rb +170 -0
- data/lib/handshake/proxy_self.rb +19 -0
- data/lib/handshake/version.rb +1 -1
- data/lib/handshake.rb +589 -0
- data/test/tc_handshake.rb +54 -0
- metadata +6 -4
- data/README +0 -33
- data/lib/handshake/handshake.rb +0 -737
data/lib/handshake.rb
CHANGED
@@ -1 +1,590 @@
|
|
1
1
|
Dir[File.join(File.dirname(__FILE__), 'handshake/**/*.rb')].sort.each { |lib| require lib }
|
2
|
+
|
3
|
+
require 'test/unit/assertions'
|
4
|
+
|
5
|
+
# A module for defining class and method contracts (as in design-by-contract
|
6
|
+
# programming). To use it in your code, include this module in a class.
|
7
|
+
# Note that when you do so, that class's +new+ method will be replaced with
|
8
|
+
# one that returns a contract-checked proxy object.
|
9
|
+
# There are three different types of contracts you can specify on a class.
|
10
|
+
# See Handshake::ClassMethods for more documentation.
|
11
|
+
module Handshake
|
12
|
+
|
13
|
+
# Catches any thrown :contract exception raised within the given block,
|
14
|
+
# appends the given message to the violation message, and re-raises the
|
15
|
+
# exception.
|
16
|
+
def Handshake.catch_contract(mesg=nil, &block) # :nodoc:
|
17
|
+
violation = catch(:contract, &block)
|
18
|
+
if violation.is_a?(Exception)
|
19
|
+
# Re-raise the violation with the given message, ensuring that the
|
20
|
+
# callback stack begins with the caller of this method rather than
|
21
|
+
# this method.
|
22
|
+
message = ( mesg.nil? ? "" : ( mesg + ": " ) ) + violation.message
|
23
|
+
raise violation.class, message, caller
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# When Handshake is included in a class, that class's +new+ method is
|
28
|
+
# overridden to provide custom functionality. A proxy object, returned
|
29
|
+
# in place of the real object, filters all external method calls through
|
30
|
+
# any contracts that have been defined.
|
31
|
+
# <b>N.B.:<b> Handshake is designed to act as a barrier between an object and
|
32
|
+
# its callers. However, anything that takes place within that barrier
|
33
|
+
# is not checked. This means that Handshake is, at the moment, unable
|
34
|
+
# to enforce contracts on methods called only internally, notably private
|
35
|
+
# methods.
|
36
|
+
def Handshake.included(base)
|
37
|
+
base.extend(ClassMethods)
|
38
|
+
base.extend(ClauseMethods)
|
39
|
+
|
40
|
+
base.send(:include, Test::Unit::Assertions)
|
41
|
+
base.send(:include, Handshake::InstanceMethods)
|
42
|
+
|
43
|
+
base.class_inheritable_array :invariants
|
44
|
+
base.write_inheritable_array :invariants, []
|
45
|
+
|
46
|
+
base.class_inheritable_hash :method_contracts
|
47
|
+
base.write_inheritable_hash :method_contracts, {}
|
48
|
+
|
49
|
+
class << base
|
50
|
+
alias :instantiate :new
|
51
|
+
# Override the class-level new method of every class that includes
|
52
|
+
# Contract and cause it to return a proxy object for the original.
|
53
|
+
def new(*args, &block)
|
54
|
+
if @non_instantiable
|
55
|
+
raise ContractViolation, "This class has been marked as abstract and cannot be instantiated."
|
56
|
+
end
|
57
|
+
o = nil
|
58
|
+
|
59
|
+
# Special case: at this stage it's only possible to check arguments
|
60
|
+
# (before) and invariants (after). Maybe postconditions?
|
61
|
+
Handshake.catch_contract("Contract violated in call to constructor of class #{self}") do
|
62
|
+
if contract_defined? :initialize
|
63
|
+
method_contracts[:initialize].check_accepts!(*args, &block)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
### Instantiate the object itself.
|
68
|
+
o = self.instantiate(*args, &block)
|
69
|
+
|
70
|
+
Handshake.catch_contract("Invariant violated by constructor of class #{self}") do
|
71
|
+
o.check_invariants!
|
72
|
+
end
|
73
|
+
|
74
|
+
raise ContractError, "Could not instantiate object" if o.nil?
|
75
|
+
|
76
|
+
### Wrap the object in a proxy.
|
77
|
+
p = Proxy.new( o )
|
78
|
+
# Make sure that the object has a reference back to the proxy.
|
79
|
+
o.instance_variable_set("@checked_self", p)
|
80
|
+
p
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# This module contains methods that are mixed into any class that includes
|
86
|
+
# Handshake. They allow you to define constraints on that class and its
|
87
|
+
# methods. Subclasses will inherit the contracts and invariants of its
|
88
|
+
# superclass, but Handshake contracts currently can't be mixed-in via a
|
89
|
+
# module.
|
90
|
+
#
|
91
|
+
# This module defines three kinds of contracts: class invariants, method
|
92
|
+
# signature constraints, and more general method pre- and post-conditions.
|
93
|
+
# Invariants accept a block which should return a boolean. Pre- and post-
|
94
|
+
# conditions expect you to use assertions (all of Test::Unit's standard
|
95
|
+
# assertions are available) and will pass unless an assertion fails.
|
96
|
+
# Method signature contracts map inputs clauses to output clauses. A
|
97
|
+
# "clause" is defined as any object that implements the === method.
|
98
|
+
#
|
99
|
+
# All method contracts are defined on the method defined immediately after
|
100
|
+
# their declaration unless a method name is specified. For example,
|
101
|
+
#
|
102
|
+
# contract :foo, String => Integer
|
103
|
+
#
|
104
|
+
# is equivalent to
|
105
|
+
#
|
106
|
+
# contract String => Integer
|
107
|
+
# def foo ...
|
108
|
+
#
|
109
|
+
# ===Method signature contracts
|
110
|
+
# contract String => Integer
|
111
|
+
# contract [ String, 1..5, [ Integer ], Block ] => [ String, String ]
|
112
|
+
# contract clause("must equal 'foo'") { |o| o == "foo" } => anything
|
113
|
+
# contract Block(String => Integer) => Symbol
|
114
|
+
#
|
115
|
+
# A method signature contract is defined as a mapping from valid inputs to
|
116
|
+
# to valid outputs. A clause here is any object which implements the
|
117
|
+
# <tt>===</tt> method. Classes, ranges, regexes, and other useful objects
|
118
|
+
# are thus all valid values for a method signature contract.
|
119
|
+
#
|
120
|
+
# Multiple arguments are specified as an array. To specify that a method
|
121
|
+
# accepts varargs, define a nested array as the last or second-to-last
|
122
|
+
# item in the array. To specify that a method accepts a block, use the
|
123
|
+
# Block clause, which accepts a method signature hash as an argument,
|
124
|
+
# as above. To specify that a method should accept a block, but that the
|
125
|
+
# block should be unchecked, simply use Block instead of Block(... => ...).
|
126
|
+
#
|
127
|
+
# New clauses may be created easily with the Handshake::ClauseMethods#clause
|
128
|
+
# method. Handshake::ClauseMethods also provides a number of useful contract
|
129
|
+
# combinators for specifying rich input and output contracts.
|
130
|
+
#
|
131
|
+
# ===Contract-checked accessors
|
132
|
+
# contract_reader :foo => String, :bar => Integer
|
133
|
+
# contract_writer ...
|
134
|
+
# contract_accessor ...
|
135
|
+
# Defines contract-checked accessors. Method names and clauses are specified
|
136
|
+
# in a hash. Hash values are any valid clause.
|
137
|
+
#
|
138
|
+
# ===Invariants
|
139
|
+
# invariant(optional_message) { returns true }
|
140
|
+
# Aliased as +always+. Has access to instance variables and methods of object
|
141
|
+
# but calls to same are unchecked.
|
142
|
+
#
|
143
|
+
# ===Pre/post-conditions
|
144
|
+
# before(optional_message) { |arg1, ...| assert condition }
|
145
|
+
# after(optional_message) { |arg1, ..., returned| assert condition }
|
146
|
+
# around(optional_message) { |arg1, ...| assert condition }
|
147
|
+
# Check a set of conditions, using assertions, before and after method
|
148
|
+
# invocation. +before+ and +after+ are aliased as +requires+ and +ensures+
|
149
|
+
# respectively. +around+ currently throws a block argument warning; this
|
150
|
+
# should be fixed soon. Same scope rules as invariants, so you can check
|
151
|
+
# instance variables and local methods. All Test::Unit::Assertions are available
|
152
|
+
# for use, but any such AssertionFailed errors encountered are re-raised
|
153
|
+
# by Handshake as Handshake::AssertionFailed errors to avoid confusion
|
154
|
+
# with test case execution.
|
155
|
+
#
|
156
|
+
# ===Abstract class decorator
|
157
|
+
# class SuperDuperContract
|
158
|
+
# include Handshake; abstract!
|
159
|
+
# ...
|
160
|
+
# end
|
161
|
+
#
|
162
|
+
# To define a class as non-instantiable and have Handshake raise a
|
163
|
+
# ContractViolation if a caller attempts to do so, call <tt>abstract!</tt>
|
164
|
+
# at the top of the class definition. This attribute is not inherited
|
165
|
+
# by subclasses, but is useful if you would like to define a pure-contract
|
166
|
+
# superclass that isn't intended to be instantiated directly.
|
167
|
+
module ClassMethods
|
168
|
+
|
169
|
+
# Define this class as non-instantiable. Subclasses do not inherit this
|
170
|
+
# attribute.
|
171
|
+
def abstract!
|
172
|
+
@non_instantiable = true
|
173
|
+
end
|
174
|
+
|
175
|
+
# Specify an invariant, with a block and an optional error message.
|
176
|
+
def invariant(mesg=nil, &block) # :yields:
|
177
|
+
write_inheritable_array(:invariants, [ Invariant.new(mesg, &block) ] )
|
178
|
+
nil
|
179
|
+
end
|
180
|
+
alias :always :invariant
|
181
|
+
|
182
|
+
# In order for contract clauses to work in conjunction with Handshake
|
183
|
+
# proxy objects, the === method must be redefined in terms of is_a?.
|
184
|
+
def ===(other)
|
185
|
+
other.is_a? self
|
186
|
+
end
|
187
|
+
|
188
|
+
# Specify an argument contract, with argument clauses on one side of the
|
189
|
+
# hash arrow and returned values on the other. Each clause must implement
|
190
|
+
# the === method or have been created with the assert method. This
|
191
|
+
# method should generally not be called directly.
|
192
|
+
def contract(meth_or_hash, contract_hash=nil)
|
193
|
+
if meth_or_hash.is_a? Hash
|
194
|
+
defer :contract, meth_or_hash
|
195
|
+
else
|
196
|
+
define_contract(meth_or_hash, contract_hash)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Specify a precondition.
|
201
|
+
def before(meth_or_mesg=nil, mesg=nil, &block)
|
202
|
+
condition(:before, meth_or_mesg, mesg, &block)
|
203
|
+
end
|
204
|
+
alias :requires :before
|
205
|
+
|
206
|
+
# Specify a postcondition.
|
207
|
+
def after(meth_or_mesg=nil, mesg=nil, &block)
|
208
|
+
condition(:after, meth_or_mesg, mesg, &block)
|
209
|
+
end
|
210
|
+
alias :ensures :after
|
211
|
+
|
212
|
+
# Specify a bothcondition.
|
213
|
+
def around(meth_or_mesg=nil, mesg=nil, &block)
|
214
|
+
condition(:around, meth_or_mesg, mesg, &block)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Returns the MethodContract for the given method name. Side effect:
|
218
|
+
# creates one if none defined.
|
219
|
+
def contract_for(method)
|
220
|
+
if contract_defined?(method)
|
221
|
+
method_contracts[method]
|
222
|
+
else
|
223
|
+
contract = MethodContract.new("#{self}##{method}")
|
224
|
+
write_inheritable_hash :method_contracts, { method => contract }
|
225
|
+
contract
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Returns true if a contract is defined for the named method.
|
230
|
+
def contract_defined?(method)
|
231
|
+
method_contracts.has_key?(method)
|
232
|
+
end
|
233
|
+
|
234
|
+
# Defines contract-checked attribute readers with the given hash of method
|
235
|
+
# name to clause.
|
236
|
+
def contract_reader(meth_to_clause)
|
237
|
+
attr_reader *(meth_to_clause.keys)
|
238
|
+
meth_to_clause.each do |meth, cls|
|
239
|
+
contract meth, nil => cls
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Defines contract-checked attribute writers with the given hash of method
|
244
|
+
# name to clause.
|
245
|
+
def contract_writer(meth_to_clause)
|
246
|
+
attr_writer *(meth_to_clause.keys)
|
247
|
+
meth_to_clause.each do |meth, cls|
|
248
|
+
contract "#{meth}=".to_sym, cls => anything
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# Defines contract-checked attribute accessors for the given hash of method
|
253
|
+
# name to clause.
|
254
|
+
def contract_accessor(meth_to_clause)
|
255
|
+
contract_reader meth_to_clause
|
256
|
+
contract_writer meth_to_clause
|
257
|
+
end
|
258
|
+
|
259
|
+
# Callback from method add event. If a previous method contract
|
260
|
+
# declaration was deferred, complete it now with the name of the newly-
|
261
|
+
# added method.
|
262
|
+
def method_added(meth_name)
|
263
|
+
@deferred ||= {}
|
264
|
+
unless @deferred.empty?
|
265
|
+
@deferred.each do |k, v|
|
266
|
+
case k
|
267
|
+
when :before, :after, :around
|
268
|
+
define_condition meth_name, k, v
|
269
|
+
when :contract
|
270
|
+
define_contract meth_name, v
|
271
|
+
end
|
272
|
+
end
|
273
|
+
@deferred.clear
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
private
|
278
|
+
|
279
|
+
def define_contract(method, contract_hash)
|
280
|
+
contract = contract_for(method).dup
|
281
|
+
contract.signature = contract_hash
|
282
|
+
write_inheritable_hash :method_contracts, { method => contract }
|
283
|
+
end
|
284
|
+
|
285
|
+
def define_condition(method, type, condition)
|
286
|
+
defined_before = [ :before, :around ].include? type
|
287
|
+
defined_after = [ :after, :around ].include? type
|
288
|
+
contract = contract_for(method).dup
|
289
|
+
contract.preconditions << condition if defined_before
|
290
|
+
contract.postconditions << condition if defined_after
|
291
|
+
write_inheritable_hash :method_contracts, { method => contract }
|
292
|
+
end
|
293
|
+
|
294
|
+
def condition(type, meth_or_mesg=nil, mesg=nil, &block)
|
295
|
+
method_specified = meth_or_mesg.is_a?(Symbol)
|
296
|
+
message = method_specified ? mesg : meth_or_mesg
|
297
|
+
condition = MethodCondition.new(message, &block)
|
298
|
+
if method_specified
|
299
|
+
define_condition(type, meth_or_mesg, condition)
|
300
|
+
else
|
301
|
+
defer type, condition
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def defer(type, value)
|
306
|
+
( @deferred ||= {} )[type] = value
|
307
|
+
end
|
308
|
+
|
309
|
+
end
|
310
|
+
|
311
|
+
module InstanceMethods
|
312
|
+
# Checks the invariants defined on this class against +self+, raising a
|
313
|
+
# ContractViolation if any of them fail.
|
314
|
+
def check_invariants!
|
315
|
+
self.class.invariants.each do |invar|
|
316
|
+
unless invar.holds?(self)
|
317
|
+
mesg = invar.mesg || "Invariant check failed"
|
318
|
+
throw :contract, ContractViolation.new(mesg)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
protected
|
324
|
+
# Returns the contract-checked proxy of self.
|
325
|
+
def checked_self
|
326
|
+
@checked_self || self
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# A ProcContract encapsulates knowledge about the signature of a method and
|
331
|
+
# can check arrays of values against the signature through the
|
332
|
+
# +check_equivalence!+ method.
|
333
|
+
class ProcContract # :nodoc:
|
334
|
+
attr_accessor :accepts, :returns
|
335
|
+
|
336
|
+
def initialize
|
337
|
+
@accepts, @returns = [], []
|
338
|
+
end
|
339
|
+
|
340
|
+
# Accepts signatures of the form:
|
341
|
+
# Clause => Clause
|
342
|
+
# [ Clause, Clause ] => Clause
|
343
|
+
def signature=(contract_hash)
|
344
|
+
raise ArgumentError unless contract_hash.length == 1
|
345
|
+
sig_accepts, sig_returns = [ contract_hash.keys.first, contract_hash.values.first ].map {|v| arrayify v}
|
346
|
+
self.accepts = sig_accepts
|
347
|
+
self.returns = sig_returns
|
348
|
+
end
|
349
|
+
|
350
|
+
def check_accepts!(*args, &block)
|
351
|
+
@accepts.each_with_index do |expected_arg, i|
|
352
|
+
# Varargs: consume all remaining arguments.
|
353
|
+
if expected_arg.is_a? Array
|
354
|
+
check_varargs!(args, expected_arg.first, i) and break
|
355
|
+
end
|
356
|
+
check_equivalence!(args[i], expected_arg)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
def check_returns!(*args)
|
361
|
+
@returns.each_with_index do |expected, i|
|
362
|
+
check_equivalence!(args[i], expected)
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
def accepts_varargs?
|
367
|
+
accepts.last.is_a? Array
|
368
|
+
end
|
369
|
+
|
370
|
+
private
|
371
|
+
def check_varargs!(given_args, expected, index)
|
372
|
+
given_args[index..-1].each {|arg| check_equivalence!(arg, expected)}
|
373
|
+
end
|
374
|
+
|
375
|
+
def arrayify(value_or_array)
|
376
|
+
value_or_array.is_a?(Array) ? value_or_array : [ value_or_array ]
|
377
|
+
end
|
378
|
+
|
379
|
+
|
380
|
+
# Checks the given value against the expected value using === and throws
|
381
|
+
# :contract if it fails. This is a bit clunky.
|
382
|
+
def check_equivalence!(given, expected)
|
383
|
+
unless expected === given
|
384
|
+
mesg = "expected #{expected.inspect}, received #{given.inspect}"
|
385
|
+
throw :contract, ContractViolation.new(mesg)
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
# Class representing method contracts. Not for external use.
|
391
|
+
class MethodContract < ProcContract # :nodoc:
|
392
|
+
attr_accessor :preconditions, :postconditions
|
393
|
+
attr_reader :block_contract
|
394
|
+
|
395
|
+
def initialize(method_name)
|
396
|
+
@method_name = method_name
|
397
|
+
@preconditions, @postconditions = [], []
|
398
|
+
@accepts, @returns = [], []
|
399
|
+
end
|
400
|
+
|
401
|
+
def check_accepts!(*args, &block)
|
402
|
+
super(*args, &block)
|
403
|
+
if expects_block?
|
404
|
+
check_equivalence!(block, Proc)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
# Returns true only if this MethodContract has been set up to check
|
409
|
+
# for one or more contract conditions.
|
410
|
+
def defined?
|
411
|
+
[ preconditions, postconditions, accepts, returns ].any? do |ary|
|
412
|
+
not ary.empty?
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
# Checks the postconditions of this contract against the given object
|
417
|
+
# and return values. Any assertions thrown are re-raised as
|
418
|
+
# Handshake::AssertionViolation errors.
|
419
|
+
def check_post!(o, *args)
|
420
|
+
check_conditions!(o, args, @postconditions)
|
421
|
+
end
|
422
|
+
|
423
|
+
# Checks the preconditions of this contract against the given object
|
424
|
+
# and arugment values. Any assertions thrown are re-raised as
|
425
|
+
# Handshake::AssertionFailed errors.
|
426
|
+
def check_pre!(o, *args)
|
427
|
+
check_conditions!(o, args, @preconditions)
|
428
|
+
end
|
429
|
+
|
430
|
+
# Checks the given conditions against the object, passing the given args
|
431
|
+
# into the block. Throws :contract if any fail or if an exception is
|
432
|
+
# raised. Because of the need to evaluate the condition in the context
|
433
|
+
# of the object itself, a temporary method called +bound_condition_passes?+
|
434
|
+
# is defined on the object, using the block associated with the condition.
|
435
|
+
# TODO Is there a better way to evaluate an arbitary block in a particular binding? There must be.
|
436
|
+
def check_conditions!(o, args, conditions)
|
437
|
+
conditions.each do |condition|
|
438
|
+
o.class.instance_eval do
|
439
|
+
define_method(:bound_condition_passes?, &(condition.block))
|
440
|
+
end
|
441
|
+
begin
|
442
|
+
o.bound_condition_passes?(*args)
|
443
|
+
rescue Test::Unit::AssertionFailedError => afe
|
444
|
+
throw :contract, AssertionFailed.new(afe.message)
|
445
|
+
rescue Exception => e
|
446
|
+
throw :contract, e
|
447
|
+
end
|
448
|
+
o.class.send(:remove_method, :bound_condition_passes?)
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
# If the last argument is a Block, handle it as a special case. We
|
453
|
+
# do this to ensure that there's no conflict with any real arguments
|
454
|
+
# which may accept Procs.
|
455
|
+
def accepts=(args)
|
456
|
+
if args.last == Block # Transform into a ProcContract
|
457
|
+
args.pop
|
458
|
+
@block_contract = ProcContract.new
|
459
|
+
@block_contract.accepts = ClauseMethods::ANYTHING
|
460
|
+
@block_contract.returns = ClauseMethods::ANYTHING
|
461
|
+
elsif args.last.is_a?(ProcContract)
|
462
|
+
@block_contract = args.pop
|
463
|
+
end
|
464
|
+
|
465
|
+
if args.find_all {|o| o.is_a? Array}.length > 1
|
466
|
+
raise ContractError, "Cannot define more than one expected variable argument"
|
467
|
+
end
|
468
|
+
super(args)
|
469
|
+
end
|
470
|
+
|
471
|
+
def expects_block?
|
472
|
+
not @block_contract.nil?
|
473
|
+
end
|
474
|
+
|
475
|
+
end
|
476
|
+
|
477
|
+
# Specifies a condition on a method. Not for external use.
|
478
|
+
class MethodCondition # :nodoc:
|
479
|
+
attr_accessor :message, :block
|
480
|
+
def initialize(message=nil, &block)
|
481
|
+
@message, @block = message, block
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
# This class defines a class invariant, which has a block and an optional
|
486
|
+
# method. Not for external use.
|
487
|
+
class Invariant # :nodoc:
|
488
|
+
def initialize(mesg=nil, &block)
|
489
|
+
@mesg = mesg
|
490
|
+
@block = block
|
491
|
+
end
|
492
|
+
# Any -> Boolean
|
493
|
+
# Evaluates this class's block in the binding of the given object.
|
494
|
+
def holds?(o)
|
495
|
+
block = @block
|
496
|
+
o.instance_eval &block
|
497
|
+
end
|
498
|
+
def mesg
|
499
|
+
@mesg || "Invariant check failed"
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
# This class filters all method calls to its proxied object through any
|
504
|
+
# contracts defined on that object's class. It attempts to look and act
|
505
|
+
# like its proxied object for all intents and purposes, although it notably
|
506
|
+
# does not proxy +__id__+, +__send__+, or +class+.
|
507
|
+
class Proxy
|
508
|
+
NOT_PROXIED = [ "__id__", "__send__" ]
|
509
|
+
SELF_PROXIED = Object.instance_methods - NOT_PROXIED
|
510
|
+
|
511
|
+
# Redefine language-level methods inherited from Object, ensuring that
|
512
|
+
# they are forwarded to the proxy object.
|
513
|
+
proxy_self *SELF_PROXIED
|
514
|
+
|
515
|
+
# Accepts an object to be proxied.
|
516
|
+
def initialize(proxied)
|
517
|
+
@proxied = proxied
|
518
|
+
end
|
519
|
+
|
520
|
+
# Returns the wrapped object. Method calls made against this object
|
521
|
+
# will not be checked.
|
522
|
+
def unchecked!
|
523
|
+
@proxied
|
524
|
+
end
|
525
|
+
|
526
|
+
# Returns the class of the proxied object.
|
527
|
+
def proxied_class
|
528
|
+
@proxied.class
|
529
|
+
end
|
530
|
+
|
531
|
+
# Override the send method, and alias method_missing to same.
|
532
|
+
# This method intercepts all method calls and runs them through the
|
533
|
+
# contract filter. The order of contract checks is as follows:
|
534
|
+
# * Before: invariants, method signature, precondition
|
535
|
+
# * Method is called
|
536
|
+
# * After: method signature, postcondition, invariants
|
537
|
+
def send(meth_name, *args, &block)
|
538
|
+
meth_string = "#{@proxied.class}##{meth_name}"
|
539
|
+
contract = @proxied.class.contract_for(meth_name)
|
540
|
+
return_val = nil
|
541
|
+
# Use throw/catch rather than raise/rescue in order to pull exceptions
|
542
|
+
# once and only once from within the stack trace.
|
543
|
+
Handshake.catch_contract("Contract violated in call to #{meth_string}") do
|
544
|
+
@proxied.check_invariants!
|
545
|
+
contract.check_accepts! *args, &block
|
546
|
+
contract.check_pre! @proxied, *args
|
547
|
+
end
|
548
|
+
|
549
|
+
# make actual call, wrapping the given block in a new block so that
|
550
|
+
# contract checks work if receiver uses yield.
|
551
|
+
return_val = nil
|
552
|
+
if contract.expects_block?
|
553
|
+
block.contract = contract.block_contract
|
554
|
+
return_val = @proxied.send(meth_name, *args) { |*argz| block.call(*argz) }
|
555
|
+
else
|
556
|
+
return_val = @proxied.send(meth_name, *args, &block)
|
557
|
+
end
|
558
|
+
|
559
|
+
Handshake.catch_contract("Contract violated by #{meth_string}") do
|
560
|
+
contract.check_returns! return_val
|
561
|
+
contract.check_post! @proxied, *(args << return_val)
|
562
|
+
@proxied.check_invariants!
|
563
|
+
end
|
564
|
+
|
565
|
+
return return_val
|
566
|
+
end
|
567
|
+
alias :method_missing :send
|
568
|
+
|
569
|
+
end
|
570
|
+
|
571
|
+
class ContractViolation < RuntimeError; end
|
572
|
+
class AssertionFailed < ContractViolation; end
|
573
|
+
class ContractError < RuntimeError; end
|
574
|
+
end
|
575
|
+
|
576
|
+
module Test # :nodoc:
|
577
|
+
module Unit # :nodoc:
|
578
|
+
module Assertions
|
579
|
+
# Asserts that the given block violates the contract by raising an
|
580
|
+
# instance of Handshake::ContractViolation.
|
581
|
+
def assert_violation(&block)
|
582
|
+
assert_raise(Handshake::ContractViolation, Handshake::AssertionFailed, &block)
|
583
|
+
end
|
584
|
+
|
585
|
+
def assert_passes(&block)
|
586
|
+
assert_nothing_raised(&block)
|
587
|
+
end
|
588
|
+
end
|
589
|
+
end
|
590
|
+
end
|
data/test/tc_handshake.rb
CHANGED
@@ -491,4 +491,58 @@ class TestContract < Test::Unit::TestCase
|
|
491
491
|
assert_passes { AcceptsSuperAndSub.new.call Subclass.new }
|
492
492
|
assert_passes { Superclass.new == Subclass.new }
|
493
493
|
end
|
494
|
+
|
495
|
+
class CheckedSelf
|
496
|
+
include Handshake
|
497
|
+
def call_checked(obj)
|
498
|
+
checked_self.call(obj)
|
499
|
+
end
|
500
|
+
def call_unchecked(obj)
|
501
|
+
call(obj)
|
502
|
+
end
|
503
|
+
contract String => anything
|
504
|
+
def call(str); str; end
|
505
|
+
end
|
506
|
+
|
507
|
+
class ExtendsCheckedSelf < CheckedSelf
|
508
|
+
private
|
509
|
+
contract Numeric => anything
|
510
|
+
def call(n); n; end
|
511
|
+
end
|
512
|
+
|
513
|
+
def test_checked_self
|
514
|
+
assert_violation { CheckedSelf.new.call(5) }
|
515
|
+
assert_violation { CheckedSelf.new.call_checked(5) }
|
516
|
+
assert_passes { CheckedSelf.new.call_unchecked(5) }
|
517
|
+
assert_passes { CheckedSelf.new.call_checked("foo") }
|
518
|
+
assert_violation { ExtendsCheckedSelf.new.call_checked("foo") }
|
519
|
+
assert_passes { ExtendsCheckedSelf.new.call_checked(5) }
|
520
|
+
assert_passes { ExtendsCheckedSelf.new.call_unchecked("foo") }
|
521
|
+
end
|
522
|
+
|
523
|
+
class CheckedBlockContract
|
524
|
+
include Handshake
|
525
|
+
|
526
|
+
contract [ anything, Block(String => Integer) ] => Integer
|
527
|
+
def yields(value); yield(value); end
|
528
|
+
|
529
|
+
contract [ anything, Block(String => Integer) ] => Integer
|
530
|
+
def calls(value, &block); block.call(value); end
|
531
|
+
end
|
532
|
+
|
533
|
+
def test_checked_block_contract_yields
|
534
|
+
assert_violation { CheckedBlockContract.new.yields("3") {|s| s.to_s } }
|
535
|
+
assert_violation { CheckedBlockContract.new.yields("3") {|s| "foo" } }
|
536
|
+
assert_violation { CheckedBlockContract.new.yields(3) {|s| s.to_i} }
|
537
|
+
assert_passes { CheckedBlockContract.new.yields("3") {|s| 3 } }
|
538
|
+
assert_passes { CheckedBlockContract.new.yields("3") {|s| s.to_i } }
|
539
|
+
end
|
540
|
+
|
541
|
+
def test_checked_block_contract_calls
|
542
|
+
assert_violation { CheckedBlockContract.new.calls("3") {|s| s.to_s } }
|
543
|
+
assert_violation { CheckedBlockContract.new.calls("3") {|s| "foo" } }
|
544
|
+
assert_violation { CheckedBlockContract.new.calls(3) {|s| s.to_i} }
|
545
|
+
assert_passes { CheckedBlockContract.new.calls("3") {|s| 3 } }
|
546
|
+
assert_passes { CheckedBlockContract.new.calls("3") {|s| s.to_i } }
|
547
|
+
end
|
494
548
|
end
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
|
|
3
3
|
specification_version: 1
|
4
4
|
name: handshake
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
7
|
-
date: 2007-
|
6
|
+
version: 0.2.0
|
7
|
+
date: 2007-05-01 00:00:00 -04:00
|
8
8
|
summary: Handshake is a simple design-by-contract system for Ruby.
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -30,12 +30,14 @@ authors:
|
|
30
30
|
- Brian Guthrie
|
31
31
|
files:
|
32
32
|
- Manifest.txt
|
33
|
-
- README
|
33
|
+
- README.txt
|
34
34
|
- MIT-LICENSE
|
35
35
|
- Rakefile
|
36
36
|
- lib/handshake.rb
|
37
|
-
- lib/handshake/
|
37
|
+
- lib/handshake/block_contract.rb
|
38
|
+
- lib/handshake/clause_methods.rb
|
38
39
|
- lib/handshake/inheritable_attributes.rb
|
40
|
+
- lib/handshake/proxy_self.rb
|
39
41
|
- lib/handshake/version.rb
|
40
42
|
- test/tc_handshake.rb
|
41
43
|
test_files:
|