handshake 0.1.0 → 0.2.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/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:
|