minitest-proptest 0.2.0 → 0.3.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: '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