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.
@@ -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