rantly 0.2.0 → 3.0.0
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.
- checksums.yaml +7 -0
- data/.travis.yml +16 -0
- data/CHANGELOG.md +94 -0
- data/Gemfile +11 -0
- data/LICENSE +3 -0
- data/README.md +417 -0
- data/Rakefile +13 -30
- data/VERSION.yml +4 -4
- data/lib/rantly/data.rb +1 -1
- data/lib/rantly/generator.rb +126 -112
- data/lib/rantly/minitest.rb +1 -0
- data/lib/rantly/minitest_extensions.rb +15 -0
- data/lib/rantly/property.rb +60 -29
- data/lib/rantly/rspec.rb +1 -0
- data/lib/rantly/rspec_extensions.rb +8 -0
- data/lib/rantly/shrinks.rb +192 -0
- data/lib/rantly/silly.rb +42 -42
- data/lib/rantly/spec.rb +4 -4
- data/lib/rantly/testunit_extensions.rb +8 -0
- data/lib/rantly.rb +10 -2
- data/rantly.gemspec +36 -49
- data/test/rantly_test.rb +103 -225
- data/test/shrinks_test.rb +104 -0
- data/test/test_helper.rb +12 -8
- metadata +50 -45
- data/.gitignore +0 -5
- data/README.textile +0 -296
data/lib/rantly/generator.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
class Rantly
|
2
|
-
|
3
2
|
class << self
|
4
3
|
attr_writer :default_size
|
4
|
+
|
5
5
|
def singleton
|
6
6
|
@singleton ||= Rantly.new
|
7
7
|
@singleton
|
@@ -11,20 +11,20 @@ class Rantly
|
|
11
11
|
@default_size || 6
|
12
12
|
end
|
13
13
|
|
14
|
-
def each(n,limit=10
|
15
|
-
gen.each(n,limit
|
14
|
+
def each(n, limit = 10, &block)
|
15
|
+
gen.each(n, limit, &block)
|
16
16
|
end
|
17
17
|
|
18
|
-
def map(n,limit=10
|
19
|
-
gen.map(n,limit
|
18
|
+
def map(n, limit = 10, &block)
|
19
|
+
gen.map(n, limit, &block)
|
20
20
|
end
|
21
21
|
|
22
|
-
def value(limit=10
|
23
|
-
gen.value(limit
|
22
|
+
def value(limit = 10, &block)
|
23
|
+
gen.value(limit, &block)
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
26
|
def gen
|
27
|
-
|
27
|
+
singleton
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -32,8 +32,7 @@ class Rantly
|
|
32
32
|
end
|
33
33
|
|
34
34
|
class TooManyTries < RuntimeError
|
35
|
-
|
36
|
-
def initialize(limit,nfailed)
|
35
|
+
def initialize(limit, nfailed)
|
37
36
|
@limit = limit
|
38
37
|
@nfailed = nfailed
|
39
38
|
end
|
@@ -42,38 +41,37 @@ class Rantly
|
|
42
41
|
@nfailed
|
43
42
|
end
|
44
43
|
|
45
|
-
|
46
|
-
"Exceed gen limit #{@limit}: #{@nfailed} failed guards)"
|
47
|
-
end
|
44
|
+
attr_reader :limit
|
48
45
|
end
|
49
46
|
|
50
47
|
# limit attempts to 10 times of how many things we want to generate
|
51
|
-
def each(n,limit=10
|
52
|
-
generate(n,limit,block)
|
48
|
+
def each(n, limit = 10, &block)
|
49
|
+
generate(n, limit, block)
|
53
50
|
end
|
54
51
|
|
55
|
-
def map(n,limit=10
|
52
|
+
def map(n, limit = 10, &block)
|
56
53
|
acc = []
|
57
|
-
generate(n,limit,block) do |val|
|
54
|
+
generate(n, limit, block) do |val|
|
58
55
|
acc << val
|
59
56
|
end
|
60
57
|
acc
|
61
58
|
end
|
62
59
|
|
63
|
-
def value(limit=10
|
64
|
-
generate(1,limit,block) do |val|
|
60
|
+
def value(limit = 10, &block)
|
61
|
+
generate(1, limit, block) do |val|
|
65
62
|
return val
|
66
63
|
end
|
67
64
|
end
|
68
65
|
|
69
|
-
def generate(n,limit_arg,gen_block
|
66
|
+
def generate(n, limit_arg, gen_block, &handler)
|
70
67
|
limit = n * limit_arg
|
71
68
|
nfailed = 0
|
72
69
|
nsuccess = 0
|
73
70
|
while nsuccess < n
|
74
|
-
raise TooManyTries.new(limit_arg*n,nfailed) if limit
|
71
|
+
raise TooManyTries.new(limit_arg * n, nfailed) if limit.zero?
|
72
|
+
|
75
73
|
begin
|
76
|
-
val =
|
74
|
+
val = instance_eval(&gen_block)
|
77
75
|
rescue GuardFailure
|
78
76
|
nfailed += 1
|
79
77
|
limit -= 1
|
@@ -81,10 +79,10 @@ class Rantly
|
|
81
79
|
end
|
82
80
|
nsuccess += 1
|
83
81
|
limit -= 1
|
84
|
-
|
82
|
+
yield(val) if handler
|
85
83
|
end
|
86
84
|
end
|
87
|
-
|
85
|
+
|
88
86
|
attr_accessor :classifiers
|
89
87
|
|
90
88
|
def initialize
|
@@ -101,73 +99,89 @@ class Rantly
|
|
101
99
|
end
|
102
100
|
|
103
101
|
def guard(test)
|
104
|
-
|
102
|
+
return true if test
|
103
|
+
|
104
|
+
raise GuardFailure
|
105
105
|
end
|
106
106
|
|
107
107
|
def size
|
108
108
|
@size || Rantly.default_size
|
109
109
|
end
|
110
|
-
|
111
|
-
def sized(n
|
112
|
-
raise
|
110
|
+
|
111
|
+
def sized(n, &block)
|
112
|
+
raise 'size needs to be greater than zero' if n.negative?
|
113
|
+
|
113
114
|
old_size = @size
|
114
115
|
@size = n
|
115
|
-
r =
|
116
|
+
r = instance_eval(&block)
|
116
117
|
@size = old_size
|
117
|
-
|
118
|
+
r
|
118
119
|
end
|
119
120
|
|
120
121
|
# wanna avoid going into Bignum when calling range with these.
|
121
|
-
INTEGER_MAX = (2**(0.size * 8 -2) -1) / 2
|
122
|
-
INTEGER_MIN = -
|
123
|
-
def integer(limit=nil)
|
122
|
+
INTEGER_MAX = (2**(0.size * 8 - 2) - 1) / 2
|
123
|
+
INTEGER_MIN = -INTEGER_MAX
|
124
|
+
def integer(limit = nil)
|
124
125
|
case limit
|
125
126
|
when Range
|
126
127
|
hi = limit.end
|
127
128
|
lo = limit.begin
|
128
129
|
when Integer
|
129
|
-
raise
|
130
|
-
|
130
|
+
raise 'n should be greater than zero' if limit.negative?
|
131
|
+
|
132
|
+
hi = limit
|
133
|
+
lo = -limit
|
131
134
|
else
|
132
|
-
hi
|
135
|
+
hi = INTEGER_MAX
|
136
|
+
lo = INTEGER_MIN
|
133
137
|
end
|
134
|
-
range(lo,hi)
|
138
|
+
range(lo, hi)
|
135
139
|
end
|
136
140
|
|
137
141
|
def positive_integer
|
138
142
|
range(0)
|
139
143
|
end
|
140
144
|
|
141
|
-
def float
|
142
|
-
|
145
|
+
def float(distribution = nil, params = {})
|
146
|
+
case distribution
|
147
|
+
when :normal
|
148
|
+
params[:center] ||= 0
|
149
|
+
params[:scale] ||= 1
|
150
|
+
raise 'The distribution scale should be greater than zero' if params[:scale].negative?
|
151
|
+
|
152
|
+
# Sum of 6 draws from a uniform distribution give as a draw of a normal
|
153
|
+
# distribution centered in 3 (central limit theorem).
|
154
|
+
([rand, rand, rand, rand, rand, rand].sum - 3) * params[:scale] + params[:center]
|
155
|
+
else
|
156
|
+
rand
|
157
|
+
end
|
143
158
|
end
|
144
159
|
|
145
|
-
def range(lo=
|
146
|
-
lo
|
147
|
-
hi ||= INTEGER_MAX
|
148
|
-
rand(hi+1-lo) + lo
|
160
|
+
def range(lo = INTEGER_MIN, hi = INTEGER_MAX)
|
161
|
+
rand(lo..hi)
|
149
162
|
end
|
150
163
|
|
151
|
-
def call(gen
|
164
|
+
def call(gen, *args)
|
152
165
|
case gen
|
153
166
|
when Symbol
|
154
|
-
|
167
|
+
send(gen, *args)
|
155
168
|
when Array
|
156
|
-
raise
|
157
|
-
|
169
|
+
raise 'empty array' if gen.empty?
|
170
|
+
|
171
|
+
send(gen[0], *gen[1..-1])
|
158
172
|
when Proc
|
159
|
-
|
173
|
+
instance_eval(&gen)
|
160
174
|
else
|
161
175
|
raise "don't know how to call type: #{gen}"
|
162
176
|
end
|
163
177
|
end
|
164
178
|
|
165
179
|
def branch(*gens)
|
166
|
-
|
180
|
+
call(choose(*gens))
|
167
181
|
end
|
168
182
|
|
169
183
|
def choose(*vals)
|
170
|
-
vals[range(0,vals.length-1)]
|
184
|
+
vals[range(0, vals.length - 1)] if vals.length.positive?
|
171
185
|
end
|
172
186
|
|
173
187
|
def literal(value)
|
@@ -175,99 +189,99 @@ class Rantly
|
|
175
189
|
end
|
176
190
|
|
177
191
|
def boolean
|
178
|
-
range(0,1)
|
192
|
+
range(0, 1).zero?
|
179
193
|
end
|
180
194
|
|
181
195
|
def freq(*pairs)
|
182
196
|
pairs = pairs.map do |pair|
|
183
197
|
case pair
|
184
198
|
when Symbol, String, Proc
|
185
|
-
[1,pair]
|
199
|
+
[1, pair]
|
186
200
|
when Array
|
187
|
-
|
188
|
-
[1] + pair
|
189
|
-
else
|
201
|
+
if pair.first.is_a?(Integer)
|
190
202
|
pair
|
203
|
+
else
|
204
|
+
[1] + pair
|
191
205
|
end
|
192
206
|
end
|
193
207
|
end
|
194
|
-
total = pairs.inject(0) { |sum,p| sum + p.first }
|
195
|
-
raise(
|
196
|
-
|
208
|
+
total = pairs.inject(0) { |sum, p| sum + p.first }
|
209
|
+
raise("Illegal frequency:#{pairs.inspect}") if total.zero?
|
210
|
+
|
211
|
+
pos = range(1, total)
|
197
212
|
pairs.each do |p|
|
198
213
|
weight, gen, *args = p
|
199
|
-
if pos <= p[0]
|
200
|
-
|
201
|
-
|
202
|
-
pos -= weight
|
203
|
-
end
|
214
|
+
return call(gen, *args) if pos <= p[0]
|
215
|
+
|
216
|
+
pos -= weight
|
204
217
|
end
|
205
218
|
end
|
206
219
|
|
207
|
-
def array(
|
208
|
-
|
209
|
-
|
210
|
-
|
220
|
+
def array(n = size, &block)
|
221
|
+
n.times.map { instance_eval(&block) }
|
222
|
+
end
|
223
|
+
|
224
|
+
def dict(n = size, &block)
|
225
|
+
h = {}
|
226
|
+
each(n) do
|
227
|
+
k, v = instance_eval(&block)
|
228
|
+
h[k] = v if guard(!h.key?(k))
|
229
|
+
end
|
230
|
+
h
|
211
231
|
end
|
212
232
|
|
213
233
|
module Chars
|
214
|
-
|
215
234
|
class << self
|
216
|
-
ASCII =
|
217
|
-
(0..127).to_a.each do |i|
|
218
|
-
ASCII << i
|
219
|
-
end
|
235
|
+
ASCII = (0..127).to_a.each_with_object('') { |i, obj| obj << i }
|
220
236
|
|
221
237
|
def of(regexp)
|
222
|
-
ASCII.scan(regexp).to_a.map! { |char| char[0] }
|
238
|
+
ASCII.scan(regexp).to_a.map! { |char| char[0].ord }
|
223
239
|
end
|
224
240
|
end
|
225
|
-
|
226
|
-
ALNUM = Chars.of
|
227
|
-
ALPHA = Chars.of
|
228
|
-
BLANK = Chars.of
|
229
|
-
CNTRL = Chars.of
|
230
|
-
DIGIT = Chars.of
|
231
|
-
GRAPH = Chars.of
|
232
|
-
LOWER = Chars.of
|
233
|
-
PRINT = Chars.of
|
234
|
-
PUNCT = Chars.of
|
235
|
-
SPACE = Chars.of
|
236
|
-
UPPER = Chars.of
|
237
|
-
XDIGIT = Chars.of
|
238
|
-
ASCII = Chars.of
|
239
|
-
|
240
|
-
|
241
|
+
|
242
|
+
ALNUM = Chars.of(/[[:alnum:]]/)
|
243
|
+
ALPHA = Chars.of(/[[:alpha:]]/)
|
244
|
+
BLANK = Chars.of(/[[:blank:]]/)
|
245
|
+
CNTRL = Chars.of(/[[:cntrl:]]/)
|
246
|
+
DIGIT = Chars.of(/[[:digit:]]/)
|
247
|
+
GRAPH = Chars.of(/[[:graph:]]/)
|
248
|
+
LOWER = Chars.of(/[[:lower:]]/)
|
249
|
+
PRINT = Chars.of(/[[:print:]]/)
|
250
|
+
PUNCT = Chars.of(/[[:punct:]]/)
|
251
|
+
SPACE = Chars.of(/[[:space:]]/)
|
252
|
+
UPPER = Chars.of(/[[:upper:]]/)
|
253
|
+
XDIGIT = Chars.of(/[[:xdigit:]]/)
|
254
|
+
ASCII = Chars.of(/./)
|
255
|
+
|
241
256
|
CLASSES = {
|
242
|
-
:
|
243
|
-
:
|
244
|
-
:
|
245
|
-
:
|
246
|
-
:
|
247
|
-
:
|
248
|
-
:
|
249
|
-
:
|
250
|
-
:
|
251
|
-
:
|
252
|
-
:
|
253
|
-
:
|
254
|
-
:
|
255
|
-
}
|
256
|
-
|
257
|
+
alnum: ALNUM,
|
258
|
+
alpha: ALPHA,
|
259
|
+
blank: BLANK,
|
260
|
+
cntrl: CNTRL,
|
261
|
+
digit: DIGIT,
|
262
|
+
graph: GRAPH,
|
263
|
+
lower: LOWER,
|
264
|
+
print: PRINT,
|
265
|
+
punct: PUNCT,
|
266
|
+
space: SPACE,
|
267
|
+
upper: UPPER,
|
268
|
+
xdigit: XDIGIT,
|
269
|
+
ascii: ASCII
|
270
|
+
}.freeze
|
257
271
|
end
|
258
272
|
|
259
|
-
def string(char_class
|
273
|
+
def string(char_class = :print)
|
260
274
|
chars = case char_class
|
261
275
|
when Regexp
|
262
276
|
Chars.of(char_class)
|
263
277
|
when Symbol
|
264
278
|
Chars::CLASSES[char_class]
|
265
279
|
end
|
266
|
-
raise
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
str
|
280
|
+
raise 'bad arg' unless chars
|
281
|
+
|
282
|
+
char_strings = chars.map(&:chr)
|
283
|
+
str = Array.new(size)
|
284
|
+
size.times { |i| str[i] = char_strings.sample }
|
285
|
+
str.join
|
272
286
|
end
|
273
287
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'rantly/minitest_extensions'
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'minitest'
|
2
|
+
require 'rantly/property'
|
3
|
+
require "minitest/unit" unless defined?(MiniTest)
|
4
|
+
|
5
|
+
test_class = if defined?(MiniTest::Test)
|
6
|
+
MiniTest::Test
|
7
|
+
else
|
8
|
+
MiniTest::Unit::TestCase
|
9
|
+
end
|
10
|
+
|
11
|
+
test_class.class_eval do
|
12
|
+
def property_of(&blk)
|
13
|
+
Rantly::Property.new(blk)
|
14
|
+
end
|
15
|
+
end
|
data/lib/rantly/property.rb
CHANGED
@@ -1,50 +1,81 @@
|
|
1
1
|
require 'rantly'
|
2
|
-
require 'test/unit'
|
3
2
|
require 'pp'
|
3
|
+
require 'stringio'
|
4
4
|
|
5
5
|
class Rantly::Property
|
6
|
+
attr_reader :failed_data, :shrunk_failed_data
|
7
|
+
|
8
|
+
VERBOSITY = ENV.fetch('RANTLY_VERBOSE', 1).to_i
|
9
|
+
RANTLY_COUNT = ENV.fetch('RANTLY_COUNT', 100).to_i
|
10
|
+
|
11
|
+
def io
|
12
|
+
@io ||= if VERBOSITY >= 1
|
13
|
+
$stdout
|
14
|
+
else
|
15
|
+
StringIO.new
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def pretty_print(object)
|
20
|
+
PP.pp(object, io)
|
21
|
+
end
|
6
22
|
|
7
23
|
def initialize(property)
|
8
24
|
@property = property
|
9
25
|
end
|
10
|
-
|
11
|
-
def check(n=
|
26
|
+
|
27
|
+
def check(n = RANTLY_COUNT, limit = 10, &assertion)
|
12
28
|
i = 0
|
13
29
|
test_data = nil
|
14
30
|
begin
|
15
|
-
Rantly.singleton.generate(n,limit
|
31
|
+
Rantly.singleton.generate(n, limit, @property) do |val|
|
16
32
|
test_data = val
|
17
|
-
|
18
|
-
puts
|
19
|
-
print
|
33
|
+
yield(val) if assertion
|
34
|
+
io.puts '' if (i % 100).zero?
|
35
|
+
io.print '.' if (i % 10).zero?
|
20
36
|
i += 1
|
21
37
|
end
|
22
|
-
puts
|
23
|
-
puts "
|
38
|
+
io.puts
|
39
|
+
io.puts "SUCCESS - #{i} successful tests"
|
24
40
|
rescue Rantly::TooManyTries => e
|
25
|
-
puts
|
26
|
-
puts "too many tries: #{e.tries}"
|
27
|
-
raise e
|
28
|
-
rescue =>
|
29
|
-
puts
|
30
|
-
puts "
|
31
|
-
|
32
|
-
|
41
|
+
io.puts
|
42
|
+
io.puts "FAILURE - #{i} successful tests, too many tries: #{e.tries}"
|
43
|
+
raise e.exception("#{i} successful tests, too many tries: #{e.tries} (limit: #{e.limit})")
|
44
|
+
rescue Exception => e
|
45
|
+
io.puts
|
46
|
+
io.puts "FAILURE - #{i} successful tests, failed on:"
|
47
|
+
pretty_print test_data
|
48
|
+
@failed_data = test_data
|
49
|
+
if @failed_data.respond_to?(:shrink)
|
50
|
+
@shrunk_failed_data, @depth = shrinkify(assertion, @failed_data)
|
51
|
+
io.puts "Minimal failed data (depth #{@depth}) is:"
|
52
|
+
pretty_print @shrunk_failed_data
|
53
|
+
end
|
54
|
+
raise e.exception("#{i} successful tests, failed on:\n#{test_data}\n\n#{e}\n")
|
33
55
|
end
|
34
56
|
end
|
35
57
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
58
|
+
# Explore the failures tree
|
59
|
+
def shrinkify(assertion, data, depth = 0, iteration = 0)
|
60
|
+
min_data = data
|
61
|
+
max_depth = depth
|
62
|
+
if data.shrinkable?
|
63
|
+
while iteration < 1024
|
64
|
+
# We assume that data.shrink is non-destructive
|
65
|
+
shrunk_data = data.shrink
|
66
|
+
begin
|
67
|
+
assertion.call(shrunk_data)
|
68
|
+
rescue Exception
|
69
|
+
# If the assertion was verified, recursively shrink failure case
|
70
|
+
branch_data, branch_depth, iteration = shrinkify(assertion, shrunk_data, depth + 1, iteration + 1)
|
71
|
+
if branch_depth > max_depth
|
72
|
+
min_data = branch_data
|
73
|
+
max_depth = branch_depth
|
74
|
+
end
|
75
|
+
end
|
76
|
+
break unless data.retry?
|
77
|
+
end
|
41
78
|
end
|
79
|
+
[min_data, max_depth, iteration]
|
42
80
|
end
|
43
81
|
end
|
44
|
-
|
45
|
-
module Test::Unit::Assertions
|
46
|
-
def property_of(&block)
|
47
|
-
Rantly::Property.new(block)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
data/lib/rantly/rspec.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'rantly/rspec_extensions'
|