contracts 0.0.6 → 0.0.7
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.
- 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: []
|