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 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