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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d2f1cde4cf0a022319a3336a06e5509275ea420b5b36d39835f938c6ef830fae
4
- data.tar.gz: 50a1ce6ade048840831ad943840c914cda998da1e6f7b98c07d02b7ee859d2db
3
+ metadata.gz: '0596e6f6a66614dba0199eccc4e18a5bf6b869fd0db46dff7636a8772eacb75b'
4
+ data.tar.gz: a956c579c5bb355a5b687f64109917918870dd7dac1323a18a72d44e4ca623a6
5
5
  SHA512:
6
- metadata.gz: 6af2348e841f27a7401a64cc777dde8aad6a1487289cea278ecb6ec2771602143bce354dad192c55d2c57c4aa828ed6a34842d048a1dcb41d45d09fcc5e9e96a
7
- data.tar.gz: 8919b2d754966201a1f489c0b6594128e36fcf3301774cbabb0f67848b363d2702143829301042e0c8344dc0950c62995f2bf10147d94e4bbedc1e5f853cf063
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
- .flat_map { |i| [i - 1, i, i + 1] }
129
- .reject { |i| i.abs >= x.abs }
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 == 0 || 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
- candidates
142
- .flat_map { |i| [i - 1, i, i + 1] }
143
- .reject { |i| i.abs >= x.abs }
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 do |f|
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
- generator_for(Float64) do
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 do |f|
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
- (0..7)
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 do |f|
339
- if f.nan? || f.infinite?
340
- 0
341
- else
342
- f.abs.ceil
343
- end
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
- attr_reader :result, :status, :trivial
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 / 2
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
- if instance_eval(&@test_proc)
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
- else
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
- if instance_eval(&@test_proc)
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
- else
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] = -1
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
- unless instance_eval(&@test_proc)
183
- best_generated = @generated
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
- @valid_test_cases < @max_success &&
206
- @calls < @max_success * @max_discard_ratio
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Minitest
4
4
  module Proptest
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  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
- Proptest.record_failure(file, classname, methodname, prop.result.map(&:value))
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.1.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-13 00:00:00.000000000 Z
11
+ date: 2024-03-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest