handshake 0.3.0 → 0.3.1
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/{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
|