minitest-proptest 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e01d66af6e62a9d5931cd1da501b08679e8f78a4aa625aa694c28029ae5228a9
4
+ data.tar.gz: 3d99ae675bbeac0c89c807a535c7400fc0806a223a68c0d1a50eb2cb2917c148
5
+ SHA512:
6
+ metadata.gz: 2a4abe803c73e8e254dfa2022e5bcbb84f3bdf6c1e502f7fa34d3d95c06dad05f44be371a935907135f8bb7f67cd1240b24835c7cf233e1fd85f52608ed53ac8
7
+ data.tar.gz: dd81efa85c34580d0d29cfbf21ab5042f0af14f493b7041712a61fd491638c5a6e02d79e758a459a03671110bf057474f9fae34c96c96607e8c0f1e7853c2e31
@@ -0,0 +1,525 @@
1
+ module Minitest
2
+ module Proptest
3
+ class Gen
4
+ class ValueGenerator
5
+ attr_accessor :entropy
6
+ attr_writer :type_parameters
7
+
8
+ def self.with_shrink_function(&f)
9
+ define_method(:shrink_function, &f)
10
+ self
11
+ end
12
+
13
+ def self.with_score_function(&f)
14
+ define_method(:score_function, &f)
15
+ self
16
+ end
17
+
18
+ def self.with_append(bound_min, bound_max, &f)
19
+ define_singleton_method(:bound_max) { bound_max }
20
+ define_singleton_method(:bound_min) { bound_min }
21
+ define_method(:append) do |other|
22
+ @value = f.call(value, other.value)
23
+ self
24
+ end
25
+ self
26
+ end
27
+
28
+ def self.with_empty(&f)
29
+ define_singleton_method(:empty) do |gen|
30
+ temp = new(gen)
31
+ temp.instance_variable_set(:@value, f.call)
32
+ temp
33
+ end
34
+ self
35
+ end
36
+
37
+ def self.bound_max
38
+ 1
39
+ end
40
+
41
+ def self.bound_min
42
+ 0
43
+ end
44
+
45
+ # append is not expected to be called unless overridden
46
+ def append(other)
47
+ self
48
+ end
49
+
50
+ def self.empty(gen)
51
+ self.new(gen)
52
+ end
53
+
54
+ def force(v)
55
+ temp = self.class.new(ArgumentError)
56
+ temp.instance_variable_set(:@value, v)
57
+ temp.type_parameters = @type_parameters
58
+ temp
59
+ end
60
+
61
+ def generate_value
62
+ gen = @generated.reduce(@generator) do |gen, val|
63
+ gen.call(val)
64
+ gen
65
+ end
66
+
67
+ while gen.is_a?(Proc) || gen.is_a?(Method)
68
+ gen = gen.call(*@type_parameters.map(&:value))
69
+ if gen.is_a?(ValueGenerator)
70
+ gen = gen.value
71
+ end
72
+ end
73
+
74
+ gen
75
+ end
76
+
77
+ def value
78
+ return false if @value == false
79
+ @value ||= generate_value
80
+ end
81
+
82
+ def prefix_entropy_generation(vals)
83
+ @generated = vals + @generated
84
+ end
85
+
86
+ def score
87
+ value
88
+ fs = @type_parameters.map { |x| x.method(:score_function) }
89
+ score_function(*fs, value)
90
+ end
91
+
92
+ def score_function(v)
93
+ v.to_i.abs
94
+ end
95
+
96
+ def shrink_candidates
97
+ fs = @type_parameters.map { |x| x.method(:shrink_function) }
98
+ os = score
99
+ candidates = shrink_function(*fs, value)
100
+ candidates
101
+ .map { |c| [force(c).score, c] }
102
+ .reject { |(s, _)| s > os }
103
+ .sort { |x, y| x.first <=> y.first }
104
+ .uniq
105
+ end
106
+
107
+ def shrink_function(x)
108
+ [x.itself]
109
+ end
110
+
111
+ def shrink_parameter(x)
112
+ @shrink_parameter.call(x)
113
+ end
114
+
115
+ # Generator helpers
116
+
117
+ def sized(n)
118
+ entropy.call(n + 1)
119
+ end
120
+
121
+ def one_of(r)
122
+ r.to_a[sized(r.to_a.length - 1)]
123
+ end
124
+ end
125
+
126
+ class Int8 < Integer; end
127
+ class Int16 < Integer; end
128
+ class Int32 < Integer; end
129
+ class Int64 < Integer; end
130
+ class UInt8 < Integer; end
131
+ class UInt16 < Integer; end
132
+ class UInt32 < Integer; end
133
+ class UInt64 < Integer; end
134
+ class Float32 < Float; end
135
+ class Float64 < Float; end
136
+ class ASCIIChar < String; end
137
+ class Char < String; end
138
+ class Bool < TrueClass; end
139
+
140
+ # Default maximum random value size is the local architecture's word size
141
+ MAX_SIZE = ((1 << (1.size * 8)) - 1)
142
+ SIGN_BIT = (1 << ((1.size * 8) - 1))
143
+
144
+ attr_reader :generated
145
+
146
+ instance_variable_set(:@_generators, {})
147
+
148
+ def self.create_type_constructor(arity, classes)
149
+ constructor = ->(c1) do
150
+ if classes.length == arity
151
+ f.call(*classes)
152
+ else
153
+ ->(c2) { constructor.call(c2) }
154
+ end
155
+ end
156
+ end
157
+
158
+ def self.generator_for(klass, &f)
159
+ new_class = Class.new(ValueGenerator)
160
+ new_class.define_method(:initialize) do |g|
161
+ @entropy = ->(b = MAX_SIZE) { (@generated << g.rand(b)).last }
162
+ @generated = []
163
+ @generator = self.method(:generator).curry
164
+ @parent_gen = g
165
+ @value = nil
166
+ @type_parameters = []
167
+ end
168
+
169
+ new_class.define_method(:generator, &f)
170
+
171
+ instance_variable_get(:@_generators)[klass] = new_class #.method(:new)
172
+ self.const_set((klass.name + 'Gen').split('::').last, new_class)
173
+ new_class
174
+ end
175
+
176
+ def initialize(random)
177
+ @random = random
178
+ @generated = []
179
+ end
180
+
181
+ def rand(max_size = MAX_SIZE)
182
+ (@generated << @random.rand(max_size)).last
183
+ end
184
+
185
+ def for(*classes)
186
+ generators = self.class.instance_variable_get(:@_generators)
187
+ case classes.length
188
+ when 0
189
+ raise(TypeError, "A generator for #{classes.join(' ')} is not known. Try adding it with Gen.generator_for.")
190
+ when 1
191
+ gen = generators[classes.first]
192
+ if gen.bound_max > 1
193
+ c = rand(gen.bound_max - gen.bound_min + 1) + gen.bound_min
194
+ c.times.reduce(gen.empty(self)) { |g, _| g.append(gen.new(self)) }
195
+ else
196
+ gen.new(self)
197
+ end
198
+ else
199
+ classgen = ->() do
200
+ classes[1..-1].map do |c|
201
+ if c.is_a?(Array)
202
+ self.for(*c)
203
+ else
204
+ self.for(c)
205
+ end
206
+ end
207
+ end
208
+ cs = classgen.call
209
+
210
+ gen = generators[classes.first]
211
+ typegen = gen.bound_min < 1 ? gen.empty(self) : gen.new(self)
212
+ typegen.type_parameters = cs
213
+ typegen.prefix_entropy_generation(cs)
214
+
215
+ if gen.bound_max > 1
216
+ c = rand(gen.bound_max - gen.bound_min + 1) + gen.bound_min
217
+ c.times.reduce(typegen) do |g, _|
218
+ cs2 = classgen.call
219
+ g2 = gen.new(self)
220
+ g2.type_parameters = cs2
221
+ g2.prefix_entropy_generation(cs2)
222
+ g.append(g2)
223
+ end
224
+ else
225
+ typegen
226
+ end
227
+ end
228
+ end
229
+
230
+ # Common shrinking machinery for all integral types. This includes chars,
231
+ # etc.
232
+ integral_shrink = ->(x) do
233
+ candidates = []
234
+ y = x
235
+
236
+ until y == 0
237
+ candidates << (x - y)
238
+ candidates << y
239
+ # Prevent negative integral from preventing termination
240
+ y = (y / 2.0).to_i
241
+ end
242
+
243
+ candidates
244
+ .flat_map { |i| [i - 1, i, i + 1] }
245
+ .reject { |i| i.abs >= x.abs }
246
+ end
247
+
248
+ float_shrink = ->(x) do
249
+ candidates = [Float::NAN, Float::INFINITY]
250
+ y = x
251
+
252
+ until y == 0 || y
253
+ candidates << (x - y)
254
+ y = (y / 2.0).to_i
255
+ end
256
+
257
+ candidates
258
+ .flat_map { |i| [i - 1, i, i + 1] }
259
+ .reject { |i| i.abs >= x.abs }
260
+ end
261
+
262
+ # List shrink adapted from QuickCheck
263
+ list_remove = ->(k, n, xs) do
264
+ xs1 = xs.take(k)
265
+ xs2 = xs.drop(k)
266
+ if k > n
267
+ []
268
+ elsif xs2.empty?
269
+ [[]]
270
+ else
271
+ [xs2] + list_remove.call(k, (n-k), xs2).map { |ys| xs1 + ys }
272
+ end
273
+ end
274
+
275
+ shrink_one = ->(f, xs) do
276
+ if xs.empty?
277
+ []
278
+ else
279
+ x = xs.first
280
+ xs = xs.drop(1)
281
+
282
+ ys = f.call(x).map { |y| [y] + xs }
283
+ zs = shrink_one.call(f, xs).map { |z| [x] + z }
284
+ ys + zs
285
+ end
286
+ end
287
+
288
+ list_shrink = ->(f, xs) do
289
+ candidates = [[]]
290
+ n = xs.length
291
+ k = n
292
+ while k > 0
293
+ candidates += list_remove.call(k, n, xs)
294
+ k /= 2
295
+ end
296
+ candidates + shrink_one.call(f, xs)
297
+ end
298
+
299
+ hash_remove = ->(k, n, h) do
300
+ xs = h.keys
301
+ xs1 = xs.take(k)
302
+ xs2 = xs.drop(k)
303
+
304
+ if k > n
305
+ []
306
+ elsif xs2.empty?
307
+ [{}]
308
+ else
309
+ h1 = xs1.reduce({}) { |c, e| c.merge({ e => h[e] }) }
310
+ h2 = xs2.reduce({}) { |c, e| c.merge({ e => h[e] }) }
311
+ [h1, h2] + list_remove.call(k, (n-k), h2).map { |ys| h1.merge(ys.to_h) }
312
+ end
313
+ end
314
+
315
+ hash_shrink = ->(fk, fv, h) do
316
+ candidates = []
317
+ n = h.length
318
+ k = n
319
+ while k > 0
320
+ candidates += hash_remove.call(k, n, h)
321
+ k /= 2
322
+ end
323
+ candidates
324
+ end
325
+
326
+ # Use two's complement for all signed integers in order to optimize for
327
+ # random values to shrink towards 0.
328
+ generator_for(Integer) do
329
+ r = sized(MAX_SIZE)
330
+ if (r & SIGN_BIT).zero?
331
+ r
332
+ else
333
+ -(((r & (MAX_SIZE ^ SIGN_BIT)) - 1) ^ (MAX_SIZE ^ SIGN_BIT))
334
+ end
335
+ end.with_shrink_function do |i|
336
+ i = if (i & SIGN_BIT).zero?
337
+ i
338
+ else
339
+ -(((i & (MAX_SIZE ^ SIGN_BIT)) - 1) ^ (MAX_SIZE ^ SIGN_BIT))
340
+ end
341
+ integral_shrink.call(i)
342
+ end
343
+
344
+ generator_for(Int8) do
345
+ r = sized(0xff)
346
+ (r & 0x80).zero? ? r : -(((r & 0x7f) - 1) ^ 0x7f)
347
+ end.with_shrink_function do |i|
348
+ i = (i & 0x80).zero? ? i : -(((i & 0x7f) - 1) ^ 0x7f)
349
+ integral_shrink.call(i)
350
+ end
351
+
352
+ generator_for(Int16) do
353
+ r = sized(0xffff)
354
+ (r & 0x8000).zero? ? r : -(((r & 0x7fff) - 1) ^ 0x7fff)
355
+ end.with_shrink_function do |i|
356
+ i = (i & 0x8000).zero? ? i : -(((i & 0x7fff) - 1) ^ 0x7fff)
357
+ integral_shrink.call(i)
358
+ end
359
+
360
+ generator_for(Int32) do
361
+ r = sized(0xffffffff)
362
+ (r & 0x80000000).zero? ? r : -(((r & 0x7fffffff) - 1) ^ 0x7fffffff)
363
+ end.with_shrink_function do |i|
364
+ i = if (i & 0x80000000).zero?
365
+ i
366
+ else
367
+ -(((i & 0x7fffffff) - 1) ^ 0x7fffffff)
368
+ end
369
+ integral_shrink.call(i)
370
+ end
371
+
372
+ generator_for(Int64) do
373
+ r = sized(0xffffffffffffffff)
374
+ if (r & 0x8000000000000000).zero?
375
+ r
376
+ else
377
+ -(((r & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
378
+ end
379
+ end.with_shrink_function do |i|
380
+ i = if (i & 0x8000000000000000).zero?
381
+ i
382
+ else
383
+ -(((i & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
384
+ end
385
+ integral_shrink.call(i)
386
+ end
387
+
388
+ generator_for(UInt8) do
389
+ sized(0xff)
390
+ end.with_shrink_function do |i|
391
+ integral_shrink.call(i).reject(&:negative?)
392
+ end
393
+
394
+ generator_for(UInt16) do
395
+ sized(0xffff)
396
+ end.with_shrink_function do |i|
397
+ integral_shrink.call(i).reject(&:negative?)
398
+ end
399
+
400
+ generator_for(UInt32) do
401
+ sized(0xffffffff)
402
+ end.with_shrink_function do |i|
403
+ integral_shrink.call(i).reject(&:negative?)
404
+ end
405
+
406
+ generator_for(UInt64) do
407
+ sized(0xffffffffffffffff)
408
+ end.with_shrink_function do |i|
409
+ integral_shrink.call(i).reject(&:negative?)
410
+ end
411
+
412
+ generator_for(Float32) do
413
+ # There is most likely a faster way to do this which doesn't involve
414
+ # FFI, but it was faster than manual bit twiddling in ruby
415
+ bits = sized(0xffffffff)
416
+ (0..3)
417
+ .map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
418
+ .join
419
+ .unpack('f')
420
+ .first
421
+ end.with_shrink_function do |f|
422
+ float_shrink.call(f)
423
+ end.with_score_function do |f|
424
+ if f.nan? || f.infinite?
425
+ 0
426
+ else
427
+ f.abs.ceil
428
+ end
429
+ end
430
+
431
+ generator_for(Float64) do
432
+ bits = sized(0xffffffffffffffff)
433
+ (0..7)
434
+ .map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
435
+ .join
436
+ .unpack('d')
437
+ .first
438
+ end.with_shrink_function do |f|
439
+ float_shrink.call(f)
440
+ end.with_score_function do |f|
441
+ if f.nan? || f.infinite?
442
+ 0
443
+ else
444
+ f.abs.ceil
445
+ end
446
+ end
447
+
448
+ generator_for(Float) do
449
+ bits = sized(0xffffffffffffffff)
450
+ (0..7)
451
+ .map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
452
+ .join
453
+ .unpack('d')
454
+ .first
455
+ end.with_shrink_function do |f|
456
+ float_shrink.call(f)
457
+ end.with_score_function do |f|
458
+ if f.nan? || f.infinite?
459
+ 0
460
+ else
461
+ f.abs.ceil
462
+ end
463
+ end
464
+
465
+ generator_for(ASCIIChar) do
466
+ sized(0x7f).chr
467
+ end.with_shrink_function do |c|
468
+ integral_shrink.call(c.ord).reject(&:negative?).map(&:chr)
469
+ end.with_score_function do |c|
470
+ c.ord
471
+ end
472
+
473
+ generator_for(Char) do
474
+ sized(0xff).chr
475
+ end.with_shrink_function do |c|
476
+ integral_shrink.call(c.ord).reject(&:negative?).map(&:chr)
477
+ end.with_score_function do |c|
478
+ c.ord
479
+ end
480
+
481
+ generator_for(String) do
482
+ sized(0xff).chr
483
+ end.with_shrink_function do |s|
484
+ xs = list_shrink.call(integral_shrink, s.chars.map(&:ord))
485
+ xs.map { |str| str.map { |t| t & 0xff }.map(&:chr).join }
486
+ end.with_score_function do |s|
487
+ s.chars.map(&:ord).reduce(1) do |c, x|
488
+ y = x.abs
489
+ c * (y > 0 ? y + 1 : 1)
490
+ end
491
+ end.with_append(0, 0x20) do |x, y|
492
+ x + y
493
+ end.with_empty { "" }
494
+
495
+ generator_for(Array) do |x|
496
+ [x]
497
+ end.with_shrink_function(&list_shrink).with_score_function do |f, xs|
498
+ xs.reduce(1) do |c, x|
499
+ y = f.call(x).abs
500
+ c * (y > 0 ? y + 1 : 1)
501
+ end.to_i * xs.length
502
+ end.with_append(0, 0x10) do |xs, ys|
503
+ xs + ys
504
+ end.with_empty { [] }
505
+
506
+ generator_for(Hash) do |key, value|
507
+ { key => value }
508
+ end.with_shrink_function(&hash_shrink).with_score_function do |fk, fv, h|
509
+ h.reduce(1) do |c, (k, v)|
510
+ sk = fk.call(k).abs
511
+ sv = fv.call(v).abs
512
+ c * ((sk > 0 ? sk + 1 : 1) + (sv > 0 ? sv + 1 : 1))
513
+ end
514
+ end.with_append(0, 0x10) do |xm, ym|
515
+ xm.merge(ym)
516
+ end.with_empty { Hash.new }
517
+
518
+ generator_for(Bool) do
519
+ sized(0x1).even? ? false : true
520
+ end.with_score_function do |_|
521
+ 1
522
+ end
523
+ end
524
+ end
525
+ end
@@ -0,0 +1,177 @@
1
+ module Minitest
2
+ module Proptest
3
+ class Property
4
+
5
+ attr_reader :result, :status, :trivial
6
+ def initialize(
7
+ # The function which proves the property
8
+ test_proc,
9
+ # Any class which provides `rand` accepting both an Integer and a Range
10
+ # is acceptable. The default value is Ruby's standard Mersenne Twister
11
+ # implementation.
12
+ random: Random.method(:new),
13
+ # Maximum number of successful cases before considering the test a
14
+ # success.
15
+ max_success: 100,
16
+ # Maximum ratio of discarded tests per successful test before giving up.
17
+ max_discard_ratio: 10,
18
+ # Maximum amount of entropy to generate in a single run
19
+ max_size: 0x100,
20
+ # Maximum number of shrink attempts (default of half of max unsigned int
21
+ # on the system architecture adopted from QuickCheck
22
+ max_shrinks: 0x7fffffffffffffff
23
+ )
24
+ @test_proc = test_proc
25
+ @random = random.call
26
+ @generator = ::Minitest::Proptest::Gen.new(@random)
27
+ @max_success = max_success
28
+ @max_discard_ratio = max_discard_ratio
29
+ @max_size = max_size
30
+ @max_shrinks = max_shrinks
31
+ @status = Status.unknown
32
+ @trivial = false
33
+ @result = nil
34
+ @exception = nil
35
+ @calls = 0
36
+ @valid_test_cases = 0
37
+ @generated = []
38
+ @arbitrary = nil
39
+ end
40
+
41
+ def run!
42
+ iterate!
43
+ shrink!
44
+ end
45
+
46
+ def arbitrary(*classes)
47
+ if @arbitrary
48
+ @arbitrary.call(*classes)
49
+ else
50
+ a = @generator.for(*classes)
51
+ @generated << a
52
+ @status = Status.overrun unless @generated.length <= @max_size
53
+ a.value
54
+ end
55
+ end
56
+
57
+ def explain
58
+ prop = if @status.valid?
59
+ "The property was proved to satsfaction across " +
60
+ "#{@valid_test_cases} assertions."
61
+ elsif @status.invalid?
62
+ "The property was determined to be invalid due to " +
63
+ "#{@exception.class.name}: #{@exception.message}\n" +
64
+ @exception.backtrace.map { |l| " #{l}" }.join("\n")
65
+ elsif @status.overrun?
66
+ "The property attempted to generate more than #{@max_size} " +
67
+ "bytes of entropy, violating the property's maximum size." +
68
+ "This might be rectified by increasing max_size."
69
+ elsif @status.unknown?
70
+ "The property has not yet been tested."
71
+ elsif @status.interesting?
72
+ "The property has found the following counterexample after " +
73
+ "#{@valid_test_cases} valid " +
74
+ "example#{@valid_test_cases == 1 ? '' : 's'}:\n" +
75
+ @generated.map(&:value).inspect
76
+ end
77
+ trivial = if @trivial
78
+ "\nThe test does not appear to use any generated values " +
79
+ "and as such is likely not generating much value. " +
80
+ "Consider reworking this test to make use of arbitrary " +
81
+ "data."
82
+ else
83
+ ""
84
+ end
85
+ prop + trivial
86
+ end
87
+
88
+ private
89
+
90
+ def iterate!
91
+ while continue_iterate? && @result.nil? && @valid_test_cases <= @max_success / 2
92
+ @generated = []
93
+ @generator = ::Minitest::Proptest::Gen.new(@random)
94
+ @calls += 1
95
+ if instance_eval(&@test_proc)
96
+ @status = Status.valid if @status.unknown?
97
+ @valid_test_cases += 1
98
+ else
99
+ @result = @generated
100
+ @status = Status.interesting
101
+ end
102
+ @trivial = true if @generated.empty?
103
+ end
104
+ rescue => e
105
+ @status = Status.invalid
106
+ @exception = e
107
+ raise e
108
+ end
109
+
110
+ def shrink!
111
+ return if @result.nil?
112
+ old_random = @random
113
+ old_generator = @generator
114
+ best_score = @generated.map(&:score).reduce(&:+)
115
+ best_generated = @generated
116
+ candidates = @generated.map(&:shrink_candidates)
117
+ old_arbitrary = @arbitrary
118
+
119
+ to_test = candidates
120
+ .map { |x| x.map { |y| [y] } }
121
+ .reduce { |c, e| c.flat_map { |a| e.map { |b| a + b } } }
122
+ .sort { |x, y| x.map(&:first).reduce(&:+) <=> y.map(&:first).reduce(&:+) }
123
+ .uniq
124
+ run = { run: 0, index: -1 }
125
+
126
+ @arbitrary = ->(*classes) do
127
+ run[:index] += 1
128
+ raise IndexError if run[:index] >= to_test[run[:run]].length
129
+
130
+ a = @generator.for(*classes)
131
+ a = a.force(to_test[run[:run]][run[:index]].last)
132
+ @generated << a
133
+ to_test[run[:run]][run[:index]].last
134
+ end
135
+
136
+ while continue_shrink? && run[:run] < to_test.length
137
+ @generated = []
138
+ run[:index] = -1
139
+
140
+ @generator = ::Minitest::Proptest::Gen.new(@random)
141
+ if to_test[run[:run]].map(&:first).reduce(&:+) < best_score
142
+ unless instance_eval(&@test_proc)
143
+ best_generated = @generated
144
+ # Because we pre-sorted our shrink candidates, the first hit is
145
+ # necessarily the best scoring
146
+ break
147
+ end
148
+ end
149
+
150
+ @calls += 1
151
+ run[:run] += 1
152
+ end
153
+ # Clean up after we're done
154
+ @generated = best_generated
155
+ @result = best_generated
156
+ @generator = old_generator
157
+ @random = old_random
158
+ @arbitrary = old_arbitrary
159
+ end
160
+
161
+ def continue_iterate?
162
+ !@trivial &&
163
+ !@status.invalid? &&
164
+ !@status.overrun? &&
165
+ @valid_test_cases < @max_success &&
166
+ @calls < @max_success * @max_discard_ratio
167
+ end
168
+
169
+ def continue_shrink?
170
+ !@trivial &&
171
+ !@status.invalid? &&
172
+ !@status.overrun?
173
+ @calls < @max_shrinks
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,61 @@
1
+ module Minitest
2
+ module Proptest
3
+ # Sum type representing the possible statuses of a test run.
4
+ # Invalid, Overrun, and Interesting represent different failure classes.
5
+ # Unknown represents a lack of information about the test run (typically
6
+ # having not run to satisfaction).
7
+ # Valid represents a test which has run to satisfaction.
8
+ class Status
9
+ class Interesting < Status
10
+ end
11
+
12
+ class Invalid < Status
13
+ end
14
+
15
+ class Overrun < Status
16
+ end
17
+
18
+ class Unknown < Status
19
+ end
20
+
21
+ class Valid < Status
22
+ end
23
+
24
+ invalid = Invalid.new.freeze
25
+ interesting = Interesting.new.freeze
26
+ overrun = Overrun.new.freeze
27
+ unknown = Unknown.new.freeze
28
+ valid = Valid.new.freeze
29
+
30
+ define_singleton_method(:invalid) { invalid }
31
+ define_singleton_method(:interesting) { interesting }
32
+ define_singleton_method(:overrun) { overrun }
33
+ define_singleton_method(:unknown) { unknown }
34
+ define_singleton_method(:valid) { valid }
35
+
36
+ def invalid?
37
+ self.is_a?(Invalid)
38
+ end
39
+
40
+ def overrun?
41
+ self.is_a?(Overrun)
42
+ end
43
+
44
+ def unknown?
45
+ self.is_a?(Unknown)
46
+ end
47
+
48
+ def valid?
49
+ self.is_a?(Valid)
50
+ end
51
+
52
+ def interesting?
53
+ self.is_a?(Interesting)
54
+ end
55
+
56
+ def initialize
57
+ raise 'Please use singleton instances'
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Proptest
5
+ VERSION = '0.0.2'
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ require 'minitest'
2
+ require 'minitest/proptest/gen'
3
+ require 'minitest/proptest/property'
4
+ require 'minitest/proptest/status'
5
+ require 'minitest/proptest/version'
6
+
7
+ module Minitest
8
+ module Proptest
9
+ DEFAULT_RANDOM = Random.method(:new)
10
+ DEFAULT_MAX_SUCCESS = 100
11
+ DEFAULT_MAX_DISCARD_RATIO = 10
12
+ DEFAULT_MAX_SIZE = 0x100
13
+ DEFAULT_MAX_SHRINKS = (((1 << (1.size * 8)) - 1) / 2)
14
+
15
+ def self.set_seed(seed)
16
+ self.instance_variable_set(:@_random_seed, seed)
17
+ end
18
+ end
19
+ end
20
+
21
+ module Kernel
22
+ def generator_for(klass, &f)
23
+ ::Minitest::Proptest::Gen.generator_for(klass, &f)
24
+ end
25
+ private :generator_for
26
+ end
@@ -0,0 +1,52 @@
1
+ require 'minitest'
2
+ require 'minitest/proptest'
3
+ require 'minitest/proptest/gen'
4
+ require 'minitest/proptest/property'
5
+ require 'minitest/proptest/status'
6
+ require 'minitest/proptest/version'
7
+
8
+ module Minitest
9
+ def self.plugin_proptest_init(options)
10
+ %i[Int8 Int16 Int32 Int64
11
+ UInt8 UInt16 UInt32 UInt64
12
+ Float32 Float64
13
+ ASCIIChar Char
14
+ Bool
15
+ ].each do |const|
16
+ unless Minitest::Assertions.const_defined?(const)
17
+ ::Minitest::Assertions.const_set(const, ::Minitest::Proptest::Gen.const_get(const))
18
+ end
19
+ end
20
+
21
+ if options.has_key?(:seed)
22
+ Proptest.set_seed(options[:seed])
23
+ end
24
+ end
25
+
26
+ module Assertions
27
+ def property(&f)
28
+ self.assertions += 1
29
+
30
+ random_thunk = if Proptest.instance_variable_defined?(:@_random_seed)
31
+ r = Proptest.instance_variable_get(:@_random_seed)
32
+ ->() { Proptest::DEFAULT_RANDOM.call(r) }
33
+ else
34
+ Proptest::DEFAULT_RANDOM
35
+ end
36
+
37
+ prop = Minitest::Proptest::Property.new(
38
+ f,
39
+ random: random_thunk,
40
+ max_success: Proptest::DEFAULT_MAX_SUCCESS,
41
+ max_discard_ratio: Proptest::DEFAULT_MAX_DISCARD_RATIO,
42
+ max_size: Proptest::DEFAULT_MAX_SIZE,
43
+ max_shrinks: Proptest::DEFAULT_MAX_SHRINKS
44
+ )
45
+ prop.run!
46
+
47
+ unless prop.status.valid? && !prop.trivial
48
+ raise Minitest::Assertion, prop.explain
49
+ end
50
+ end
51
+ end
52
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minitest-proptest
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Tina Wuest
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-09-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5'
27
+ description: Property testing in Minitest, a la Haskell's QuickCheck and Python's
28
+ Hypothesis
29
+ email: tina@wuest.me
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/minitest/proptest.rb
35
+ - lib/minitest/proptest/gen.rb
36
+ - lib/minitest/proptest/property.rb
37
+ - lib/minitest/proptest/status.rb
38
+ - lib/minitest/proptest/version.rb
39
+ - lib/minitest/proptest_plugin.rb
40
+ homepage: https://github.com/wuest/minitest-proptest
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ homepage_uri: https://github.com/wuest/minitest-proptest
45
+ source_code_uri: https://github.com/wuest/minitest-proptest
46
+ changelog_uri: https://github.com/wuest/minitest-proptest/blob/main/CHANGELOG.md
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 2.7.0
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.3.7
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Property testing in Minitest
66
+ test_files: []