minitest-proptest 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/minitest/proptest/gen/value_generator.rb +1 -1
- data/lib/minitest/proptest/gen.rb +109 -35
- data/lib/minitest/proptest/property.rb +67 -12
- data/lib/minitest/proptest/status.rb +13 -0
- data/lib/minitest/proptest/version.rb +1 -1
- data/lib/minitest/proptest_plugin.rb +4 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0596e6f6a66614dba0199eccc4e18a5bf6b869fd0db46dff7636a8772eacb75b'
|
4
|
+
data.tar.gz: a956c579c5bb355a5b687f64109917918870dd7dac1323a18a72d44e4ca623a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d6206c7a778b9b55e0674e204afea71721eb8b6e4fe2257691cc1cefd5e76885eff08d648f9a61172806872f1abe5f1b232186fcc803e6fcc560241fd71fac8c
|
7
|
+
data.tar.gz: 1ca1d04ddcdecb9f661e0af6cffc87dda30af6a9b06bb6fe406a5f27ec445a0cf3cd9ac4d9050b4a243ab40d76d066358f6cf9612181cc546a2076b76a7d9041
|
@@ -19,8 +19,8 @@ module Minitest
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def self.with_append(bound_min, bound_max, &f)
|
22
|
-
define_singleton_method(:bound_max) { bound_max }
|
23
22
|
define_singleton_method(:bound_min) { bound_min }
|
23
|
+
define_singleton_method(:bound_max) { bound_max }
|
24
24
|
define_method(:append) do |other|
|
25
25
|
@value = f.call(value, other.value)
|
26
26
|
self
|
@@ -119,28 +119,60 @@ module Minitest
|
|
119
119
|
|
120
120
|
until y == 0
|
121
121
|
candidates << (x - y)
|
122
|
-
candidates << y
|
123
|
-
# Prevent negative integral from preventing termination
|
122
|
+
candidates << y if y.abs < x.abs
|
124
123
|
y = (y / 2.0).to_i
|
125
124
|
end
|
126
125
|
|
127
126
|
candidates
|
128
|
-
|
129
|
-
|
127
|
+
end
|
128
|
+
|
129
|
+
score_float = ->(f) do
|
130
|
+
if f.nan? || f.infinite?
|
131
|
+
0
|
132
|
+
else
|
133
|
+
f.abs.ceil
|
134
|
+
end
|
130
135
|
end
|
131
136
|
|
132
137
|
float_shrink = ->(x) do
|
138
|
+
return [] if x.nan? || x.infinite? || x.zero?
|
139
|
+
|
133
140
|
candidates = [Float::NAN, Float::INFINITY]
|
134
141
|
y = x
|
135
142
|
|
136
|
-
until y
|
143
|
+
until y.zero? || y.to_f.infinite? || y.to_f.nan?
|
137
144
|
candidates << (x - y)
|
138
145
|
y = (y / 2.0).to_i
|
139
146
|
end
|
140
147
|
|
141
|
-
|
142
|
-
|
143
|
-
|
148
|
+
score = score_float.call(x)
|
149
|
+
candidates.reduce([]) do |cs, c|
|
150
|
+
cs + (score_float.call(c) < score ? [c.to_f] : [])
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
score_complex = ->(c) do
|
155
|
+
r = if c.real.to_f.nan? || c.real.to_f.infinite?
|
156
|
+
0
|
157
|
+
else
|
158
|
+
c.real.abs.ceil
|
159
|
+
end
|
160
|
+
i = if c.imaginary.to_f.nan? || c.imaginary.to_f.infinite?
|
161
|
+
0
|
162
|
+
else
|
163
|
+
c.imaginary.abs.ceil
|
164
|
+
end
|
165
|
+
r + i
|
166
|
+
end
|
167
|
+
|
168
|
+
complex_shrink = ->(x) do
|
169
|
+
rs = float_shrink.call(x.real)
|
170
|
+
is = float_shrink.call(x.imaginary)
|
171
|
+
|
172
|
+
score = score_complex.call(x)
|
173
|
+
rs.flat_map { |real| is.map { |imag| Complex(real, imag) } }
|
174
|
+
.reject { |c| score_complex.call(c) >= score }
|
175
|
+
.uniq
|
144
176
|
end
|
145
177
|
|
146
178
|
# List shrink adapted from QuickCheck
|
@@ -196,6 +228,34 @@ module Minitest
|
|
196
228
|
end
|
197
229
|
end
|
198
230
|
|
231
|
+
range_shrink = ->(f, r) do
|
232
|
+
xs = f.call(r.first)
|
233
|
+
ys = f.call(r.last)
|
234
|
+
|
235
|
+
xs.flat_map { |x| ys.map { |y| x <= y ? (x..y) : (y..x) } }
|
236
|
+
end
|
237
|
+
|
238
|
+
score_rational = ->(r) do
|
239
|
+
(r.numerator * r.denominator).abs
|
240
|
+
end
|
241
|
+
|
242
|
+
rational_shrink = ->(r) do
|
243
|
+
ns = integral_shrink.call(r.numerator)
|
244
|
+
ds = integral_shrink.call(r.denominator)
|
245
|
+
|
246
|
+
score = score_rational.call(r)
|
247
|
+
ns.flat_map do |n|
|
248
|
+
ds.reduce([]) do |rs, d|
|
249
|
+
if d.zero?
|
250
|
+
rs
|
251
|
+
else
|
252
|
+
rational = Rational(n, d)
|
253
|
+
rs + (score_rational.call(rational) < score ? [rational] : [])
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
199
259
|
hash_shrink = ->(_fk, _fv, h) do
|
200
260
|
candidates = []
|
201
261
|
n = h.length
|
@@ -303,45 +363,36 @@ module Minitest
|
|
303
363
|
.unpack1('f')
|
304
364
|
end.with_shrink_function do |f|
|
305
365
|
float_shrink.call(f)
|
306
|
-
end.with_score_function
|
307
|
-
if f.nan? || f.infinite?
|
308
|
-
0
|
309
|
-
else
|
310
|
-
f.abs.ceil
|
311
|
-
end
|
312
|
-
end
|
366
|
+
end.with_score_function(&score_float)
|
313
367
|
|
314
|
-
|
315
|
-
bits = sized(0xffffffffffffffff)
|
368
|
+
float64build = ->(bits) do
|
316
369
|
(0..7)
|
317
370
|
.map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
|
318
371
|
.join
|
319
372
|
.unpack1('d')
|
373
|
+
end
|
374
|
+
|
375
|
+
generator_for(Float64) do
|
376
|
+
bits = sized(0xffffffffffffffff)
|
377
|
+
float64build.call(bits)
|
320
378
|
end.with_shrink_function do |f|
|
321
379
|
float_shrink.call(f)
|
322
|
-
end.with_score_function
|
323
|
-
if f.nan? || f.infinite?
|
324
|
-
0
|
325
|
-
else
|
326
|
-
f.abs.ceil
|
327
|
-
end
|
328
|
-
end
|
380
|
+
end.with_score_function(&score_float)
|
329
381
|
|
330
382
|
generator_for(Float) do
|
331
383
|
bits = sized(0xffffffffffffffff)
|
332
|
-
(
|
333
|
-
.map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
|
334
|
-
.join
|
335
|
-
.unpack1('d')
|
384
|
+
float64build.call(bits)
|
336
385
|
end.with_shrink_function do |f|
|
337
386
|
float_shrink.call(f)
|
338
|
-
end.with_score_function
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
end
|
387
|
+
end.with_score_function(&score_float)
|
388
|
+
|
389
|
+
generator_for(Complex) do
|
390
|
+
real = sized(0xffffffffffffffff)
|
391
|
+
imag = sized(0xffffffffffffffff)
|
392
|
+
Complex(float64build.call(real), float64build.call(imag))
|
393
|
+
end.with_shrink_function do |c|
|
394
|
+
complex_shrink.call(c)
|
395
|
+
end.with_score_function(&score_complex)
|
345
396
|
|
346
397
|
generator_for(ASCIIChar) do
|
347
398
|
sized(0x7f).chr
|
@@ -392,6 +443,29 @@ module Minitest
|
|
392
443
|
xm.merge(ym)
|
393
444
|
end.with_empty { {} }
|
394
445
|
|
446
|
+
generator_for(Range) do |x|
|
447
|
+
(x..x)
|
448
|
+
end.with_shrink_function(&range_shrink).with_score_function do |f, r|
|
449
|
+
r.to_a.reduce(1) do |c, x|
|
450
|
+
y = f.call(x).abs
|
451
|
+
c * (y > 0 ? y + 1 : 1)
|
452
|
+
end.to_i * r.to_a.length
|
453
|
+
end.with_append(2, 2) do |ra, rb|
|
454
|
+
xs = [ra.first, ra.last, rb.first, rb.last].sort
|
455
|
+
(xs.first..xs.last)
|
456
|
+
end
|
457
|
+
|
458
|
+
generator_for(Rational) do
|
459
|
+
n = sized(MAX_SIZE)
|
460
|
+
d = sized(MAX_SIZE - 1) + 1
|
461
|
+
if (n & SIGN_BIT).zero?
|
462
|
+
Rational(n, d)
|
463
|
+
else
|
464
|
+
Rational(-(((n & (MAX_SIZE ^ SIGN_BIT)) - 1) ^ (MAX_SIZE ^ SIGN_BIT)), d)
|
465
|
+
end
|
466
|
+
end.with_shrink_function(&rational_shrink)
|
467
|
+
.with_score_function(&score_rational)
|
468
|
+
|
395
469
|
generator_for(Bool) do
|
396
470
|
sized(0x1).odd?
|
397
471
|
end.with_score_function do |_|
|
@@ -4,7 +4,12 @@ module Minitest
|
|
4
4
|
module Proptest
|
5
5
|
# Property evaluation - status, scoring, shrinking
|
6
6
|
class Property
|
7
|
-
|
7
|
+
require 'minitest/assertions'
|
8
|
+
include Minitest::Assertions
|
9
|
+
|
10
|
+
attr_reader :calls, :result, :status, :trivial
|
11
|
+
|
12
|
+
attr_accessor :assertions
|
8
13
|
|
9
14
|
def initialize(
|
10
15
|
# The function which proves the property
|
@@ -36,9 +41,11 @@ module Minitest
|
|
36
41
|
@max_shrinks = max_shrinks
|
37
42
|
@status = Status.unknown
|
38
43
|
@trivial = false
|
44
|
+
@valid_test_case = true
|
39
45
|
@result = nil
|
40
46
|
@exception = nil
|
41
47
|
@calls = 0
|
48
|
+
@assertions = 0
|
42
49
|
@valid_test_cases = 0
|
43
50
|
@generated = []
|
44
51
|
@arbitrary = nil
|
@@ -62,6 +69,10 @@ module Minitest
|
|
62
69
|
end
|
63
70
|
end
|
64
71
|
|
72
|
+
def where(&b)
|
73
|
+
@valid_test_case &= b.call
|
74
|
+
end
|
75
|
+
|
65
76
|
def explain
|
66
77
|
prop = if @status.valid?
|
67
78
|
'The property was proved to satsfaction across ' \
|
@@ -81,6 +92,12 @@ module Minitest
|
|
81
92
|
"#{@valid_test_cases} valid " \
|
82
93
|
"example#{@valid_test_cases == 1 ? '' : 's'}:\n" \
|
83
94
|
"#{@generated.map(&:value).inspect}"
|
95
|
+
elsif @status.exhausted?
|
96
|
+
"The property was unable to generate #{@max_success} test " \
|
97
|
+
'cases before generating ' \
|
98
|
+
"#{@max_success * @max_discard_ratio} rejected test " \
|
99
|
+
"cases. This might be a problem with the property's " \
|
100
|
+
'`where` blocks.'
|
84
101
|
end
|
85
102
|
trivial = if @trivial
|
86
103
|
"\nThe test does not appear to use any generated values " \
|
@@ -96,17 +113,31 @@ module Minitest
|
|
96
113
|
private
|
97
114
|
|
98
115
|
def iterate!
|
99
|
-
while continue_iterate? && @result.nil? && @valid_test_cases <= @max_success
|
116
|
+
while continue_iterate? && @result.nil? && @valid_test_cases <= @max_success
|
117
|
+
@valid_test_case = true
|
100
118
|
@generated = []
|
101
119
|
@generator = ::Minitest::Proptest::Gen.new(@random)
|
102
120
|
@calls += 1
|
103
|
-
|
121
|
+
|
122
|
+
success = begin
|
123
|
+
instance_eval(&@test_proc)
|
124
|
+
rescue Minitest::Assertion
|
125
|
+
if @valid_test_case
|
126
|
+
@result = @generated
|
127
|
+
@status = Status.interesting
|
128
|
+
end
|
129
|
+
rescue => e
|
130
|
+
raise e if @valid_test_case
|
131
|
+
end
|
132
|
+
if @valid_test_case && success
|
104
133
|
@status = Status.valid if @status.unknown?
|
105
134
|
@valid_test_cases += 1
|
106
|
-
|
135
|
+
elsif @valid_test_case
|
107
136
|
@result = @generated
|
108
137
|
@status = Status.interesting
|
109
138
|
end
|
139
|
+
|
140
|
+
@status = Status.exhausted if @calls >= @max_success * (@max_discard_ratio + 1)
|
110
141
|
@trivial = true if @generated.empty?
|
111
142
|
end
|
112
143
|
rescue => e
|
@@ -133,9 +164,20 @@ module Minitest
|
|
133
164
|
end
|
134
165
|
|
135
166
|
@generator = ::Minitest::Proptest::Gen.new(@random)
|
136
|
-
|
167
|
+
success = begin
|
168
|
+
instance_eval(&@test_proc)
|
169
|
+
rescue Minitest::Assertion
|
170
|
+
!@valid_test_case
|
171
|
+
rescue => e
|
172
|
+
if @valid_test_case
|
173
|
+
@status = Status.invalid
|
174
|
+
@exception = e
|
175
|
+
false
|
176
|
+
end
|
177
|
+
end
|
178
|
+
if success || !@valid_test_case
|
137
179
|
@generated = []
|
138
|
-
|
180
|
+
elsif @valid_test_case
|
139
181
|
@result = @generated
|
140
182
|
@status = Status.interesting
|
141
183
|
end
|
@@ -174,15 +216,28 @@ module Minitest
|
|
174
216
|
end
|
175
217
|
|
176
218
|
while continue_shrink? && run[:run] < to_test.length
|
177
|
-
@generated
|
178
|
-
run[:index]
|
219
|
+
@generated = []
|
220
|
+
run[:index] = -1
|
221
|
+
@valid_test_case = true
|
179
222
|
|
180
223
|
@generator = ::Minitest::Proptest::Gen.new(@random)
|
181
224
|
if to_test[run[:run]].map(&:first).reduce(&:+) < best_score
|
182
|
-
|
183
|
-
|
225
|
+
success = begin
|
226
|
+
instance_eval(&@test_proc)
|
227
|
+
rescue Minitest::Assertion
|
228
|
+
false
|
229
|
+
rescue => e
|
230
|
+
next unless @valid_test_case
|
231
|
+
|
232
|
+
@status = Status.invalid
|
233
|
+
@excption = e
|
234
|
+
break
|
235
|
+
end
|
236
|
+
|
237
|
+
if !success && @valid_test_case
|
184
238
|
# The first hit is guaranteed to be the best scoring due to the
|
185
239
|
# shrink candidates are pre-sorted.
|
240
|
+
best_generated = @generated
|
186
241
|
break
|
187
242
|
end
|
188
243
|
end
|
@@ -202,8 +257,8 @@ module Minitest
|
|
202
257
|
!@trivial &&
|
203
258
|
!@status.invalid? &&
|
204
259
|
!@status.overrun? &&
|
205
|
-
|
206
|
-
@
|
260
|
+
!@status.exhausted? &&
|
261
|
+
@valid_test_cases < @max_success
|
207
262
|
end
|
208
263
|
|
209
264
|
def continue_shrink?
|
@@ -4,6 +4,10 @@ module Minitest
|
|
4
4
|
module Proptest
|
5
5
|
# Sum type representing the possible statuses of a test run.
|
6
6
|
# Invalid, Overrun, and Interesting represent different failure classes.
|
7
|
+
# Exhausted represents having generated too many invalid test cases to
|
8
|
+
# verify the property. This precipitates as a failure class (the property is
|
9
|
+
# not proved) but can be a matter of inappropriate predicates in `where`
|
10
|
+
# blocks.
|
7
11
|
# Unknown represents a lack of information about the test run (typically
|
8
12
|
# having not run to satisfaction).
|
9
13
|
# Valid represents a test which has run to satisfaction.
|
@@ -23,16 +27,21 @@ module Minitest
|
|
23
27
|
class Valid < Status
|
24
28
|
end
|
25
29
|
|
30
|
+
class Exhausted < Status
|
31
|
+
end
|
32
|
+
|
26
33
|
invalid = Invalid.new.freeze
|
27
34
|
interesting = Interesting.new.freeze
|
28
35
|
overrun = Overrun.new.freeze
|
29
36
|
unknown = Unknown.new.freeze
|
37
|
+
exhausted = Exhausted.new.freeze
|
30
38
|
valid = Valid.new.freeze
|
31
39
|
|
32
40
|
define_singleton_method(:invalid) { invalid }
|
33
41
|
define_singleton_method(:interesting) { interesting }
|
34
42
|
define_singleton_method(:overrun) { overrun }
|
35
43
|
define_singleton_method(:unknown) { unknown }
|
44
|
+
define_singleton_method(:exhausted) { exhausted }
|
36
45
|
define_singleton_method(:valid) { valid }
|
37
46
|
|
38
47
|
def invalid?
|
@@ -47,6 +56,10 @@ module Minitest
|
|
47
56
|
self.is_a?(Unknown)
|
48
57
|
end
|
49
58
|
|
59
|
+
def exhausted?
|
60
|
+
self.is_a?(Exhausted)
|
61
|
+
end
|
62
|
+
|
50
63
|
def valid?
|
51
64
|
self.is_a?(Valid)
|
52
65
|
end
|
@@ -50,8 +50,6 @@ module Minitest
|
|
50
50
|
|
51
51
|
module Assertions
|
52
52
|
def property(&f)
|
53
|
-
self.assertions += 1
|
54
|
-
|
55
53
|
random_thunk = if Proptest.instance_variable_defined?(:@_random_seed)
|
56
54
|
r = Proptest.instance_variable_get(:@_random_seed)
|
57
55
|
->() { Proptest::DEFAULT_RANDOM.call(r) }
|
@@ -73,11 +71,14 @@ module Minitest
|
|
73
71
|
previous_failure: Proptest.reporter.lookup(file, classname, methodname)
|
74
72
|
)
|
75
73
|
prop.run!
|
74
|
+
self.assertions += prop.calls
|
76
75
|
|
77
76
|
if prop.status.valid? && !prop.trivial
|
78
77
|
Proptest.strike_failure(file, classname, methodname)
|
79
78
|
else
|
80
|
-
|
79
|
+
unless prop.status.exhausted? || prop.status.invalid?
|
80
|
+
Proptest.record_failure(file, classname, methodname, prop.result.map(&:value))
|
81
|
+
end
|
81
82
|
|
82
83
|
raise Minitest::Assertion, prop.explain
|
83
84
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: minitest-proptest
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tina Wuest
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-03-
|
11
|
+
date: 2024-03-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|