contracts 0.4 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,75 @@
1
+ class InvariantError < StandardError
2
+ def to_contract_error
3
+ self
4
+ end
5
+ end
6
+
7
+ module Contracts
8
+ module Invariants
9
+ def self.included(base)
10
+ common base
11
+ end
12
+
13
+ def self.extended(base)
14
+ common base
15
+ end
16
+
17
+ def self.common(base)
18
+ return if base.respond_to?(:Invariant)
19
+
20
+ base.extend(InvariantExtension)
21
+ end
22
+
23
+ def verify_invariants!(method)
24
+ return unless self.class.respond_to?(:invariants)
25
+
26
+ self.class.invariants.each do |invariant|
27
+ invariant.check_on(self, method)
28
+ end
29
+ end
30
+
31
+ module InvariantExtension
32
+ def Invariant(name, &condition)
33
+ return if ENV["NO_CONTRACTS"]
34
+
35
+ invariants << Invariant.new(self, name, &condition)
36
+ end
37
+
38
+ def invariants
39
+ @invariants ||= []
40
+ end
41
+ end
42
+
43
+ class Invariant
44
+ def initialize(klass, name, &condition)
45
+ @klass, @name, @condition = klass, name, condition
46
+ end
47
+
48
+ def expected
49
+ "#{@name} condition to be true"
50
+ end
51
+
52
+ def check_on(target, method)
53
+ return if target.instance_eval(&@condition)
54
+
55
+ self.class.failure_callback(:expected => expected,
56
+ :actual => false,
57
+ :target => target,
58
+ :method => method)
59
+ end
60
+
61
+ def self.failure_callback(data)
62
+ raise InvariantError, failure_msg(data)
63
+ end
64
+
65
+ def self.failure_msg(data)
66
+ %{Invariant violation:
67
+ Expected: #{data[:expected]}
68
+ Actual: #{data[:actual]}
69
+ Value guarded in: #{data[:target].class}::#{Support.method_name(data[:method])}
70
+ At: #{Support.method_position(data[:method])}}
71
+ end
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,22 @@
1
+ module Contracts
2
+ module Support
3
+
4
+ def self.method_position(method)
5
+ if RUBY_VERSION =~ /^1\.8/
6
+ if method.respond_to?(:__file__)
7
+ method.__file__ + ":" + method.__line__.to_s
8
+ else
9
+ method.inspect
10
+ end
11
+ else
12
+ file, line = method.source_location
13
+ file + ":" + line.to_s
14
+ end
15
+ end
16
+
17
+ def self.method_name(method)
18
+ method.is_a?(Proc) ? "Proc" : method.name
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,3 @@
1
+ module Contracts
2
+ VERSION = "0.5"
3
+ end
@@ -0,0 +1,216 @@
1
+ include Contracts
2
+
3
+ RSpec.describe "Contracts:" do
4
+ before :all do
5
+ @o = Object.new
6
+ end
7
+
8
+ describe "Num:" do
9
+ it "should pass for Fixnums" do
10
+ expect { @o.double(2) }.to_not raise_error
11
+ end
12
+
13
+ it "should pass for Floats" do
14
+ expect { @o.double(2.2) }.to_not raise_error
15
+ end
16
+
17
+ it "should fail for Strings" do
18
+ expect { @o.double("bad") }.to raise_error(ContractError)
19
+ end
20
+ end
21
+
22
+ describe "Pos:" do
23
+ it "should pass for positive numbers" do
24
+ expect { @o.pos_test(1) }.to_not raise_error
25
+ end
26
+
27
+ it "should fail for negative numbers" do
28
+ expect { @o.pos_test(-1) }.to raise_error(ContractError)
29
+ end
30
+ end
31
+
32
+ describe "Neg:" do
33
+ it "should pass for negative numbers" do
34
+ expect { @o.neg_test(-1) }.to_not raise_error
35
+ end
36
+
37
+ it "should fail for positive numbers" do
38
+ expect { @o.neg_test(1) }.to raise_error(ContractError)
39
+ end
40
+ end
41
+
42
+ describe "Any:" do
43
+ it "should pass for numbers" do
44
+ expect { @o.show(1) }.to_not raise_error
45
+ end
46
+ it "should pass for strings" do
47
+ expect { @o.show("bad") }.to_not raise_error
48
+ end
49
+ it "should pass for procs" do
50
+ expect { @o.show(lambda {}) }.to_not raise_error
51
+ end
52
+ it "should pass for nil" do
53
+ expect { @o.show(nil) }.to_not raise_error
54
+ end
55
+ end
56
+
57
+ describe "None:" do
58
+ it "should fail for numbers" do
59
+ expect { @o.fail_all(1) }.to raise_error(ContractError)
60
+ end
61
+ it "should fail for strings" do
62
+ expect { @o.fail_all("bad") }.to raise_error(ContractError)
63
+ end
64
+ it "should fail for procs" do
65
+ expect { @o.fail_all(lambda {}) }.to raise_error(ContractError)
66
+ end
67
+ it "should fail for nil" do
68
+ expect { @o.fail_all(nil) }.to raise_error(ContractError)
69
+ end
70
+ end
71
+
72
+ describe "Or:" do
73
+ it "should pass for nums" do
74
+ expect { @o.num_or_string(1) }.to_not raise_error
75
+ end
76
+
77
+ it "should pass for strings" do
78
+ expect { @o.num_or_string("bad") }.to_not raise_error
79
+ end
80
+
81
+ it "should fail for nil" do
82
+ expect { @o.num_or_string(nil) }.to raise_error(ContractError)
83
+ end
84
+ end
85
+
86
+ describe "Xor:" do
87
+ it "should pass for an object with a method :good" do
88
+ expect { @o.xor_test(A.new) }.to_not raise_error
89
+ end
90
+
91
+ it "should pass for an object with a method :bad" do
92
+ expect { @o.xor_test(B.new) }.to_not raise_error
93
+ end
94
+
95
+ it "should fail for an object with neither method" do
96
+ expect { @o.xor_test(1) }.to raise_error(ContractError)
97
+ end
98
+
99
+ it "should fail for an object with both methods :good and :bad" do
100
+ expect { @o.xor_test(C.new) }.to raise_error(ContractError)
101
+ end
102
+ end
103
+
104
+ describe "And:" do
105
+ it "should pass for an object of class A that has a method :good" do
106
+ expect { @o.and_test(A.new) }.to_not raise_error
107
+ end
108
+
109
+ it "should fail for an object that has a method :good but isn't of class A" do
110
+ expect { @o.and_test(C.new) }.to raise_error(ContractError)
111
+ end
112
+ end
113
+
114
+ describe "RespondTo:" do
115
+ it "should pass for an object that responds to :good" do
116
+ expect { @o.responds_test(A.new) }.to_not raise_error
117
+ end
118
+
119
+ it "should fail for an object that doesn't respond to :good" do
120
+ expect { @o.responds_test(B.new) }.to raise_error(ContractError)
121
+ end
122
+ end
123
+
124
+ describe "Send:" do
125
+ it "should pass for an object that returns true for method :good" do
126
+ expect { @o.send_test(A.new) }.to_not raise_error
127
+ end
128
+
129
+ it "should fail for an object that returns false for method :good" do
130
+ expect { @o.send_test(C.new) }.to raise_error(ContractError)
131
+ end
132
+ end
133
+
134
+ describe "Exactly:" do
135
+ it "should pass for an object that is exactly a Parent" do
136
+ expect { @o.exactly_test(Parent.new) }.to_not raise_error
137
+ end
138
+
139
+ it "should fail for an object that inherits from Parent" do
140
+ expect { @o.exactly_test(Child.new) }.to raise_error(ContractError)
141
+ end
142
+
143
+ it "should fail for an object that is not related to Parent at all" do
144
+ expect { @o.exactly_test(A.new) }.to raise_error(ContractError)
145
+ end
146
+ end
147
+
148
+ describe "Not:" do
149
+ it "should pass for an argument that isn't nil" do
150
+ expect { @o.not_nil(1) }.to_not raise_error
151
+ end
152
+
153
+ it "should fail for nil" do
154
+ expect { @o.not_nil(nil) }.to raise_error(ContractError)
155
+ end
156
+ end
157
+
158
+ describe "ArrayOf:" do
159
+ it "should pass for an array of nums" do
160
+ expect { @o.product([1, 2, 3]) }.to_not raise_error
161
+ end
162
+
163
+ it "should fail for an array with one non-num" do
164
+ expect { @o.product([1, 2, 3, "bad"]) }.to raise_error(ContractError)
165
+ end
166
+
167
+ it "should fail for a non-array" do
168
+ expect { @o.product(1) }.to raise_error(ContractError)
169
+ end
170
+ end
171
+
172
+ describe "Bool:" do
173
+ it "should pass for an argument that is a boolean" do
174
+ expect { @o.bool_test(true) }.to_not raise_error
175
+ expect { @o.bool_test(false) }.to_not raise_error
176
+ end
177
+
178
+ it "should fail for nil" do
179
+ expect { @o.bool_test(nil) }.to raise_error(ContractError)
180
+ end
181
+ end
182
+
183
+ describe "Maybe:" do
184
+ it "should pass for nums" do
185
+ expect(@o.maybe_double(1)).to eq(2)
186
+ end
187
+
188
+ it "should pass for nils" do
189
+ expect(@o.maybe_double(nil)).to eq(nil)
190
+ end
191
+
192
+ it "should fail for strings" do
193
+ expect { @o.maybe_double("foo") }.to raise_error(ContractError)
194
+ end
195
+ end
196
+
197
+ describe 'HashOf:' do
198
+ context 'given a fulfilled contract' do
199
+ it { expect(@o.gives_max_value(:panda => 1, :bamboo => 2)).to eq(2) }
200
+ end
201
+
202
+ context 'given an unfulfilled contract' do
203
+ it { expect { @o.gives_max_value(:panda => '1', :bamboo => '2') }.to raise_error(ContractError) }
204
+ end
205
+
206
+ describe '#to_s' do
207
+ context 'given Symbol => String' do
208
+ it { expect(HashOf[Symbol, String].to_s).to eq('Hash<Symbol, String>') }
209
+ end
210
+
211
+ context 'given String => Num' do
212
+ it { expect(HashOf[String, Num].to_s).to eq('Hash<String, Contracts::Num>') }
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,273 @@
1
+ include Contracts
2
+
3
+ RSpec.describe "Contracts:" do
4
+ before :all do
5
+ @o = Object.new
6
+ end
7
+
8
+ describe "basic" do
9
+ it "should fail for insufficient arguments" do
10
+ expect {
11
+ @o.hello
12
+ }.to raise_error
13
+ end
14
+
15
+ it "should fail for insufficient contracts" do
16
+ expect { @o.bad_double(2) }.to raise_error(ContractError)
17
+ end
18
+ end
19
+
20
+ describe "pattern matching" do
21
+ let(:string_with_hello) { "Hello, world" }
22
+ let(:string_without_hello) { "Hi, world" }
23
+ let(:expected_decorated_string) { "Hello, world!" }
24
+ subject { PatternMatchingExample.new }
25
+
26
+ it "should work as expected when there is no contract violation" do
27
+ expect(
28
+ subject.process_request(PatternMatchingExample::Success[string_with_hello])
29
+ ).to eq(PatternMatchingExample::Success[expected_decorated_string])
30
+
31
+ expect(
32
+ subject.process_request(PatternMatchingExample::Failure.new)
33
+ ).to be_a(PatternMatchingExample::Failure)
34
+ end
35
+
36
+ it "should not fall through to next pattern when there is a deep contract violation" do
37
+ expect(PatternMatchingExample::Failure).not_to receive(:is_a?)
38
+ expect {
39
+ subject.process_request(PatternMatchingExample::Success[string_without_hello])
40
+ }.to raise_error(ContractError)
41
+ end
42
+
43
+ it "should fail when the pattern-matched method's contract fails" do
44
+ expect {
45
+ subject.process_request("bad input")
46
+ }.to raise_error(ContractError)
47
+ end
48
+
49
+ context "when failure_callback was overriden" do
50
+ before do
51
+ ::Contract.override_failure_callback do |_data|
52
+ raise RuntimeError, "contract violation"
53
+ end
54
+ end
55
+
56
+ it "calls a method when first pattern matches" do
57
+ expect(
58
+ subject.process_request(PatternMatchingExample::Success[string_with_hello])
59
+ ).to eq(PatternMatchingExample::Success[expected_decorated_string])
60
+ end
61
+
62
+ it "falls through to 2nd pattern when first pattern does not match" do
63
+ expect(
64
+ subject.process_request(PatternMatchingExample::Failure.new)
65
+ ).to be_a(PatternMatchingExample::Failure)
66
+ end
67
+
68
+ it "uses overriden failure_callback when pattern matching fails" do
69
+ expect {
70
+ subject.process_request("hello")
71
+ }.to raise_error(RuntimeError, /contract violation/)
72
+ end
73
+ end
74
+ end
75
+
76
+ describe "instance methods" do
77
+ it "should allow two classes to have the same method with different contracts" do
78
+ a = A.new
79
+ b = B.new
80
+ expect {
81
+ a.triple(5)
82
+ b.triple("a string")
83
+ }.to_not raise_error
84
+ end
85
+ end
86
+
87
+ describe "instance and class methods" do
88
+ it "should allow a class to have an instance method and a class method with the same name" do
89
+ a = A.new
90
+ expect {
91
+ a.instance_and_class_method(5)
92
+ A.instance_and_class_method("a string")
93
+ }.to_not raise_error
94
+ end
95
+ end
96
+
97
+ describe "class methods" do
98
+ it "should pass for correct input" do
99
+ expect { Object.a_class_method(2) }.to_not raise_error
100
+ end
101
+
102
+ it "should fail for incorrect input" do
103
+ expect { Object.a_class_method("bad") }.to raise_error(ContractError)
104
+ end
105
+ end
106
+
107
+ it "should work for functions with no args" do
108
+ expect { @o.no_args }.to_not raise_error
109
+ end
110
+
111
+ describe "classes" do
112
+ it "should pass for correct input" do
113
+ expect { @o.hello("calvin") }.to_not raise_error
114
+ end
115
+
116
+ it "should fail for incorrect input" do
117
+ expect { @o.hello(1) }.to raise_error(ContractError)
118
+ end
119
+ end
120
+
121
+ describe "classes with a valid? class method" do
122
+ it "should pass for correct input" do
123
+ expect { @o.double(2) }.to_not raise_error
124
+ end
125
+
126
+ it "should fail for incorrect input" do
127
+ expect { @o.double("bad") }.to raise_error(ContractError)
128
+ end
129
+ end
130
+
131
+ describe "Procs" do
132
+ it "should pass for correct input" do
133
+ expect { @o.square(2) }.to_not raise_error
134
+ end
135
+
136
+ it "should fail for incorrect input" do
137
+ expect { @o.square("bad") }.to raise_error(ContractError)
138
+ end
139
+ end
140
+
141
+ describe "Arrays" do
142
+ it "should pass for correct input" do
143
+ expect { @o.sum_three([1, 2, 3]) }.to_not raise_error
144
+ end
145
+
146
+ it "should fail for insufficient items" do
147
+ expect { @o.square([1, 2]) }.to raise_error(ContractError)
148
+ end
149
+
150
+ it "should fail for some incorrect elements" do
151
+ expect { @o.sum_three([1, 2, "three"]) }.to raise_error(ContractError)
152
+ end
153
+ end
154
+
155
+ describe "Hashes" do
156
+ it "should pass for exact correct input" do
157
+ expect { @o.person({:name => "calvin", :age => 10}) }.to_not raise_error
158
+ end
159
+
160
+ it "should pass even if some keys don't have contracts" do
161
+ expect { @o.person({:name => "calvin", :age => 10, :foo => "bar"}) }.to_not raise_error
162
+ end
163
+
164
+ it "should fail if a key with a contract on it isn't provided" do
165
+ expect { @o.person({:name => "calvin"}) }.to raise_error(ContractError)
166
+ end
167
+
168
+ it "should fail for incorrect input" do
169
+ expect { @o.person({:name => 50, :age => 10}) }.to raise_error(ContractError)
170
+ end
171
+ end
172
+
173
+ describe "blocks" do
174
+ it "should pass for correct input" do
175
+ expect { @o.do_call {
176
+ 2 + 2
177
+ }}.to_not raise_error
178
+ end
179
+
180
+ it "should fail for incorrect input" do
181
+ expect { @o.do_call(nil) }.to raise_error(ContractError)
182
+ end
183
+ end
184
+
185
+ describe "varargs" do
186
+ it "should pass for correct input" do
187
+ expect { @o.sum(1, 2, 3) }.to_not raise_error
188
+ end
189
+
190
+ it "should fail for incorrect input" do
191
+ expect { @o.sum(1, 2, "bad") }.to raise_error(ContractError)
192
+ end
193
+ end
194
+
195
+ describe "contracts on functions" do
196
+ it "should pass for a function that passes the contract" do
197
+ expect { @o.map([1, 2, 3], lambda { |x| x + 1 }) }.to_not raise_error
198
+ end
199
+
200
+ it "should fail for a function that doesn't pass the contract" do
201
+ expect { @o.map([1, 2, 3], lambda { |x| "bad return value" }) }.to raise_error(ContractError)
202
+ end
203
+ end
204
+
205
+ describe "default args to functions" do
206
+ it "should work for a function call that relies on default args" do
207
+ expect { @o.default_args }.to_not raise_error
208
+ expect { @o.default_args("foo") }.to raise_error(ContractError)
209
+ end
210
+ end
211
+
212
+ describe "classes" do
213
+ it "should not fail for an object that is the exact type as the contract" do
214
+ p = Parent.new
215
+ expect { @o.id_(p) }.to_not raise_error
216
+ end
217
+
218
+ it "should not fail for an object that is a subclass of the type in the contract" do
219
+ c = Child.new
220
+ expect { @o.id_(c) }.to_not raise_error
221
+ end
222
+ end
223
+
224
+ describe "failure callbacks" do
225
+ before :each do
226
+ ::Contract.override_failure_callback do |_data|
227
+ should_call
228
+ end
229
+ end
230
+
231
+ context "when failure_callback returns false" do
232
+ let(:should_call) { false }
233
+
234
+ it "does not call a function for which the contract fails" do
235
+ res = @o.double("bad")
236
+ expect(res).to eq(nil)
237
+ end
238
+ end
239
+
240
+ context "when failure_callback returns true" do
241
+ let(:should_call) { true }
242
+
243
+ it "calls a function for which the contract fails" do
244
+ res = @o.double("bad")
245
+ expect(res).to eq("badbad")
246
+ end
247
+ end
248
+ end
249
+
250
+ describe "functype" do
251
+ it "should correctly print out a instance method's type" do
252
+ expect(@o.functype(:double)).not_to eq("")
253
+ end
254
+
255
+ it "should correctly print out a class method's type" do
256
+ expect(A.functype(:a_class_method)).not_to eq("")
257
+ end
258
+ end
259
+
260
+ describe "private methods" do
261
+ it "should raise an error if you try to access a private method" do
262
+ expect { @o.a_private_method }.to raise_error
263
+ end
264
+ end
265
+
266
+ describe "inherited methods" do
267
+ it "should apply the contract to an inherited method" do
268
+ c = Child.new
269
+ expect { c.double(2) }.to_not raise_error
270
+ expect { c.double("asd") }.to raise_error
271
+ end
272
+ end
273
+ end