handshake 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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. Handshake is written by Brian Guthrie
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
- # An array to which only strings may be added.
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) 2007 Brian Guthrie
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 'rake/packagetask'
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
- include FileUtils
13
- require File.join(File.dirname(__FILE__), 'lib', 'handshake', 'version')
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
- # Generate all the Rake tasks
40
- # Run 'rake -T' to see list of generated tasks (from gem root directory)
41
- hoe = Hoe.new(GEM_NAME, VERS) do |p|
42
- p.author = AUTHOR
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
@@ -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 :instantiate :new
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 @non_instantiable
59
- raise ContractViolation, "This class has been marked as abstract and cannot be instantiated."
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
- ### Instantiate the object itself.
72
- o = self.instantiate(*args, &block)
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
- raise ContractError, "Could not instantiate object" if o.nil?
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 *(meth_to_clause.keys)
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 *(meth_to_clause.keys)
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 &block
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 *SELF_PROXIED
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! *args, &block
528
+ contract.check_accepts!(*args, &block)
550
529
  contract.check_pre! @proxied, *args
551
530
  end
552
531
 
@@ -13,7 +13,7 @@ module Handshake
13
13
 
14
14
  def call(*args)
15
15
  Handshake.catch_contract("Contract violated in call to proc #{self}") do
16
- @contract.check_accepts! *args
16
+ @contract.check_accepts!(*args)
17
17
  end if checked?
18
18
 
19
19
  return_value = super(*args)
@@ -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? *respond_assertions
154
+ all?(*respond_assertions)
155
155
  end
156
156
 
157
157
  # Allows you to check whether the argument is_a? of the given symbol.
@@ -2,7 +2,7 @@ module Handshake # :nodoc:
2
2
  module VERSION # :nodoc:
3
3
  MAJOR = 0
4
4
  MINOR = 3
5
- TINY = 0
5
+ TINY = 1
6
6
  STRING = [MAJOR, MINOR, TINY].join('.')
7
7
  end
8
8
  end
@@ -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