minitest-proptest 0.2.0 → 0.3.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: '0596e6f6a66614dba0199eccc4e18a5bf6b869fd0db46dff7636a8772eacb75b'
4
- data.tar.gz: a956c579c5bb355a5b687f64109917918870dd7dac1323a18a72d44e4ca623a6
3
+ metadata.gz: 236dabad0376379b518d9c29fc12f1bca0a17343c9dc85c6ff19ef94419698e8
4
+ data.tar.gz: dbe3075e50971bac885e3fcc6d3c607c9edb4a9e5bac4cc9dc1863a874ac2d5a
5
5
  SHA512:
6
- metadata.gz: d6206c7a778b9b55e0674e204afea71721eb8b6e4fe2257691cc1cefd5e76885eff08d648f9a61172806872f1abe5f1b232186fcc803e6fcc560241fd71fac8c
7
- data.tar.gz: 1ca1d04ddcdecb9f661e0af6cffc87dda30af6a9b06bb6fe406a5f27ec445a0cf3cd9ac4d9050b4a243ab40d76d066358f6cf9612181cc546a2076b76a7d9041
6
+ metadata.gz: 70f655e9358ce5e198b5489e94a8cdb32d0a58b0d88393482f7faf8e911ab81a50bdc5514add06cb168f13a2c687a825725e00e59c0497937bd076ba8279bc91
7
+ data.tar.gz: 9dde2177e0f6a9458f1226abd43c5bd1b7048c3c69c6f746043fb02a4c5e9de1f5e8082bad699001f238b478528da350a63ede6d84b6058f011f9d6960217c88
@@ -98,7 +98,11 @@ module Minitest
98
98
  def shrink_candidates
99
99
  fs = @type_parameters.map { |x| x.method(:shrink_function) }
100
100
  os = score
101
- candidates = shrink_function(*fs, value)
101
+ # Ensure that the end of the shrink attempt will contain the original
102
+ # value. This is necessary to guarantee that the shrink process
103
+ # produces at least one failure for the purpose of capturing variable
104
+ # assignment.
105
+ candidates = shrink_function(*fs, value) + [value]
102
106
  candidates
103
107
  .map { |c| [force(c).score, c] }
104
108
  .reject { |(s, _)| s > os }
@@ -287,47 +287,27 @@ module Minitest
287
287
 
288
288
  generator_for(Int8) do
289
289
  r = sized(0xff)
290
- (r & 0x80).zero? ? r : -(((r & 0x7f) - 1) ^ 0x7f)
291
- end.with_shrink_function do |i|
292
- j = (i & 0x80).zero? ? i : -(((i & 0x7f) - 1) ^ 0x7f)
293
- integral_shrink.call(j)
294
- end
290
+ (r & 0x80).zero? ? r : -((r ^ 0x7f) - 0x7f)
291
+ end.with_shrink_function(&integral_shrink)
295
292
 
296
293
  generator_for(Int16) do
297
294
  r = sized(0xffff)
298
- (r & 0x8000).zero? ? r : -(((r & 0x7fff) - 1) ^ 0x7fff)
299
- end.with_shrink_function do |i|
300
- j = (i & 0x8000).zero? ? i : -(((i & 0x7fff) - 1) ^ 0x7fff)
301
- integral_shrink.call(j)
302
- end
295
+ (r & 0x8000).zero? ? r : -((r ^ 0x7fff) - 0x7fff)
296
+ end.with_shrink_function(&integral_shrink)
303
297
 
304
298
  generator_for(Int32) do
305
299
  r = sized(0xffffffff)
306
- (r & 0x80000000).zero? ? r : -(((r & 0x7fffffff) - 1) ^ 0x7fffffff)
307
- end.with_shrink_function do |i|
308
- j = if (i & 0x80000000).zero?
309
- i
310
- else
311
- -(((i & 0x7fffffff) - 1) ^ 0x7fffffff)
312
- end
313
- integral_shrink.call(j)
314
- end
300
+ (r & 0x80000000).zero? ? r : -((r ^ 0x7fffffff) - 0x7fffffff)
301
+ end.with_shrink_function(&integral_shrink)
315
302
 
316
303
  generator_for(Int64) do
317
304
  r = sized(0xffffffffffffffff)
318
305
  if (r & 0x8000000000000000).zero?
319
306
  r
320
307
  else
321
- -(((r & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
308
+ -((r ^ 0x7fffffffffffffff) - 0x7fffffffffffffff)
322
309
  end
323
- end.with_shrink_function do |i|
324
- j = if (i & 0x8000000000000000).zero?
325
- i
326
- else
327
- -(((i & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
328
- end
329
- integral_shrink.call(j)
330
- end
310
+ end.with_shrink_function(&integral_shrink)
331
311
 
332
312
  generator_for(UInt8) do
333
313
  sized(0xff)
@@ -471,6 +451,15 @@ module Minitest
471
451
  end.with_score_function do |_|
472
452
  1
473
453
  end
454
+
455
+ generator_for(Time) do
456
+ r = sized(0xffffffff)
457
+ Time.at((r & 0x80000000).zero? ? r : -((r ^ 0x7fffffff) - 0x7fffffff))
458
+ end.with_shrink_function do |t|
459
+ integral_shrink.call(t.to_i).map(&Time.method(:at))
460
+ end.with_score_function do |t|
461
+ t.to_i.abs
462
+ end
474
463
  end
475
464
  end
476
465
  end
@@ -7,6 +7,8 @@ module Minitest
7
7
  require 'minitest/assertions'
8
8
  include Minitest::Assertions
9
9
 
10
+ class InvalidProperty < StandardError; end
11
+
10
12
  attr_reader :calls, :result, :status, :trivial
11
13
 
12
14
  attr_accessor :assertions
@@ -14,6 +16,10 @@ module Minitest
14
16
  def initialize(
15
17
  # The function which proves the property
16
18
  test_proc,
19
+ # The file in which our property lives
20
+ filename,
21
+ # The method containing our property
22
+ methodname,
17
23
  # Any class which provides `rand` accepting both an Integer and a Range
18
24
  # is acceptable. The default value is Ruby's standard Mersenne Twister
19
25
  # implementation.
@@ -33,6 +39,8 @@ module Minitest
33
39
  previous_failure: []
34
40
  )
35
41
  @test_proc = test_proc
42
+ @filename = filename
43
+ @methodname = methodname
36
44
  @random = random.call
37
45
  @generator = ::Minitest::Proptest::Gen.new(@random)
38
46
  @max_success = max_success
@@ -41,7 +49,6 @@ module Minitest
41
49
  @max_shrinks = max_shrinks
42
50
  @status = Status.unknown
43
51
  @trivial = false
44
- @valid_test_case = true
45
52
  @result = nil
46
53
  @exception = nil
47
54
  @calls = 0
@@ -50,6 +57,7 @@ module Minitest
50
57
  @generated = []
51
58
  @arbitrary = nil
52
59
  @previous_failure = previous_failure.to_a
60
+ @local_variables = {}
53
61
  end
54
62
 
55
63
  def run!
@@ -58,21 +66,6 @@ module Minitest
58
66
  shrink!
59
67
  end
60
68
 
61
- def arbitrary(*classes)
62
- if @arbitrary
63
- @arbitrary.call(*classes)
64
- else
65
- a = @generator.for(*classes)
66
- @generated << a
67
- @status = Status.overrun unless @generated.length <= @max_size
68
- a.value
69
- end
70
- end
71
-
72
- def where(&b)
73
- @valid_test_case &= b.call
74
- end
75
-
76
69
  def explain
77
70
  prop = if @status.valid?
78
71
  'The property was proved to satsfaction across ' \
@@ -88,10 +81,20 @@ module Minitest
88
81
  elsif @status.unknown?
89
82
  'The property has not yet been tested.'
90
83
  elsif @status.interesting?
91
- 'The property has found the following counterexample after ' \
92
- "#{@valid_test_cases} valid " \
93
- "example#{@valid_test_cases == 1 ? '' : 's'}:\n" \
94
- "#{@generated.map(&:value).inspect}"
84
+ info = 'A counterexample to a property has been found after ' \
85
+ "#{@valid_test_cases} valid " \
86
+ "example#{@valid_test_cases == 1 ? '' : 's'}.\n"
87
+ var_info = if @local_variables.empty?
88
+ 'Variables local to the property were unable ' \
89
+ 'to be determined. This is usually a bug.'
90
+ else
91
+ "The values at the time of the failure were:\n"
92
+ end
93
+ vars = @local_variables
94
+ .map { |k, v| "\t#{k}: #{v.inspect}" }
95
+ .join("\n")
96
+
97
+ info + var_info + vars
95
98
  elsif @status.exhausted?
96
99
  "The property was unable to generate #{@max_success} test " \
97
100
  'cases before generating ' \
@@ -112,37 +115,47 @@ module Minitest
112
115
 
113
116
  private
114
117
 
118
+ def arbitrary(*classes)
119
+ if @arbitrary
120
+ @arbitrary.call(*classes)
121
+ else
122
+ a = @generator.for(*classes)
123
+ @generated << a
124
+ @status = Status.overrun unless @generated.length <= @max_size
125
+ a.value
126
+ end
127
+ end
128
+
129
+ def where(&b)
130
+ raise InvalidProperty unless b.call
131
+ end
132
+
115
133
  def iterate!
116
134
  while continue_iterate? && @result.nil? && @valid_test_cases <= @max_success
117
- @valid_test_case = true
118
135
  @generated = []
119
136
  @generator = ::Minitest::Proptest::Gen.new(@random)
120
137
  @calls += 1
121
138
 
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
133
- @status = Status.valid if @status.unknown?
134
- @valid_test_cases += 1
135
- elsif @valid_test_case
139
+ begin
140
+ if instance_eval(&@test_proc)
141
+ @status = Status.valid if @status.unknown?
142
+ @valid_test_cases += 1
143
+ else
144
+ @result = @generated
145
+ @status = Status.interesting
146
+ end
147
+ rescue Minitest::Assertion
136
148
  @result = @generated
137
149
  @status = Status.interesting
150
+ rescue InvalidProperty
151
+ rescue => e
152
+ @status = Status.invalid
153
+ @exception = e
138
154
  end
139
155
 
140
156
  @status = Status.exhausted if @calls >= @max_success * (@max_discard_ratio + 1)
141
157
  @trivial = true if @generated.empty?
142
158
  end
143
- rescue => e
144
- @status = Status.invalid
145
- @exception = e
146
159
  end
147
160
 
148
161
  def rerun!
@@ -164,22 +177,22 @@ module Minitest
164
177
  end
165
178
 
166
179
  @generator = ::Minitest::Proptest::Gen.new(@random)
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
179
- @generated = []
180
- elsif @valid_test_case
180
+ begin
181
+ if instance_eval(&@test_proc)
182
+ @generated = []
183
+ else
184
+ @result = @generated
185
+ @status = Status.interesting
186
+ end
187
+ rescue Minitest::Assertion
181
188
  @result = @generated
182
189
  @status = Status.interesting
190
+ rescue InvalidProperty
191
+ @generated = []
192
+ rescue => e
193
+ @result = @generated
194
+ @status = Status.invalid
195
+ @exception = e
183
196
  end
184
197
 
185
198
  # Clean up after we're done
@@ -198,6 +211,23 @@ module Minitest
198
211
  candidates = @generated.map(&:shrink_candidates)
199
212
  old_arbitrary = @arbitrary
200
213
 
214
+ # Using a TracePoint to determine variable assignments at the time of
215
+ # the failure only occurs within shrink! - this is a deliberate decision
216
+ # which eliminates all time lost in iterate! to optimize for the success
217
+ # case. The tradeoff is that if all shrinking fails, one additional
218
+ # cycle (with the values which produced the original failure) will be
219
+ # required.
220
+ local_variables = {}
221
+ tracepoint = TracePoint.new(:b_return) do |trace|
222
+ if trace.path == @filename && trace.method_id.to_s == @methodname
223
+ b = trace.binding
224
+ vs = b.local_variables
225
+ known = vs.to_h { |lv| [lv.to_s, b.local_variable_get(lv)] }
226
+ local_variables.delete_if { true }
227
+ local_variables.merge!(known)
228
+ end
229
+ end
230
+
201
231
  to_test = candidates
202
232
  .map { |x| x.map { |y| [y] } }
203
233
  .reduce { |c, e| c.flat_map { |a| e.map { |b| a + b } } }
@@ -221,36 +251,42 @@ module Minitest
221
251
  @valid_test_case = true
222
252
 
223
253
  @generator = ::Minitest::Proptest::Gen.new(@random)
224
- if to_test[run[:run]].map(&:first).reduce(&:+) < best_score
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
238
- # The first hit is guaranteed to be the best scoring due to the
239
- # shrink candidates are pre-sorted.
254
+ if to_test[run[:run]].map(&:first).reduce(&:+) <= best_score
255
+ begin
256
+ tracepoint.enable
257
+ unless instance_eval(&@test_proc)
258
+ # The first hit is guaranteed to be the best scoring since the
259
+ # shrink candidates are pre-sorted.
260
+ best_generated = @generated
261
+ break
262
+ end
263
+ rescue Minitest::Assertion
240
264
  best_generated = @generated
241
265
  break
266
+ rescue InvalidProperty
267
+ # Invalid test case generated- continue
268
+ rescue => e
269
+ next unless @valid_test_case
270
+
271
+ @status = Status.invalid
272
+ @excption = e
273
+ break
274
+ ensure
275
+ tracepoint.disable
242
276
  end
243
277
  end
244
278
 
245
279
  @calls += 1
246
280
  run[:run] += 1
247
281
  end
282
+
248
283
  # Clean up after we're done
249
- @generated = best_generated
250
- @result = best_generated
251
- @generator = old_generator
252
- @random = old_random
253
- @arbitrary = old_arbitrary
284
+ @generated = best_generated
285
+ @result = best_generated
286
+ @generator = old_generator
287
+ @random = old_random
288
+ @arbitrary = old_arbitrary
289
+ @local_variables = local_variables
254
290
  end
255
291
 
256
292
  def continue_iterate?
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Minitest
4
4
  module Proptest
5
- VERSION = '0.2.0'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  end
@@ -63,6 +63,8 @@ module Minitest
63
63
 
64
64
  prop = Minitest::Proptest::Property.new(
65
65
  f,
66
+ file,
67
+ methodname,
66
68
  random: random_thunk,
67
69
  max_success: Proptest.max_success,
68
70
  max_discard_ratio: Proptest.max_discard_ratio,
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.2.0
4
+ version: 0.3.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-21 00:00:00.000000000 Z
11
+ date: 2024-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest