minitest-proptest 0.1.0 → 0.2.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 +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
|