handshake 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/{README.txt → README.rdoc} +32 -11
- data/Rakefile +9 -49
- data/lib/handshake.rb +24 -45
- data/lib/handshake/block_contract.rb +1 -1
- data/lib/handshake/clause_methods.rb +1 -1
- data/lib/handshake/version.rb +1 -1
- data/test/handshake_test.rb +598 -0
- metadata +67 -42
- data/test/tc_disable_handshake.rb +0 -29
- data/test/tc_handshake.rb +0 -528
data/{README.txt → README.rdoc}
RENAMED
@@ -1,12 +1,8 @@
|
|
1
1
|
= Handshake
|
2
2
|
|
3
|
-
Handshake is an informal design-by-contract system written in pure Ruby.
|
3
|
+
Handshake is an informal AOP and design-by-contract system written in pure Ruby.
|
4
4
|
It's intended to allow Ruby developers to apply simple, clear constraints
|
5
|
-
to their methods and classes.
|
6
|
-
(btguthrie@gmail.com) and lives at http://handshake.rubyforge.org.
|
7
|
-
|
8
|
-
Contracts defined with Handshake are not enforced unless the global $DEBUG
|
9
|
-
flag is set.
|
5
|
+
to their methods and classes.
|
10
6
|
|
11
7
|
=== Features
|
12
8
|
|
@@ -14,19 +10,44 @@ flag is set.
|
|
14
10
|
* Contracts on blocks and procs
|
15
11
|
* Method pre- and post-conditions
|
16
12
|
* Class invariants
|
17
|
-
* Define a class as abstract
|
18
13
|
|
19
14
|
=== Examples
|
20
15
|
|
21
|
-
Here's an example of Handshake in action:
|
16
|
+
Here's an example of Handshake in action on a hypothetical BankAccount class:
|
17
|
+
|
18
|
+
class BankAccount
|
19
|
+
attr_reader :balance
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def less_than_balance?
|
23
|
+
all? positive_number?, clause {|n| n <= balance}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
invariant { balance >= 0 }
|
28
|
+
|
29
|
+
contract positive_number? => anything
|
30
|
+
def initialize(balance)
|
31
|
+
@balance = balance
|
32
|
+
end
|
33
|
+
|
34
|
+
contract less_than_balance? => positive_number?
|
35
|
+
def withdraw(amount)
|
36
|
+
new_balance = @balance - amount
|
37
|
+
@balance = new_balance
|
38
|
+
return new_balance
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
Here's an example that uses an invariant to enforce a constraint on a subclass of Array:
|
22
43
|
|
23
|
-
# An array that can never be empty.
|
24
44
|
class NonEmptyArray < Array
|
25
45
|
include Handshake
|
26
46
|
invariant { not empty? }
|
27
47
|
end
|
28
48
|
|
29
|
-
|
49
|
+
Further specializing the subclass:
|
50
|
+
|
30
51
|
class NonEmptyStringArray < NonEmptyArray
|
31
52
|
contract :initialize, [[ String ]] => anything
|
32
53
|
contract :<<, String => self
|
@@ -91,7 +112,7 @@ private method +checked_self+:
|
|
91
112
|
|
92
113
|
=== License (MIT)
|
93
114
|
|
94
|
-
Copyright (c)
|
115
|
+
Copyright (c) 2010 Brian Guthrie
|
95
116
|
|
96
117
|
Permission is hereby granted, free of charge, to any person obtaining
|
97
118
|
a copy of this software and associated documentation files (the
|
data/Rakefile
CHANGED
@@ -1,55 +1,15 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
require 'rake'
|
3
|
-
require 'rake/clean'
|
4
2
|
require 'rake/testtask'
|
5
|
-
require '
|
6
|
-
require 'rake/gempackagetask'
|
7
|
-
require 'rake/rdoctask'
|
8
|
-
require 'rake/contrib/rubyforgepublisher'
|
9
|
-
require 'fileutils'
|
10
|
-
require 'hoe'
|
3
|
+
require 'rcov/rcovtask'
|
11
4
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
AUTHOR = "Brian Guthrie" # can also be an array of Authors
|
16
|
-
EMAIL = "btguthrie@gmail.com"
|
17
|
-
DESCRIPTION = "Handshake is a simple design-by-contract system for Ruby."
|
18
|
-
GEM_NAME = "handshake" # what ppl will type to install your gem
|
19
|
-
RUBYFORGE_PROJECT = "handshake" # The unix name for your project
|
20
|
-
HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
|
21
|
-
|
22
|
-
|
23
|
-
NAME = "handshake"
|
24
|
-
REV = nil # UNCOMMENT IF REQUIRED: File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
|
25
|
-
VERS = ENV['VERSION'] || (Handshake::VERSION::STRING + (REV ? ".#{REV}" : ""))
|
26
|
-
CLEAN.include ['**/.*.sw?', '*.gem', '.config']
|
27
|
-
RDOC_OPTS = ['--quiet', '--title', "Handshake documentation",
|
28
|
-
"--opname", "index.html",
|
29
|
-
"--line-numbers",
|
30
|
-
"--main", "README",
|
31
|
-
"--inline-source"]
|
32
|
-
|
33
|
-
class Hoe
|
34
|
-
def extra_deps
|
35
|
-
@extra_deps.reject { |x| Array(x).first == 'hoe' }
|
36
|
-
end
|
5
|
+
Rake::TestTask.new do |t|
|
6
|
+
t.test_files = FileList['test/*_test.rb']
|
37
7
|
end
|
38
8
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
p.description = DESCRIPTION
|
44
|
-
p.email = EMAIL
|
45
|
-
p.summary = DESCRIPTION
|
46
|
-
p.url = HOMEPATH
|
47
|
-
p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
|
48
|
-
p.test_globs = ["test/tc_*.rb"]
|
49
|
-
p.clean_globs = CLEAN #An array of file patterns to delete on clean.
|
50
|
-
|
51
|
-
# == Optional
|
52
|
-
#p.changes - A description of the release's latest changes.
|
53
|
-
#p.extra_deps - An array of rubygem dependencies.
|
54
|
-
#p.spec_extras - A hash of extra values to set in the gemspec.
|
9
|
+
Rcov::RcovTask.new do |t|
|
10
|
+
t.rcov_opts = ["--text-summary", "--include-file lib/**/*", "--exclude gems,spec,version"]
|
11
|
+
t.test_files = FileList['test/*_test.rb']
|
12
|
+
t.verbose = true
|
55
13
|
end
|
14
|
+
|
15
|
+
task :default => :test
|
data/lib/handshake.rb
CHANGED
@@ -24,6 +24,16 @@ module Handshake
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
+
# Suppress Handshake contract checking, for use in production code.
|
28
|
+
def Handshake.suppress!
|
29
|
+
@suppress_handshake = true
|
30
|
+
end
|
31
|
+
|
32
|
+
def Handshake.suppressed?
|
33
|
+
@suppress_handshake = false unless defined?(@suppress_handshake)
|
34
|
+
@suppress_handshake
|
35
|
+
end
|
36
|
+
|
27
37
|
# When Handshake is included in a class, that class's +new+ method is
|
28
38
|
# overridden to provide custom functionality. A proxy object, returned
|
29
39
|
# in place of the real object, filters all external method calls through
|
@@ -46,42 +56,27 @@ module Handshake
|
|
46
56
|
base.class_inheritable_hash :method_contracts
|
47
57
|
base.write_inheritable_hash :method_contracts, {}
|
48
58
|
|
49
|
-
# No contracts will ever be checked if we return now, so it's a good place
|
50
|
-
# to put the $DEBUG flag check.
|
51
|
-
return unless $DEBUG
|
52
|
-
|
53
59
|
class << base
|
54
|
-
alias :
|
60
|
+
alias :__new__ :new
|
55
61
|
# Override the class-level new method of every class that includes
|
56
62
|
# Contract and cause it to return a proxy object for the original.
|
57
63
|
def new(*args, &block)
|
58
|
-
if
|
59
|
-
|
60
|
-
end
|
61
|
-
o = nil
|
62
|
-
|
63
|
-
# Special case: at this stage it's only possible to check arguments
|
64
|
-
# (before) and invariants (after). Maybe postconditions?
|
64
|
+
return __new__(*args, &block) if Handshake.suppressed?
|
65
|
+
|
65
66
|
Handshake.catch_contract("Contract violated in call to constructor of class #{self}") do
|
66
67
|
if contract_defined? :initialize
|
67
68
|
method_contracts[:initialize].check_accepts!(*args, &block)
|
68
69
|
end
|
69
70
|
end
|
70
71
|
|
71
|
-
|
72
|
-
|
72
|
+
o = __new__(*args, &block)
|
73
|
+
raise ContractError, "Could not instantiate object" if o.nil?
|
73
74
|
|
74
75
|
Handshake.catch_contract("Invariant violated by constructor of class #{self}") do
|
75
76
|
o.check_invariants!
|
76
77
|
end
|
77
78
|
|
78
|
-
|
79
|
-
|
80
|
-
### Wrap the object in a proxy.
|
81
|
-
p = Proxy.new( o )
|
82
|
-
# Make sure that the object has a reference back to the proxy.
|
83
|
-
o.instance_variable_set("@checked_self", p)
|
84
|
-
p
|
79
|
+
Proxy.new o
|
85
80
|
end
|
86
81
|
end
|
87
82
|
end
|
@@ -156,26 +151,8 @@ module Handshake
|
|
156
151
|
# for use, but any such AssertionFailed errors encountered are re-raised
|
157
152
|
# by Handshake as Handshake::AssertionFailed errors to avoid confusion
|
158
153
|
# with test case execution.
|
159
|
-
#
|
160
|
-
# ===Abstract class decorator
|
161
|
-
# class SuperDuperContract
|
162
|
-
# include Handshake; abstract!
|
163
|
-
# ...
|
164
|
-
# end
|
165
|
-
#
|
166
|
-
# To define a class as non-instantiable and have Handshake raise a
|
167
|
-
# ContractViolation if a caller attempts to do so, call <tt>abstract!</tt>
|
168
|
-
# at the top of the class definition. This attribute is not inherited
|
169
|
-
# by subclasses, but is useful if you would like to define a pure-contract
|
170
|
-
# superclass that isn't intended to be instantiated directly.
|
171
154
|
module ClassMethods
|
172
155
|
|
173
|
-
# Define this class as non-instantiable. Subclasses do not inherit this
|
174
|
-
# attribute.
|
175
|
-
def abstract!
|
176
|
-
@non_instantiable = true
|
177
|
-
end
|
178
|
-
|
179
156
|
# Specify an invariant, with a block and an optional error message.
|
180
157
|
def invariant(mesg=nil, &block) # :yields:
|
181
158
|
write_inheritable_array(:invariants, [ Invariant.new(mesg, &block) ] )
|
@@ -238,7 +215,7 @@ module Handshake
|
|
238
215
|
# Defines contract-checked attribute readers with the given hash of method
|
239
216
|
# name to clause.
|
240
217
|
def contract_reader(meth_to_clause)
|
241
|
-
attr_reader
|
218
|
+
attr_reader(*(meth_to_clause.keys))
|
242
219
|
meth_to_clause.each do |meth, cls|
|
243
220
|
contract meth, nil => cls
|
244
221
|
end
|
@@ -247,7 +224,7 @@ module Handshake
|
|
247
224
|
# Defines contract-checked attribute writers with the given hash of method
|
248
225
|
# name to clause.
|
249
226
|
def contract_writer(meth_to_clause)
|
250
|
-
attr_writer
|
227
|
+
attr_writer(*(meth_to_clause.keys))
|
251
228
|
meth_to_clause.each do |meth, cls|
|
252
229
|
contract "#{meth}=".to_sym, cls => anything
|
253
230
|
end
|
@@ -319,7 +296,7 @@ module Handshake
|
|
319
296
|
self.class.invariants.each do |invar|
|
320
297
|
unless invar.holds?(self)
|
321
298
|
mesg = invar.mesg || "Invariant check failed"
|
322
|
-
throw :contract, ContractViolation.new(mesg)
|
299
|
+
throw :contract, ContractViolation.new(self.class.to_s + " " + mesg)
|
323
300
|
end
|
324
301
|
end
|
325
302
|
end
|
@@ -400,6 +377,7 @@ module Handshake
|
|
400
377
|
@method_name = method_name
|
401
378
|
@preconditions, @postconditions = [], []
|
402
379
|
@accepts, @returns = [], []
|
380
|
+
@block_contract = nil
|
403
381
|
end
|
404
382
|
|
405
383
|
def check_accepts!(*args, &block)
|
@@ -497,7 +475,7 @@ module Handshake
|
|
497
475
|
# Evaluates this class's block in the binding of the given object.
|
498
476
|
def holds?(o)
|
499
477
|
block = @block
|
500
|
-
o.instance_eval
|
478
|
+
o.instance_eval(&block)
|
501
479
|
end
|
502
480
|
def mesg
|
503
481
|
@mesg || "Invariant check failed"
|
@@ -514,11 +492,12 @@ module Handshake
|
|
514
492
|
|
515
493
|
# Redefine language-level methods inherited from Object, ensuring that
|
516
494
|
# they are forwarded to the proxy object.
|
517
|
-
proxy_self
|
495
|
+
proxy_self(*SELF_PROXIED)
|
518
496
|
|
519
497
|
# Accepts an object to be proxied.
|
520
498
|
def initialize(proxied)
|
521
499
|
@proxied = proxied
|
500
|
+
@proxied.instance_variable_set(:@checked_self, self)
|
522
501
|
end
|
523
502
|
|
524
503
|
# Returns the wrapped object. Method calls made against this object
|
@@ -546,7 +525,7 @@ module Handshake
|
|
546
525
|
# once and only once from within the stack trace.
|
547
526
|
Handshake.catch_contract("Contract violated in call to #{meth_string}") do
|
548
527
|
@proxied.check_invariants!
|
549
|
-
contract.check_accepts!
|
528
|
+
contract.check_accepts!(*args, &block)
|
550
529
|
contract.check_pre! @proxied, *args
|
551
530
|
end
|
552
531
|
|
@@ -151,7 +151,7 @@ module Handshake
|
|
151
151
|
respond_assertions = methods.map do |m|
|
152
152
|
clause("responds to #{m}") { |o| o.respond_to? m }
|
153
153
|
end
|
154
|
-
all?
|
154
|
+
all?(*respond_assertions)
|
155
155
|
end
|
156
156
|
|
157
157
|
# Allows you to check whether the argument is_a? of the given symbol.
|
data/lib/handshake/version.rb
CHANGED
@@ -0,0 +1,598 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'handshake'
|
4
|
+
require 'shoulda'
|
5
|
+
|
6
|
+
class HandshakeTest < Test::Unit::TestCase
|
7
|
+
context Handshake do
|
8
|
+
context "invariant" do
|
9
|
+
class InvariantDeclarations
|
10
|
+
include Handshake
|
11
|
+
invariant { true }
|
12
|
+
end
|
13
|
+
|
14
|
+
class ExtendsInvariantDeclarations < InvariantDeclarations
|
15
|
+
invariant { true }
|
16
|
+
end
|
17
|
+
|
18
|
+
should "correctly track the list of invariants in superclasses and subclasses" do
|
19
|
+
assert_equal 1, InvariantDeclarations.invariants.length
|
20
|
+
assert_equal 2, ExtendsInvariantDeclarations.invariants.length
|
21
|
+
end
|
22
|
+
|
23
|
+
class NonFunctionalArray < Array
|
24
|
+
include Handshake
|
25
|
+
invariant { false }
|
26
|
+
end
|
27
|
+
|
28
|
+
should "fail a very simple invariant check" do
|
29
|
+
assert_violation { NonFunctionalArray.new }
|
30
|
+
end
|
31
|
+
|
32
|
+
class PositiveBalance
|
33
|
+
include Handshake
|
34
|
+
invariant { @balance > 0 }
|
35
|
+
attr_accessor :balance
|
36
|
+
def initialize(balance); @balance = balance; end
|
37
|
+
end
|
38
|
+
|
39
|
+
should "check invariants that leverage new instance methods" do
|
40
|
+
assert_violation { PositiveBalance.new(-10) }
|
41
|
+
assert_violation { PositiveBalance.new 0 }
|
42
|
+
assert_passes { PositiveBalance.new 10 }
|
43
|
+
assert_violation {
|
44
|
+
pb = PositiveBalance.new(10); pb.balance = -10
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
class NonEmptyArray < Array
|
49
|
+
include Handshake
|
50
|
+
invariant { not empty? }
|
51
|
+
end
|
52
|
+
class ExtendsNonEmptyArray < NonEmptyArray; end
|
53
|
+
|
54
|
+
should "check invariants that leverage inherited instance methods" do
|
55
|
+
assert_violation { NonEmptyArray.new }
|
56
|
+
assert_passes { NonEmptyArray.new [1] }
|
57
|
+
assert_violation { ExtendsNonEmptyArray.new }
|
58
|
+
assert_passes { ExtendsNonEmptyArray.new [1] }
|
59
|
+
|
60
|
+
assert_violation { NonEmptyArray.new([1]).pop }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "contract" do
|
65
|
+
class MethodDeclarations
|
66
|
+
include Handshake
|
67
|
+
contract :accepts_str, String => anything
|
68
|
+
contract :accepts_int, Integer => anything
|
69
|
+
end
|
70
|
+
class ExtendsMethodDeclarations < MethodDeclarations; end
|
71
|
+
|
72
|
+
should "tie contract declaration to the method name to which they apply, and apply them to subclasses" do
|
73
|
+
assert MethodDeclarations.method_contracts.has_key?(:accepts_str)
|
74
|
+
assert MethodDeclarations.method_contracts.has_key?(:accepts_int)
|
75
|
+
assert ExtendsMethodDeclarations.method_contracts.has_key?(:accepts_str)
|
76
|
+
assert ExtendsMethodDeclarations.method_contracts.has_key?(:accepts_int)
|
77
|
+
end
|
78
|
+
|
79
|
+
class AcceptsString
|
80
|
+
include Handshake
|
81
|
+
contract :initialize, String => anything
|
82
|
+
def initialize(str); @str = str; end
|
83
|
+
contract String => anything
|
84
|
+
def str=(str); @str = str; end
|
85
|
+
end
|
86
|
+
class ExtendsAcceptsString < AcceptsString; end
|
87
|
+
|
88
|
+
class AcceptsIntegerInstead < AcceptsString
|
89
|
+
contract :initialize, Integer => anything
|
90
|
+
end
|
91
|
+
|
92
|
+
class AcceptsSymbolInstead < AcceptsString
|
93
|
+
contract :initialize, Symbol => anything
|
94
|
+
end
|
95
|
+
|
96
|
+
should "check simple type contracts for a single method argument" do
|
97
|
+
assert_violation { AcceptsString.new 3 }
|
98
|
+
assert_violation { AcceptsString.new :foo }
|
99
|
+
assert_passes { AcceptsString.new "string" }
|
100
|
+
assert_violation { AcceptsString.new("foo").str = 3 }
|
101
|
+
assert_violation { ExtendsAcceptsString.new 3 }
|
102
|
+
assert_violation { ExtendsAcceptsString.new :foo }
|
103
|
+
assert_passes { ExtendsAcceptsString.new "string" }
|
104
|
+
assert_violation { ExtendsAcceptsString.new("foo").str = 3 }
|
105
|
+
assert_violation { AcceptsIntegerInstead.new("foo") }
|
106
|
+
assert_passes { AcceptsIntegerInstead.new 3 }
|
107
|
+
assert_violation { AcceptsSymbolInstead.new "foo" }
|
108
|
+
assert_violation { AcceptsSymbolInstead.new 3 }
|
109
|
+
assert_passes { AcceptsSymbolInstead.new :foo }
|
110
|
+
end
|
111
|
+
|
112
|
+
class ReturnsString
|
113
|
+
include Handshake
|
114
|
+
contract anything => String
|
115
|
+
def call(val); val; end
|
116
|
+
end
|
117
|
+
class ExtendsReturnsString < ReturnsString; end
|
118
|
+
|
119
|
+
should "check simple type contracts for a single method return value" do
|
120
|
+
assert_violation { ReturnsString.new.call(1) }
|
121
|
+
assert_violation { ReturnsString.new.call(true) }
|
122
|
+
assert_passes { ReturnsString.new.call("foo") }
|
123
|
+
assert_violation { ExtendsReturnsString.new.call(1) }
|
124
|
+
assert_violation { ExtendsReturnsString.new.call(true) }
|
125
|
+
assert_passes { ExtendsReturnsString.new.call("foo") }
|
126
|
+
end
|
127
|
+
|
128
|
+
class ReturnsMultiple
|
129
|
+
include Handshake
|
130
|
+
contract [ String, Integer ] => anything
|
131
|
+
def call(arg1, arg2); return arg1, arg2; end
|
132
|
+
end
|
133
|
+
|
134
|
+
should "check simple type contracts for multiple method arguments" do
|
135
|
+
assert_violation { ReturnsMultiple.new.call("foo", "foo") }
|
136
|
+
assert_violation { ReturnsMultiple.new.call(3, 3) }
|
137
|
+
assert_passes { ReturnsMultiple.new.call("foo", 3) }
|
138
|
+
end
|
139
|
+
|
140
|
+
class AcceptsVarargs
|
141
|
+
include Handshake
|
142
|
+
contract [[ String ]] => anything
|
143
|
+
def initialize(*strs); @strs = strs; end
|
144
|
+
end
|
145
|
+
|
146
|
+
should "check simple type contracts for methods that accept varargs" do
|
147
|
+
assert_passes { AcceptsVarargs.new }
|
148
|
+
assert_violation { AcceptsVarargs.new(1, 2, 3) }
|
149
|
+
assert_violation { AcceptsVarargs.new("foo", 1, 2) }
|
150
|
+
assert_violation { AcceptsVarargs.new(:foo, "foo") }
|
151
|
+
assert_passes { AcceptsVarargs.new("foo") }
|
152
|
+
assert_passes { AcceptsVarargs.new("foo1", "foo2") }
|
153
|
+
end
|
154
|
+
|
155
|
+
class AcceptsBlock
|
156
|
+
include Handshake
|
157
|
+
contract Block => anything
|
158
|
+
def call1; end
|
159
|
+
contract Block => anything
|
160
|
+
def call2(&block); end
|
161
|
+
end
|
162
|
+
|
163
|
+
should "check simple type contracts that ensure methods can accept a block" do
|
164
|
+
assert_violation { AcceptsBlock.new.call1 }
|
165
|
+
assert_violation { AcceptsBlock.new.call2 }
|
166
|
+
assert_passes { AcceptsBlock.new.call1 { true } }
|
167
|
+
assert_passes { AcceptsBlock.new.call2 { true } }
|
168
|
+
assert_passes { AcceptsBlock.new.call1 { "foo" } }
|
169
|
+
assert_violation { AcceptsBlock.new.call1("foo") }
|
170
|
+
assert_violation { AcceptsBlock.new.call2("foo") }
|
171
|
+
end
|
172
|
+
|
173
|
+
class AcceptsWriter
|
174
|
+
include Handshake
|
175
|
+
contract String => anything
|
176
|
+
def val=(str); @str = str; end
|
177
|
+
end
|
178
|
+
|
179
|
+
should "check simple type contracts for writer methods" do
|
180
|
+
assert_violation { AcceptsWriter.new.val = 3 }
|
181
|
+
assert_violation { AcceptsWriter.new.val = :foo }
|
182
|
+
assert_passes { AcceptsWriter.new.val = "foo" }
|
183
|
+
end
|
184
|
+
|
185
|
+
class AcceptsMixed
|
186
|
+
include Handshake
|
187
|
+
contract [ String, String, [ Integer ], Block ] => String
|
188
|
+
def call(str1, str2, *ints, &block); "foo"; end
|
189
|
+
end
|
190
|
+
|
191
|
+
should "check simple type contracts for methods that accept multiple arguments, varargs, and a block" do
|
192
|
+
assert_violation { AcceptsMixed.new.call }
|
193
|
+
assert_violation { AcceptsMixed.new.call 3 }
|
194
|
+
assert_violation { AcceptsMixed.new.call "foo" }
|
195
|
+
assert_violation { AcceptsMixed.new.call "foo", 3 }
|
196
|
+
assert_violation { AcceptsMixed.new.call "foo", "bar" }
|
197
|
+
assert_passes { AcceptsMixed.new.call("foo", "bar") { true } }
|
198
|
+
assert_passes { AcceptsMixed.new.call("foo", "bar", 3) { true } }
|
199
|
+
assert_passes { AcceptsMixed.new.call("foo", "bar", 3, 4, 5) { true } }
|
200
|
+
end
|
201
|
+
|
202
|
+
class Superclass
|
203
|
+
include Handshake
|
204
|
+
contract Superclass => boolean?
|
205
|
+
def ==(other); self.class === other; end
|
206
|
+
end
|
207
|
+
|
208
|
+
class Subclass < Superclass; end
|
209
|
+
|
210
|
+
class AcceptsSuperAndSub
|
211
|
+
include Handshake
|
212
|
+
contract Superclass => anything
|
213
|
+
def call(cls); cls; end
|
214
|
+
end
|
215
|
+
|
216
|
+
should "accept subclasses in place of contracts defined to accept superclasses" do
|
217
|
+
assert_violation { AcceptsSuperAndSub.new.call 3 }
|
218
|
+
assert_passes { AcceptsSuperAndSub.new.call Superclass.new }
|
219
|
+
assert_passes { AcceptsSuperAndSub.new.call Subclass.new }
|
220
|
+
assert_passes { Superclass.new == Subclass.new }
|
221
|
+
end
|
222
|
+
|
223
|
+
context "clause" do
|
224
|
+
class AcceptsSimpleAssertion
|
225
|
+
include Handshake
|
226
|
+
equals_foo = clause {|o| o == "foo"}
|
227
|
+
contract [ equals_foo ] => anything
|
228
|
+
def call(foo)
|
229
|
+
return foo
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
should "check contracts defined by custom clauses" do
|
234
|
+
assert_violation { AcceptsSimpleAssertion.new.call }
|
235
|
+
assert_violation { AcceptsSimpleAssertion.new.call 3 }
|
236
|
+
assert_violation { AcceptsSimpleAssertion.new.call "bar", "bar" }
|
237
|
+
assert_passes { AcceptsSimpleAssertion.new.call "foo" }
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
context "all?" do
|
242
|
+
class AcceptsAll
|
243
|
+
include Handshake
|
244
|
+
equals_five = clause {|o| o == 5}
|
245
|
+
contract all?(Integer, equals_five) => anything
|
246
|
+
def initialize(n); end
|
247
|
+
end
|
248
|
+
|
249
|
+
should "check contracts that require all of the given clauses" do
|
250
|
+
assert_violation { AcceptsAll.new "foo" }
|
251
|
+
assert_violation { AcceptsAll.new 3 }
|
252
|
+
assert_violation { AcceptsAll.new 5.0 }
|
253
|
+
assert_passes { AcceptsAll.new 5 }
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
context "any?" do
|
258
|
+
class AcceptsAny
|
259
|
+
include Handshake
|
260
|
+
equals_five = clause {|o| o == 5}
|
261
|
+
equals_three = clause {|o| o == 3}
|
262
|
+
contract any?(equals_five, equals_three) => anything
|
263
|
+
def three_or_five(n); end
|
264
|
+
contract any?(String, Integer, Symbol) => anything
|
265
|
+
def str_int_sym(o); end
|
266
|
+
end
|
267
|
+
|
268
|
+
should "check contracts that require any of the given clauses" do
|
269
|
+
assert_violation { AcceptsAny.new.three_or_five "foo" }
|
270
|
+
assert_violation { AcceptsAny.new.three_or_five 7 }
|
271
|
+
assert_violation { AcceptsAny.new.three_or_five 8, 9 }
|
272
|
+
assert_passes { AcceptsAny.new.three_or_five 3 }
|
273
|
+
assert_passes { AcceptsAny.new.three_or_five 5 }
|
274
|
+
|
275
|
+
assert_violation { AcceptsAny.new.str_int_sym 5.3 }
|
276
|
+
assert_raises(ArgumentError) { AcceptsAny.new.str_int_sym "str", 3, :sym }
|
277
|
+
assert_passes { AcceptsAny.new.str_int_sym "str" }
|
278
|
+
assert_passes { AcceptsAny.new.str_int_sym 3 }
|
279
|
+
assert_passes { AcceptsAny.new.str_int_sym :foo }
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
context "not?" do
|
284
|
+
class AcceptsNot
|
285
|
+
include Handshake
|
286
|
+
contract not?(String) => anything
|
287
|
+
def initialize(not_str); end
|
288
|
+
end
|
289
|
+
|
290
|
+
should "check contracts that invert the given clause" do
|
291
|
+
assert_violation { AcceptsNot.new "string" }
|
292
|
+
assert_passes { AcceptsNot.new 3 }
|
293
|
+
assert_passes { AcceptsNot.new :symbol }
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
context "boolean?" do
|
298
|
+
class AcceptsBoolean
|
299
|
+
include Handshake
|
300
|
+
contract boolean? => anything
|
301
|
+
def initialize(bool); end
|
302
|
+
end
|
303
|
+
|
304
|
+
should "check contracts that ensure the given argument is a boolean" do
|
305
|
+
assert_violation { AcceptsBoolean.new "foo" }
|
306
|
+
assert_violation { AcceptsBoolean.new :foo }
|
307
|
+
assert_passes { AcceptsBoolean.new true }
|
308
|
+
assert_passes { AcceptsBoolean.new false }
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
context "nonzero?" do
|
313
|
+
class AcceptsNonzero
|
314
|
+
include Handshake
|
315
|
+
contract nonzero? => anything
|
316
|
+
def initialize(nonzero); end
|
317
|
+
end
|
318
|
+
|
319
|
+
should "check contracts that ensure the given argument is nonzero" do
|
320
|
+
assert_violation { AcceptsNonzero.new :foo }
|
321
|
+
assert_violation { AcceptsNonzero.new 0 }
|
322
|
+
assert_passes { AcceptsNonzero.new 3 }
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
context "hash_of?" do
|
327
|
+
class AcceptsHashOf
|
328
|
+
include Handshake
|
329
|
+
contract hash_of?(Symbol, String) => anything
|
330
|
+
def initialize(arg={}); end
|
331
|
+
end
|
332
|
+
|
333
|
+
def test_hash_of_sym_string
|
334
|
+
assert_passes { AcceptsHashOf.new({}) }
|
335
|
+
assert_passes { AcceptsHashOf.new({ :symbol => "String" }) }
|
336
|
+
assert_violation { AcceptsHashOf.new({ :another => :symbol }) }
|
337
|
+
assert_violation { AcceptsHashOf.new({ "two" => "strings" }) }
|
338
|
+
assert_violation { AcceptsHashOf.new({ false => true }) }
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
context "hash_with_keys" do
|
343
|
+
class AcceptsHashWithKeys
|
344
|
+
include Handshake
|
345
|
+
contract hash_with_keys(:foo, :bar) => anything
|
346
|
+
def initialize(options={}); end
|
347
|
+
end
|
348
|
+
|
349
|
+
def test_hash_with_keys_foo_bar
|
350
|
+
assert_passes { AcceptsHashWithKeys.new({}) }
|
351
|
+
assert_passes { AcceptsHashWithKeys.new({ :foo => "anything" }) }
|
352
|
+
assert_passes { AcceptsHashWithKeys.new({ :bar => "anything" }) }
|
353
|
+
assert_passes { AcceptsHashWithKeys.new({ :foo => "anything", :bar => "goes" }) }
|
354
|
+
assert_violation { AcceptsHashWithKeys.new({ :arbitrary => "key" }) }
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
context "hash_contract" do
|
359
|
+
class AcceptsHashContract
|
360
|
+
include Handshake
|
361
|
+
contract hash_contract({ :foo => String, :bar => Integer, :baz => Symbol }) => anything
|
362
|
+
def initialize(options={}); end
|
363
|
+
end
|
364
|
+
|
365
|
+
def test_hash_contract
|
366
|
+
assert_passes { AcceptsHashContract.new({}) }
|
367
|
+
assert_passes { AcceptsHashContract.new({ :foo => "bar"}) }
|
368
|
+
assert_violation { AcceptsHashContract.new({ :foo => :bar}) }
|
369
|
+
assert_passes { AcceptsHashContract.new({ :bar => 3 }) }
|
370
|
+
assert_violation { AcceptsHashContract.new({ :bar => "foo" }) }
|
371
|
+
assert_passes { AcceptsHashContract.new({ :baz => :foo }) }
|
372
|
+
assert_violation { AcceptsHashContract.new({ :baz => "baz" }) }
|
373
|
+
assert_passes { AcceptsHashContract.new({ :foo => "bar", :bar => 3, :baz => :qux }) }
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
context "responds_to?" do
|
378
|
+
class AcceptsRespondsTo
|
379
|
+
include Handshake
|
380
|
+
contract responds_to?(:each, :first) => anything
|
381
|
+
def initialize(duck_array); end
|
382
|
+
end
|
383
|
+
|
384
|
+
def test_responds_to_each_first
|
385
|
+
assert_violation { AcceptsRespondsTo.new({}) }
|
386
|
+
assert_violation { AcceptsRespondsTo.new "foo" }
|
387
|
+
assert_violation { AcceptsRespondsTo.new 3 }
|
388
|
+
assert_passes { AcceptsRespondsTo.new([]) }
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
context "is?" do
|
393
|
+
class AcceptsIsA
|
394
|
+
include Handshake
|
395
|
+
contract is?(:String) => is?(:Symbol)
|
396
|
+
def call_is_a(str); return str.intern; end
|
397
|
+
end
|
398
|
+
|
399
|
+
def test_accepts_is_string_symbol
|
400
|
+
assert_violation { AcceptsIsA.new.call_is_a(3) }
|
401
|
+
assert_violation { AcceptsIsA.new.call_is_a(:foo) }
|
402
|
+
assert_passes { AcceptsIsA.new.call_is_a("foo") }
|
403
|
+
end
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
context "before" do
|
408
|
+
class SimpleBeforeCondition
|
409
|
+
include Handshake
|
410
|
+
before { assert false }
|
411
|
+
def call_fails; end
|
412
|
+
def call_passes; end
|
413
|
+
end
|
414
|
+
class ExtendsSimpleBeforeCondition < SimpleBeforeCondition; end
|
415
|
+
|
416
|
+
def test_simple_before_condition
|
417
|
+
assert_equal(1, SimpleBeforeCondition.method_contracts.length)
|
418
|
+
assert_not_nil(SimpleBeforeCondition.method_contracts[:call_fails])
|
419
|
+
assert_violation { SimpleBeforeCondition.new.call_fails }
|
420
|
+
assert_passes { SimpleBeforeCondition.new.call_passes }
|
421
|
+
assert_equal(1, ExtendsSimpleBeforeCondition.method_contracts.length)
|
422
|
+
assert_not_nil(ExtendsSimpleBeforeCondition.method_contracts[:call_fails])
|
423
|
+
assert_violation { ExtendsSimpleBeforeCondition.new.call_fails }
|
424
|
+
assert_passes { ExtendsSimpleBeforeCondition.new.call_passes }
|
425
|
+
end
|
426
|
+
|
427
|
+
class ScopedBeforeCondition
|
428
|
+
include Handshake
|
429
|
+
def initialize(bool); @bool = bool; end
|
430
|
+
before { assert @bool }
|
431
|
+
def call; end
|
432
|
+
end
|
433
|
+
|
434
|
+
def test_scoped_before_condition
|
435
|
+
assert_violation { ScopedBeforeCondition.new(false).call }
|
436
|
+
assert_passes { ScopedBeforeCondition.new(true).call }
|
437
|
+
end
|
438
|
+
|
439
|
+
class BeforeClauseAssert
|
440
|
+
include Handshake
|
441
|
+
|
442
|
+
before do |arg|
|
443
|
+
assert_equal("foo", arg, "arg must equal foo")
|
444
|
+
end
|
445
|
+
|
446
|
+
def call(arg)
|
447
|
+
arg
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
def test_before_clause_assert
|
452
|
+
assert_violation { BeforeClauseAssert.new.call 3 }
|
453
|
+
assert_violation { BeforeClauseAssert.new.call "bar" }
|
454
|
+
assert_passes { BeforeClauseAssert.new.call "foo" }
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
context "after" do
|
459
|
+
class SimpleAfterCondition
|
460
|
+
include Handshake
|
461
|
+
after { |accepted, returned| assert returned }
|
462
|
+
def call(bool); bool; end
|
463
|
+
end
|
464
|
+
|
465
|
+
def test_simple_after_condition
|
466
|
+
assert_equal(1, SimpleAfterCondition.method_contracts.length)
|
467
|
+
assert_not_nil(SimpleAfterCondition.method_contracts[:call])
|
468
|
+
assert_violation { SimpleAfterCondition.new.call(false) }
|
469
|
+
assert_violation { SimpleAfterCondition.new.call(nil) }
|
470
|
+
assert_passes { SimpleAfterCondition.new.call(true) }
|
471
|
+
assert_passes { SimpleAfterCondition.new.call("foo") }
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
context "around" do
|
476
|
+
class SimpleAroundCondition
|
477
|
+
include Handshake
|
478
|
+
around {|arg| assert(!arg) }
|
479
|
+
def call(bool); bool; end
|
480
|
+
end
|
481
|
+
|
482
|
+
def test_simple_around_condition
|
483
|
+
[ 1, :foo, true, false, "bar", 8.3, nil ].each do |val|
|
484
|
+
assert_violation { SimpleAroundCondition.new.call(val) }
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
context "contract_reader, contract_writer, contract_accessor" do
|
490
|
+
class ContractAccessor
|
491
|
+
include Handshake
|
492
|
+
contract_reader :foo => String
|
493
|
+
contract_writer :bar => Integer
|
494
|
+
contract_accessor :baz => Symbol, :qux => Float
|
495
|
+
def initialize(foo=nil); @foo = foo; end
|
496
|
+
end
|
497
|
+
|
498
|
+
def test_contract_accessor
|
499
|
+
assert_equal(6, ContractAccessor.method_contracts.length)
|
500
|
+
assert_violation { ContractAccessor.new.foo }
|
501
|
+
assert_violation { ContractAccessor.new(3).foo }
|
502
|
+
assert_passes { ContractAccessor.new("foo").foo }
|
503
|
+
assert_violation { ContractAccessor.new.bar = "bar" }
|
504
|
+
assert_passes { ContractAccessor.new.bar = 3 }
|
505
|
+
assert_violation { ContractAccessor.new.baz = "3" }
|
506
|
+
assert_violation { ContractAccessor.new.qux = 3 }
|
507
|
+
assert_passes { ContractAccessor.new.baz = :baz }
|
508
|
+
assert_passes { ContractAccessor.new.qux = 3.3 }
|
509
|
+
end
|
510
|
+
end
|
511
|
+
|
512
|
+
context "checked_self" do
|
513
|
+
class CheckedSelf
|
514
|
+
include Handshake
|
515
|
+
def call_checked(obj)
|
516
|
+
checked_self.call(obj)
|
517
|
+
end
|
518
|
+
def call_unchecked(obj)
|
519
|
+
call(obj)
|
520
|
+
end
|
521
|
+
contract String => anything
|
522
|
+
def call(str); str; end
|
523
|
+
end
|
524
|
+
|
525
|
+
class ExtendsCheckedSelf < CheckedSelf
|
526
|
+
private
|
527
|
+
contract Numeric => anything
|
528
|
+
def call(n); n; end
|
529
|
+
end
|
530
|
+
|
531
|
+
def test_checked_self
|
532
|
+
assert_violation { CheckedSelf.new.call(5) }
|
533
|
+
assert_violation { CheckedSelf.new.call_checked(5) }
|
534
|
+
assert_passes { CheckedSelf.new.call_unchecked(5) }
|
535
|
+
assert_passes { CheckedSelf.new.call_checked("foo") }
|
536
|
+
assert_violation { ExtendsCheckedSelf.new.call_checked("foo") }
|
537
|
+
assert_passes { ExtendsCheckedSelf.new.call_checked(5) }
|
538
|
+
assert_passes { ExtendsCheckedSelf.new.call_unchecked("foo") }
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
context "Block" do
|
543
|
+
class CheckedBlockContract
|
544
|
+
include Handshake
|
545
|
+
|
546
|
+
contract [ anything, Block(String => Integer) ] => Integer
|
547
|
+
def yields(value); yield(value); end
|
548
|
+
|
549
|
+
contract [ anything, Block(String => Integer) ] => Integer
|
550
|
+
def calls(value, &block); block.call(value); end
|
551
|
+
end
|
552
|
+
|
553
|
+
def test_checked_block_contract_yields
|
554
|
+
assert_violation { CheckedBlockContract.new.yields("3") {|s| s.to_s } }
|
555
|
+
assert_violation { CheckedBlockContract.new.yields("3") {|s| "foo" } }
|
556
|
+
assert_violation { CheckedBlockContract.new.yields(3) {|s| s.to_i} }
|
557
|
+
assert_passes { CheckedBlockContract.new.yields("3") {|s| 3 } }
|
558
|
+
assert_passes { CheckedBlockContract.new.yields("3") {|s| s.to_i } }
|
559
|
+
end
|
560
|
+
|
561
|
+
def test_checked_block_contract_calls
|
562
|
+
assert_violation { CheckedBlockContract.new.calls("3") {|s| s.to_s } }
|
563
|
+
assert_violation { CheckedBlockContract.new.calls("3") {|s| "foo" } }
|
564
|
+
assert_violation { CheckedBlockContract.new.calls(3) {|s| s.to_i} }
|
565
|
+
assert_passes { CheckedBlockContract.new.calls("3") {|s| 3 } }
|
566
|
+
assert_passes { CheckedBlockContract.new.calls("3") {|s| s.to_i } }
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
context "suppress!" do
|
571
|
+
class ComprehensiveContracts
|
572
|
+
include Handshake
|
573
|
+
|
574
|
+
invariant("foo must always be true") { @foo == true }
|
575
|
+
|
576
|
+
contract /foo/ => /bar/
|
577
|
+
before do |arg|
|
578
|
+
assert_equal "foo", arg
|
579
|
+
end
|
580
|
+
after do |arg, returned|
|
581
|
+
assert_equal "bar", returned
|
582
|
+
end
|
583
|
+
def call(str); "baz"; end
|
584
|
+
end
|
585
|
+
|
586
|
+
should "be suppressed after declaration" do
|
587
|
+
Handshake.suppress!
|
588
|
+
assert Handshake.suppressed?
|
589
|
+
end
|
590
|
+
|
591
|
+
should "not enforce contracts" do
|
592
|
+
Handshake.suppress!
|
593
|
+
assert_nothing_raised { ComprehensiveContracts.new }
|
594
|
+
assert_nothing_raised { ComprehensiveContracts.new.call 3 }
|
595
|
+
end
|
596
|
+
end
|
597
|
+
end
|
598
|
+
end
|