minitest-proptest 0.1.0 → 0.2.1

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: 61aa3a3421a7b1996cf35540600199e11d18bbf5959d96866f994be97f372bcf
4
+ data.tar.gz: f89adae15e67f595c56f274d034b18e3e325fe097865ef60f42a7d49f7509214
5
5
  SHA512:
6
- metadata.gz: 6af2348e841f27a7401a64cc777dde8aad6a1487289cea278ecb6ec2771602143bce354dad192c55d2c57c4aa828ed6a34842d048a1dcb41d45d09fcc5e9e96a
7
- data.tar.gz: 8919b2d754966201a1f489c0b6594128e36fcf3301774cbabb0f67848b363d2702143829301042e0c8344dc0950c62995f2bf10147d94e4bbedc1e5f853cf063
6
+ metadata.gz: 6c114584acb4f0d59d93958c65e665a5acde52dce3083f1a1343e8cc85962ec11f4e8e582821bf86002cb2e02cfe45d255fe653450944fdc573112168a734520
7
+ data.tar.gz: 4f23345243cb106aa1fca9e4d9a4c87c6c20e22407e2564990de246cb81c95f96a2e8138a3991121ab24a2b2bfa9bf5066a7cdc3b639f0880d6e1170bd214652
@@ -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,11 +4,20 @@ 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
11
16
  test_proc,
17
+ # The file in which our property lives
18
+ filename,
19
+ # The method containing our property
20
+ methodname,
12
21
  # Any class which provides `rand` accepting both an Integer and a Range
13
22
  # is acceptable. The default value is Ruby's standard Mersenne Twister
14
23
  # implementation.
@@ -28,6 +37,8 @@ module Minitest
28
37
  previous_failure: []
29
38
  )
30
39
  @test_proc = test_proc
40
+ @filename = filename
41
+ @methodname = methodname
31
42
  @random = random.call
32
43
  @generator = ::Minitest::Proptest::Gen.new(@random)
33
44
  @max_success = max_success
@@ -36,13 +47,16 @@ module Minitest
36
47
  @max_shrinks = max_shrinks
37
48
  @status = Status.unknown
38
49
  @trivial = false
50
+ @valid_test_case = true
39
51
  @result = nil
40
52
  @exception = nil
41
53
  @calls = 0
54
+ @assertions = 0
42
55
  @valid_test_cases = 0
43
56
  @generated = []
44
57
  @arbitrary = nil
45
58
  @previous_failure = previous_failure.to_a
59
+ @local_variables = {}
46
60
  end
47
61
 
48
62
  def run!
@@ -51,17 +65,6 @@ module Minitest
51
65
  shrink!
52
66
  end
53
67
 
54
- def arbitrary(*classes)
55
- if @arbitrary
56
- @arbitrary.call(*classes)
57
- else
58
- a = @generator.for(*classes)
59
- @generated << a
60
- @status = Status.overrun unless @generated.length <= @max_size
61
- a.value
62
- end
63
- end
64
-
65
68
  def explain
66
69
  prop = if @status.valid?
67
70
  'The property was proved to satsfaction across ' \
@@ -77,10 +80,26 @@ module Minitest
77
80
  elsif @status.unknown?
78
81
  'The property has not yet been tested.'
79
82
  elsif @status.interesting?
80
- 'The property has found the following counterexample after ' \
81
- "#{@valid_test_cases} valid " \
82
- "example#{@valid_test_cases == 1 ? '' : 's'}:\n" \
83
- "#{@generated.map(&:value).inspect}"
83
+ info = 'A counterexample to a property has been found after ' \
84
+ "#{@valid_test_cases} valid " \
85
+ "example#{@valid_test_cases == 1 ? '' : 's'}.\n"
86
+ var_info = if @local_variables.empty?
87
+ 'Variables local to the property were unable ' \
88
+ 'to be determined. This is usually a bug.'
89
+ else
90
+ "The values at the time of the failure were:\n"
91
+ end
92
+ vars = @local_variables
93
+ .map { |k, v| "\t#{k}: #{v.inspect}" }
94
+ .join("\n")
95
+
96
+ info + var_info + vars
97
+ elsif @status.exhausted?
98
+ "The property was unable to generate #{@max_success} test " \
99
+ 'cases before generating ' \
100
+ "#{@max_success * @max_discard_ratio} rejected test " \
101
+ "cases. This might be a problem with the property's " \
102
+ '`where` blocks.'
84
103
  end
85
104
  trivial = if @trivial
86
105
  "\nThe test does not appear to use any generated values " \
@@ -95,18 +114,47 @@ module Minitest
95
114
 
96
115
  private
97
116
 
117
+ def arbitrary(*classes)
118
+ if @arbitrary
119
+ @arbitrary.call(*classes)
120
+ else
121
+ a = @generator.for(*classes)
122
+ @generated << a
123
+ @status = Status.overrun unless @generated.length <= @max_size
124
+ a.value
125
+ end
126
+ end
127
+
128
+ def where(&b)
129
+ @valid_test_case &= b.call
130
+ end
131
+
98
132
  def iterate!
99
- while continue_iterate? && @result.nil? && @valid_test_cases <= @max_success / 2
133
+ while continue_iterate? && @result.nil? && @valid_test_cases <= @max_success
134
+ @valid_test_case = true
100
135
  @generated = []
101
136
  @generator = ::Minitest::Proptest::Gen.new(@random)
102
137
  @calls += 1
103
- if instance_eval(&@test_proc)
138
+
139
+ success = begin
140
+ instance_eval(&@test_proc)
141
+ rescue Minitest::Assertion
142
+ if @valid_test_case
143
+ @result = @generated
144
+ @status = Status.interesting
145
+ end
146
+ rescue => e
147
+ raise e if @valid_test_case
148
+ end
149
+ if @valid_test_case && success
104
150
  @status = Status.valid if @status.unknown?
105
151
  @valid_test_cases += 1
106
- else
152
+ elsif @valid_test_case
107
153
  @result = @generated
108
154
  @status = Status.interesting
109
155
  end
156
+
157
+ @status = Status.exhausted if @calls >= @max_success * (@max_discard_ratio + 1)
110
158
  @trivial = true if @generated.empty?
111
159
  end
112
160
  rescue => e
@@ -133,9 +181,20 @@ module Minitest
133
181
  end
134
182
 
135
183
  @generator = ::Minitest::Proptest::Gen.new(@random)
136
- if instance_eval(&@test_proc)
184
+ success = begin
185
+ instance_eval(&@test_proc)
186
+ rescue Minitest::Assertion
187
+ !@valid_test_case
188
+ rescue => e
189
+ if @valid_test_case
190
+ @status = Status.invalid
191
+ @exception = e
192
+ false
193
+ end
194
+ end
195
+ if success || !@valid_test_case
137
196
  @generated = []
138
- else
197
+ elsif @valid_test_case
139
198
  @result = @generated
140
199
  @status = Status.interesting
141
200
  end
@@ -156,6 +215,17 @@ module Minitest
156
215
  candidates = @generated.map(&:shrink_candidates)
157
216
  old_arbitrary = @arbitrary
158
217
 
218
+ local_variables = {}
219
+ tracepoint = TracePoint.new(:b_return) do |trace|
220
+ if trace.path == @filename && trace.method_id.to_s == @methodname
221
+ b = trace.binding
222
+ vs = b.local_variables
223
+ known = vs.to_h { |lv| [lv.to_s, b.local_variable_get(lv)] }
224
+ local_variables.delete_if { true }
225
+ local_variables.merge!(known)
226
+ end
227
+ end
228
+
159
229
  to_test = candidates
160
230
  .map { |x| x.map { |y| [y] } }
161
231
  .reduce { |c, e| c.flat_map { |a| e.map { |b| a + b } } }
@@ -174,15 +244,31 @@ module Minitest
174
244
  end
175
245
 
176
246
  while continue_shrink? && run[:run] < to_test.length
177
- @generated = []
178
- run[:index] = -1
247
+ @generated = []
248
+ run[:index] = -1
249
+ @valid_test_case = true
179
250
 
180
251
  @generator = ::Minitest::Proptest::Gen.new(@random)
181
252
  if to_test[run[:run]].map(&:first).reduce(&:+) < best_score
182
- unless instance_eval(&@test_proc)
183
- best_generated = @generated
184
- # The first hit is guaranteed to be the best scoring due to the
253
+ success = begin
254
+ tracepoint.enable
255
+ instance_eval(&@test_proc)
256
+ rescue Minitest::Assertion
257
+ false
258
+ rescue => e
259
+ next unless @valid_test_case
260
+
261
+ @status = Status.invalid
262
+ @excption = e
263
+ break
264
+ ensure
265
+ tracepoint.disable
266
+ end
267
+
268
+ if !success && @valid_test_case
269
+ # The first hit is guaranteed to be the best scoring since the
185
270
  # shrink candidates are pre-sorted.
271
+ best_generated = @generated
186
272
  break
187
273
  end
188
274
  end
@@ -190,20 +276,22 @@ module Minitest
190
276
  @calls += 1
191
277
  run[:run] += 1
192
278
  end
279
+
193
280
  # Clean up after we're done
194
- @generated = best_generated
195
- @result = best_generated
196
- @generator = old_generator
197
- @random = old_random
198
- @arbitrary = old_arbitrary
281
+ @generated = best_generated
282
+ @result = best_generated
283
+ @generator = old_generator
284
+ @random = old_random
285
+ @arbitrary = old_arbitrary
286
+ @local_variables = local_variables
199
287
  end
200
288
 
201
289
  def continue_iterate?
202
290
  !@trivial &&
203
291
  !@status.invalid? &&
204
292
  !@status.overrun? &&
205
- @valid_test_cases < @max_success &&
206
- @calls < @max_success * @max_discard_ratio
293
+ !@status.exhausted? &&
294
+ @valid_test_cases < @max_success
207
295
  end
208
296
 
209
297
  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.1'
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) }
@@ -65,6 +63,8 @@ module Minitest
65
63
 
66
64
  prop = Minitest::Proptest::Property.new(
67
65
  f,
66
+ file,
67
+ methodname,
68
68
  random: random_thunk,
69
69
  max_success: Proptest.max_success,
70
70
  max_discard_ratio: Proptest.max_discard_ratio,
@@ -73,11 +73,14 @@ module Minitest
73
73
  previous_failure: Proptest.reporter.lookup(file, classname, methodname)
74
74
  )
75
75
  prop.run!
76
+ self.assertions += prop.calls
76
77
 
77
78
  if prop.status.valid? && !prop.trivial
78
79
  Proptest.strike_failure(file, classname, methodname)
79
80
  else
80
- Proptest.record_failure(file, classname, methodname, prop.result.map(&:value))
81
+ unless prop.status.exhausted? || prop.status.invalid?
82
+ Proptest.record_failure(file, classname, methodname, prop.result.map(&:value))
83
+ end
81
84
 
82
85
  raise Minitest::Assertion, prop.explain
83
86
  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.1
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-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest