contracts 0.0.6 → 0.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/builtin_contracts.rb +117 -3
- data/lib/contracts.rb +22 -7
- data/lib/decorators.rb +20 -28
- data/lib/foo.rb +6 -7
- data/lib/test.rb +21 -1
- data/lib/testable.rb +69 -0
- metadata +4 -3
data/lib/builtin_contracts.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'testable'
|
2
|
+
|
1
3
|
=begin rdoc
|
2
4
|
This module contains all the builtin contracts.
|
3
5
|
If you want to use them, first:
|
@@ -21,6 +23,14 @@ module Contracts
|
|
21
23
|
def self.valid? val
|
22
24
|
val.is_a? Numeric
|
23
25
|
end
|
26
|
+
|
27
|
+
def self.testable?
|
28
|
+
true
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.test_data
|
32
|
+
[-1, 0, 1, 1.5, 50000]
|
33
|
+
end
|
24
34
|
end
|
25
35
|
|
26
36
|
# Check that an argument is a positive number.
|
@@ -28,6 +38,14 @@ module Contracts
|
|
28
38
|
def self.valid? val
|
29
39
|
val > 0
|
30
40
|
end
|
41
|
+
|
42
|
+
def testable?
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.test_data
|
47
|
+
(0..5).map { rand(999) + 1 }
|
48
|
+
end
|
31
49
|
end
|
32
50
|
|
33
51
|
# Check that an argument is a negative number.
|
@@ -35,6 +53,14 @@ module Contracts
|
|
35
53
|
def self.valid? val
|
36
54
|
val < 0
|
37
55
|
end
|
56
|
+
|
57
|
+
def testable?
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.test_data
|
62
|
+
(0..5).map { (rand(999) + 1) * -1 }
|
63
|
+
end
|
38
64
|
end
|
39
65
|
|
40
66
|
# Passes for any argument.
|
@@ -83,6 +109,19 @@ module Contracts
|
|
83
109
|
def to_s
|
84
110
|
@vals[0, @vals.size-1].join(", ") + " or " + @vals[-1].to_s
|
85
111
|
end
|
112
|
+
|
113
|
+
# this can only be tested IF all the sub-contracts have a test_data method
|
114
|
+
def testable?
|
115
|
+
@vals.all? do |val|
|
116
|
+
Testable.testable?(val)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def test_data
|
121
|
+
@vals.map { |val|
|
122
|
+
Testable.test_data(val)
|
123
|
+
}.flatten
|
124
|
+
end
|
86
125
|
end
|
87
126
|
|
88
127
|
# Takes a variable number of contracts.
|
@@ -104,6 +143,18 @@ module Contracts
|
|
104
143
|
def to_s
|
105
144
|
@vals[0, @vals.size-1].join(", ") + " xor " + @vals[-1].to_s
|
106
145
|
end
|
146
|
+
|
147
|
+
def testable?
|
148
|
+
@vals.all? do |val|
|
149
|
+
Testable.testable? val
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_data
|
154
|
+
@vals.map { |val|
|
155
|
+
Testable.test_data val
|
156
|
+
}.flatten
|
157
|
+
end
|
107
158
|
end
|
108
159
|
|
109
160
|
# Takes a variable number of contracts.
|
@@ -129,8 +180,8 @@ module Contracts
|
|
129
180
|
# Takes a variable number of method names as symbols.
|
130
181
|
# The contract passes if the argument responds to all
|
131
182
|
# of those methods.
|
132
|
-
# Example: <tt>
|
133
|
-
class
|
183
|
+
# Example: <tt>RespondTo[:password, :credit_card]</tt>
|
184
|
+
class RespondTo < CallableClass
|
134
185
|
def initialize(*meths)
|
135
186
|
@meths = meths
|
136
187
|
end
|
@@ -222,6 +273,14 @@ module Contracts
|
|
222
273
|
def to_s
|
223
274
|
"an array of #{@contract}"
|
224
275
|
end
|
276
|
+
|
277
|
+
def testable?
|
278
|
+
Testable.testable? @contract
|
279
|
+
end
|
280
|
+
|
281
|
+
def test_data
|
282
|
+
[[], [Testable.test_data(@contract)], [Testable.test_data(@contract), Testable.test_data(@contract)]]
|
283
|
+
end
|
225
284
|
end
|
226
285
|
|
227
286
|
# Used for <tt>*args</tt> (variadic functions). Takes a contract
|
@@ -237,5 +296,60 @@ module Contracts
|
|
237
296
|
def to_s
|
238
297
|
"Args[#{@contract}]"
|
239
298
|
end
|
240
|
-
|
299
|
+
|
300
|
+
def testable?
|
301
|
+
Testable.testable? @contract
|
302
|
+
end
|
303
|
+
|
304
|
+
def test_data
|
305
|
+
[[], [Testable.test_data(@contract)], [Testable.test_data(@contract), Testable.test_data(@contract)]]
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
class Bool
|
310
|
+
def self.valid? val
|
311
|
+
val.is_a?(TrueClass) || val.is_a?(FalseClass)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
class ::Hash
|
316
|
+
def testable?
|
317
|
+
self.values.all? do |val|
|
318
|
+
Testable.testable?(val)
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def test_data
|
323
|
+
keys = self.keys
|
324
|
+
_vals = keys.map do |key|
|
325
|
+
ret = Testable.test_data(self[key])
|
326
|
+
if ret.is_a? Array
|
327
|
+
ret
|
328
|
+
else
|
329
|
+
[ret]
|
330
|
+
end
|
331
|
+
end
|
332
|
+
all_vals = Testable.product(_vals)
|
333
|
+
hashes = []
|
334
|
+
all_vals.each do |vals|
|
335
|
+
hash = {}
|
336
|
+
keys.zip(vals).each do |key, val|
|
337
|
+
hash[key] = val
|
338
|
+
end
|
339
|
+
hashes << hash
|
340
|
+
end
|
341
|
+
hashes
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
class ::String
|
346
|
+
def self.testable?
|
347
|
+
true
|
348
|
+
end
|
349
|
+
|
350
|
+
def self.test_data
|
351
|
+
# send a random string
|
352
|
+
('a'..'z').to_a.shuffle[0, 10].join
|
353
|
+
end
|
354
|
+
end
|
241
355
|
end
|
data/lib/contracts.rb
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
require 'decorators'
|
2
2
|
require 'builtin_contracts'
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
|
4
|
+
module Contracts
|
5
|
+
def self.included(base)
|
6
|
+
base.extend MethodDecorators
|
7
|
+
end
|
7
8
|
|
8
|
-
|
9
|
-
|
9
|
+
def self.extended(base)
|
10
|
+
base.extend MethodDecorators
|
11
|
+
end
|
10
12
|
end
|
11
13
|
|
12
|
-
|
13
14
|
# This is the main Contract class. When you write a new contract, you'll
|
14
15
|
# write it as:
|
15
16
|
#
|
@@ -18,7 +19,7 @@ end
|
|
18
19
|
# This class also provides useful callbacks and a validation method.
|
19
20
|
class Contract < Decorator
|
20
21
|
attr_accessor :contracts, :klass, :method
|
21
|
-
decorator_name :contract
|
22
|
+
# decorator_name :contract
|
22
23
|
def initialize(klass, method, *contracts)
|
23
24
|
@klass, @method, @contracts = klass, method, contracts
|
24
25
|
end
|
@@ -185,3 +186,17 @@ Contracts: #{@contracts.map { |t| t.is_a?(Class) ? t.name : t.class.name }.join(
|
|
185
186
|
end
|
186
187
|
end
|
187
188
|
end
|
189
|
+
|
190
|
+
# convenience function for small scripts.
|
191
|
+
# Use as:
|
192
|
+
#
|
193
|
+
# use_contracts self
|
194
|
+
#
|
195
|
+
# And then you can use contracts on functions
|
196
|
+
# that aren't in any module or class.
|
197
|
+
def use_contracts(this)
|
198
|
+
this.class.send(:include, Contracts)
|
199
|
+
def this.Contract(*args)
|
200
|
+
self.class.Contract(*args)
|
201
|
+
end
|
202
|
+
end
|
data/lib/decorators.rb
CHANGED
@@ -5,27 +5,11 @@ module MethodDecorators
|
|
5
5
|
end
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
const = Object.const_get(name)
|
14
|
-
elsif Decorator.decorators.key?(name)
|
15
|
-
const = Decorator.decorators[name]
|
16
|
-
else
|
17
|
-
return super
|
18
|
-
end
|
19
|
-
|
20
|
-
instance_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
21
|
-
def #{name}(*args, &blk)
|
22
|
-
decorate(#{const.name}, *args, &blk)
|
23
|
-
end
|
24
|
-
ruby_eval
|
25
|
-
|
26
|
-
send(name, *args, &blk)
|
27
|
-
end
|
28
|
-
|
8
|
+
# first, when you write a contract, the decorate method gets called which
|
9
|
+
# sets the @decorators variable. Then when the next method after the contract
|
10
|
+
# is defined, method_added is called and we look at the @decorators variable
|
11
|
+
# to find the decorator for that method. This is how we associate decorators
|
12
|
+
# with methods.
|
29
13
|
def method_added(name)
|
30
14
|
return unless @decorators
|
31
15
|
|
@@ -33,13 +17,21 @@ module MethodDecorators
|
|
33
17
|
@decorators = nil
|
34
18
|
@decorated_methods ||= Hash.new {|h,k| h[k] = []}
|
35
19
|
|
20
|
+
# attr_accessor on the class variable decorated_methods
|
36
21
|
class << self; attr_accessor :decorated_methods; end
|
37
22
|
|
38
23
|
decorators.each do |klass, args|
|
24
|
+
# a reference to the method gets passed into the contract here. This is good because
|
25
|
+
# we are going to redefine this method with a new name below...so this reference is
|
26
|
+
# now the *only* reference to the old method that exists.
|
39
27
|
decorator = klass.respond_to?(:new) ? klass.new(self, instance_method(name), *args) : klass
|
40
28
|
@decorated_methods[name] << decorator
|
41
29
|
end
|
42
30
|
|
31
|
+
# in place of this method, we are going to define our own method. This method
|
32
|
+
# just calls the decorator passing in all args that were to be passed into the method.
|
33
|
+
# The decorator in turn has a reference to the actual method, so it can call it
|
34
|
+
# on its own, after doing it's decorating of course.
|
43
35
|
class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
44
36
|
def #{name}(*args, &blk)
|
45
37
|
ret = nil
|
@@ -49,6 +41,7 @@ module MethodDecorators
|
|
49
41
|
ret
|
50
42
|
end
|
51
43
|
ruby_eval
|
44
|
+
super
|
52
45
|
end
|
53
46
|
|
54
47
|
def decorate(klass, *args)
|
@@ -58,19 +51,18 @@ module MethodDecorators
|
|
58
51
|
end
|
59
52
|
|
60
53
|
class Decorator
|
61
|
-
class
|
62
|
-
|
63
|
-
def decorator_name(name)
|
64
|
-
Decorator.decorators ||= {}
|
65
|
-
Decorator.decorators[name] = self
|
66
|
-
end
|
67
|
-
end
|
54
|
+
# an attr_accessor for a class variable:
|
55
|
+
class << self; attr_accessor :decorators; end
|
68
56
|
|
69
57
|
def self.inherited(klass)
|
70
58
|
name = klass.name.gsub(/^./) {|m| m.downcase}
|
71
59
|
|
72
60
|
return if name =~ /^[^A-Za-z_]/ || name =~ /[^0-9A-Za-z_]/
|
73
61
|
|
62
|
+
# the file and line parameters set the text for error messages
|
63
|
+
# make a new method that is the name of your decorator.
|
64
|
+
# that method accepts random args and a block.
|
65
|
+
# inside, `decorate` is called with those params.
|
74
66
|
MethodDecorators.module_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
75
67
|
def #{klass}(*args, &blk)
|
76
68
|
decorate(#{klass}, *args, &blk)
|
data/lib/foo.rb
CHANGED
data/lib/test.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
require 'contracts'
|
2
|
+
require 'testable'
|
2
3
|
include Contracts
|
3
4
|
|
4
5
|
class Object
|
6
|
+
|
5
7
|
Contract Num, Num
|
6
8
|
def double(x)
|
7
9
|
x * 2
|
@@ -35,6 +37,24 @@ class Object
|
|
35
37
|
def person(data)
|
36
38
|
p data
|
37
39
|
end
|
40
|
+
|
41
|
+
Contract Num, Num
|
42
|
+
def test(x)
|
43
|
+
x + 2
|
44
|
+
end
|
38
45
|
end
|
39
46
|
|
40
|
-
|
47
|
+
# this doesn't work
|
48
|
+
# p Object.send(:sum, 1, 2)
|
49
|
+
|
50
|
+
# but this does:
|
51
|
+
# p send(:sum, 1, 2)
|
52
|
+
#
|
53
|
+
# why???
|
54
|
+
|
55
|
+
Testable.check_all
|
56
|
+
# Testable.check(method(:double))
|
57
|
+
|
58
|
+
# p Object.test(5)
|
59
|
+
#
|
60
|
+
Object.Hello
|
data/lib/testable.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
module Contracts
|
2
|
+
class Testable
|
3
|
+
# Given an array-of-arrays of arguments,
|
4
|
+
# gives you the product of those arguments so that
|
5
|
+
# each possible combination is tried.
|
6
|
+
# Example: <tt>[[1, 2], [3, 4]]</tt> would give you:
|
7
|
+
#
|
8
|
+
# [[1, 3], [1, 4], [2, 3], [2, 4]]
|
9
|
+
def self.product(arrays)
|
10
|
+
arrays.inject { |acc, x|
|
11
|
+
acc.product(x)
|
12
|
+
}.flatten(arrays.size - 2)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Given a contract, tells if you it's testable
|
16
|
+
def self.testable?(contract)
|
17
|
+
if contract.respond_to?(:testable?)
|
18
|
+
contract.testable?
|
19
|
+
else
|
20
|
+
contract.respond_to?(:new) && contract.method(:new).arity == 0
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Given a contract, returns the test data associated with that contract
|
25
|
+
def self.test_data(contract)
|
26
|
+
if contract.respond_to?(:testable?)
|
27
|
+
contract.test_data
|
28
|
+
else
|
29
|
+
contract.new
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# TODO Should work on whatever class it was invoked on, no?
|
34
|
+
def self.check_all
|
35
|
+
o = Object.new
|
36
|
+
Object.decorated_methods.each do |name, contracts|
|
37
|
+
check(o.method(name))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.check(meth)
|
42
|
+
contracts = meth.owner.decorated_methods[meth.name.to_sym][0].contracts
|
43
|
+
arg_contracts = contracts[0, contracts.size - 1]
|
44
|
+
return_val = contracts[-1]
|
45
|
+
checkable = arg_contracts.all? do |arg_contract|
|
46
|
+
Testable.testable?(arg_contract)
|
47
|
+
end
|
48
|
+
|
49
|
+
if checkable
|
50
|
+
print "Checking #{meth.name}..."
|
51
|
+
_test_data = arg_contracts.map do |arg_contract|
|
52
|
+
data = Testable.test_data(arg_contract)
|
53
|
+
data.is_a?(Array) ? data : [data]
|
54
|
+
end
|
55
|
+
test_data = Testable.product _test_data
|
56
|
+
test_data.each do |args|
|
57
|
+
if args.is_a? Hash
|
58
|
+
# because *hash destroys the hash
|
59
|
+
res = meth.call(args)
|
60
|
+
else
|
61
|
+
res = meth.call(*args)
|
62
|
+
end
|
63
|
+
Contract.valid?(res, return_val)
|
64
|
+
end
|
65
|
+
puts "#{test_data.size} tests run."
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: contracts
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 17
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 7
|
10
|
+
version: 0.0.7
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Aditya Bhargava
|
@@ -33,6 +33,7 @@ files:
|
|
33
33
|
- lib/decorators.rb
|
34
34
|
- lib/foo.rb
|
35
35
|
- lib/test.rb
|
36
|
+
- lib/testable.rb
|
36
37
|
has_rdoc: true
|
37
38
|
homepage: http://github.com/egonSchiele/contracts.ruby
|
38
39
|
licenses: []
|