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.
@@ -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,&block)
15
- gen.each(n,limit,&block)
14
+ def each(n, limit = 10, &block)
15
+ gen.each(n, limit, &block)
16
16
  end
17
17
 
18
- def map(n,limit=10,&block)
19
- gen.map(n,limit,&block)
18
+ def map(n, limit = 10, &block)
19
+ gen.map(n, limit, &block)
20
20
  end
21
21
 
22
- def value(limit=10,&block)
23
- gen.value(limit,&block)
22
+ def value(limit = 10, &block)
23
+ gen.value(limit, &block)
24
24
  end
25
-
25
+
26
26
  def gen
27
- self.singleton
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
- def to_s
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,&block)
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,&block)
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,&block)
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,&handler)
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 < 0
71
+ raise TooManyTries.new(limit_arg * n, nfailed) if limit.zero?
72
+
75
73
  begin
76
- val = self.instance_eval(&gen_block)
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
- handler.call(val) if handler
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
- raise GuardFailure.new unless test
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,&block)
112
- raise "size needs to be greater than zero" if n < 0
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 = self.instance_eval(&block)
116
+ r = instance_eval(&block)
116
117
  @size = old_size
117
- return r
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 = -(INTEGER_MAX)
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 "n should be greater than zero" if limit < 0
130
- hi, lo = limit, -limit
130
+ raise 'n should be greater than zero' if limit.negative?
131
+
132
+ hi = limit
133
+ lo = -limit
131
134
  else
132
- hi, lo = INTEGER_MAX, INTEGER_MIN
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
- rand
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=nil,hi=nil)
146
- lo ||= INTEGER_MIN
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,*args)
164
+ def call(gen, *args)
152
165
  case gen
153
166
  when Symbol
154
- return self.send(gen,*args)
167
+ send(gen, *args)
155
168
  when Array
156
- raise "empty array" if gen.empty?
157
- return self.send(gen[0],*gen[1..-1])
169
+ raise 'empty array' if gen.empty?
170
+
171
+ send(gen[0], *gen[1..-1])
158
172
  when Proc
159
- return self.instance_eval(&gen)
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
- self.call(choose(*gens))
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) == 0 ? true : false
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
- unless pair.first.is_a?(Integer)
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(RuntimeError, "Illegal frequency:#{pairs.inspect}") if total == 0
196
- pos = range(1,total)
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
- return self.call(gen,*args)
201
- else
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(*freq_pairs)
208
- acc = []
209
- self.size.times { acc << freq(*freq_pairs) }
210
- acc
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 /[[:alnum:]]/
227
- ALPHA = Chars.of /[[:alpha:]]/
228
- BLANK = Chars.of /[[:blank:]]/
229
- CNTRL = Chars.of /[[:cntrl:]]/
230
- DIGIT = Chars.of /[[:digit:]]/
231
- GRAPH = Chars.of /[[:graph:]]/
232
- LOWER = Chars.of /[[:lower:]]/
233
- PRINT = Chars.of /[[:print:]]/
234
- PUNCT = Chars.of /[[:punct:]]/
235
- SPACE = Chars.of /[[:space:]]/
236
- UPPER = Chars.of /[[:upper:]]/
237
- XDIGIT = Chars.of /[[:xdigit:]]/
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
- :alnum => ALNUM,
243
- :alpha => ALPHA,
244
- :blank => BLANK,
245
- :cntrl => CNTRL,
246
- :digit => DIGIT,
247
- :graph => GRAPH,
248
- :lower => LOWER,
249
- :print => PRINT,
250
- :punct => PUNCT,
251
- :space => SPACE,
252
- :upper => UPPER,
253
- :xdigit => XDIGIT,
254
- :ascii => ASCII,
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=:print)
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 "bad arg" unless chars
267
- str = ""
268
- size.times do
269
- str << choose(*chars)
270
- end
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
@@ -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=100,limit=10,&assertion)
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,@property) do |val|
31
+ Rantly.singleton.generate(n, limit, @property) do |val|
16
32
  test_data = val
17
- assertion.call(val) if assertion
18
- puts "" if i % 100 == 0
19
- print "." if i % 10 == 0
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 "success: #{i} tests"
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 => boom
29
- puts
30
- puts "failure: #{i} tests, on:"
31
- pp test_data
32
- raise boom
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
- def report
37
- distribs = self.classifiers.sort { |a,b| b[1] <=> a[1] }
38
- total = distribs.inject(0) { |sum,pair| sum + pair[1]}
39
- distribs.each do |(classifier,count)|
40
- format "%10.5f%% of => %s", count, classifier
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
-
@@ -0,0 +1 @@
1
+ require 'rantly/rspec_extensions'
@@ -0,0 +1,8 @@
1
+ require 'rspec/core'
2
+ require 'rantly/property'
3
+
4
+ class RSpec::Core::ExampleGroup
5
+ def property_of(&block)
6
+ Rantly::Property.new(block)
7
+ end
8
+ end