contracts 0.4 → 0.5

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