prop_check 0.13.0 → 0.14.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: fb51503257a33dbe349da1b2b582be1c0fe38465bbe10fb14f610885591c9f7b
4
- data.tar.gz: d357a2221e6b078c58d78826758cb22f4df3b8c34dd746001d854c505dfe2dea
3
+ metadata.gz: 013ca5101f31799feb0c86533c4100a781c451f937d6d8497a30476073e27e4b
4
+ data.tar.gz: f592ec086ca3018cfca1016baa1690619a5783274f02c910eb2ad03e5861a53d
5
5
  SHA512:
6
- metadata.gz: 5d757c5c95b19cb3805ff1cc14af811fbb5e4f0cdb0cadffa581f46d33d20f2c37b8093fda66828b6ce3862b0743723ebb5afe01b183acc4bdbe6fc3f96ddb9b
7
- data.tar.gz: e9288cedccdaafe609c2557a74176b9eea306fbff4aa2e9b9a4219702755efa33e53fb9f22d13f5ad0d7aa1af0dca732a1fe924d2119c43df39b71d1107be6d4
6
+ metadata.gz: f49b2514fe4e179013bd8c4a4683c1210b65d6852901156c6c5373f26305343fa4cae40860f13386a8f1fda33406927bf20a357f88cd5b99936318410837ba00
7
+ data.tar.gz: 261b980fb68c2d1af8baccb09919a838f91c1f6ae33f8c8a79923b2cb50490e4406e848ffbf6a75361d13d33719907a3342b4a7eed88ebdf37d1bdd8cd981ee4
@@ -1,3 +1,5 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7
1
3
  Metrics/LineLength:
2
4
  Max: 120
3
5
  Style/AccessModifierDeclarations:
@@ -1,3 +1,4 @@
1
+ - 0.14.0 - Adds `uniq: true` option to `Generators.array`. Makes `PropCheck::Property` an immutable object that returns copies that have changes whenever reconfiguring, allowing re-usable configuration.
1
2
  - 0.13.0 - Adds Generator#resize
2
3
  - 0.12.1 - Fixes shrinking when filtering bug.
3
4
  - 0.12.0 - `PropCheck::Generators#instance`
@@ -9,7 +9,7 @@ module PropCheck
9
9
  # Use this module by including it in the class (e.g. in your test suite)
10
10
  # where you want to use them.
11
11
  module Generators
12
- extend self
12
+ module_function
13
13
 
14
14
  ##
15
15
  # Always returns the same value, regardless of `size` or `rng` (random number generator state)
@@ -146,7 +146,7 @@ module PropCheck
146
146
  end
147
147
  end
148
148
 
149
- @special_floats = [Float::NAN, Float::INFINITY, -Float::INFINITY, Float::MAX, Float::MIN, 0.0.next_float, 0.0.prev_float]
149
+ @@special_floats = [Float::NAN, Float::INFINITY, -Float::INFINITY, Float::MAX, Float::MIN, 0.0.next_float, 0.0.prev_float]
150
150
  ##
151
151
  # Generates floating-point numbers
152
152
  # Will generate NaN, Infinity, -Infinity,
@@ -159,7 +159,7 @@ module PropCheck
159
159
  # >> Generators.float().sample(10, size: 10, rng: Random.new(42))
160
160
  # => [4.0, 9.555555555555555, 0.0, -Float::INFINITY, 5.5, -5.818181818181818, 1.1428571428571428, 0.0, 8.0, 7.857142857142858]
161
161
  def float
162
- frequency(99 => real_float, 1 => one_of(*@special_floats.map(&method(:constant))))
162
+ frequency(99 => real_float, 1 => one_of(*@@special_floats.map(&method(:constant))))
163
163
  end
164
164
 
165
165
  ##
@@ -241,6 +241,12 @@ module PropCheck
241
241
  # `empty:` When false, behaves the same as `min: 1`
242
242
  # `min:` Ensures at least this many elements are generated. (default: 0)
243
243
  # `max:` Ensures at most this many elements are generated. When nil, an arbitrary count is used instead. (default: nil)
244
+ # `uniq:` When `true`, ensures that all elements in the array are unique.
245
+ # When given a proc, uses the result of this proc to check for uniqueness.
246
+ # (matching the behaviour of `Array#uniq`)
247
+ # If it is not possible to generate another unique value after the configured `max_consecutive_attempts`
248
+ # an `PropCheck::Errors::GeneratorExhaustedError` will be raised.
249
+ # (default: `false`)
244
250
  #
245
251
  #
246
252
  # >> Generators.array(Generators.positive_integer).sample(5, size: 1, rng: Random.new(42))
@@ -252,25 +258,68 @@ module PropCheck
252
258
  # => [[], [2], [], [], [2]]
253
259
  # >> Generators.array(Generators.positive_integer, empty: false).sample(5, size: 1, rng: Random.new(1))
254
260
  # => [[2], [1], [2], [1], [1]]
261
+ #
262
+ # >> Generators.array(Generators.boolean, uniq: true).sample(5, rng: Random.new(1))
263
+ # => [[true, false], [false, true], [true, false], [false, true], [false, true]]
255
264
 
256
-
257
- def array(element_generator, min: 0, max: nil, empty: true)
265
+ def array(element_generator, min: 0, max: nil, empty: true, uniq: false)
258
266
  min = 1 if min.zero? && !empty
267
+ uniq = proc { |x| x } if uniq == true
259
268
 
260
- res = proc do |count|
261
- count = min + 1 if count < min
262
- count += 1 if count == min && min != 0
263
- generators = (min...count).map do
264
- element_generator.clone
265
- end
269
+ if max.nil?
270
+ nonnegative_integer.bind { |count| make_array(element_generator, min, count, uniq) }
271
+ else
272
+ make_array(element_generator, min, max, uniq)
273
+ end
274
+ end
266
275
 
267
- tuple(*generators)
276
+ private def make_array(element_generator, min, count, uniq)
277
+ amount = min if count < min
278
+ amount = min if count == min && min != 0
279
+ amount ||= (count - min)
280
+
281
+ # Simple, optimized implementation:
282
+ return make_array_simple(element_generator, amount) unless uniq
283
+
284
+ # More complex implementation that filters duplicates
285
+ make_array_uniq(element_generator, min, amount, uniq)
286
+ end
287
+
288
+ private def make_array_simple(element_generator, amount)
289
+ generators = amount.times.map do
290
+ element_generator.clone
268
291
  end
269
292
 
270
- if max.nil?
271
- nonnegative_integer.bind(&res)
272
- else
273
- res.call(max)
293
+ tuple(*generators)
294
+ end
295
+
296
+ private def make_array_uniq(element_generator, min, amount, uniq_fun)
297
+ Generator.new do |**kwargs|
298
+ arr = []
299
+ uniques = Set.new
300
+ count = 0
301
+ (0..).lazy.map do
302
+ elem = element_generator.clone.generate(**kwargs)
303
+ if uniques.add?(uniq_fun.call(elem.root))
304
+ arr.push(elem)
305
+ count = 0
306
+ else
307
+ count += 1
308
+ end
309
+
310
+ if count > kwargs[:max_consecutive_attempts]
311
+ if arr.size >= min
312
+ # Give up and return shorter array in this case
313
+ amount = min
314
+ else
315
+ raise Errors::GeneratorExhaustedError, "Too many consecutive elements filtered by 'uniq:'."
316
+ end
317
+ end
318
+ end
319
+ .take_while { arr.size < amount }
320
+ .force
321
+
322
+ LazyTree.zip(arr).map { |array| array.uniq(&uniq_fun) }
274
323
  end
275
324
  end
276
325
 
@@ -300,7 +349,7 @@ module PropCheck
300
349
  .map(&:to_h)
301
350
  end
302
351
 
303
- @alphanumeric_chars = [('a'..'z'), ('A'..'Z'), ('0'..'9')].flat_map(&:to_a).freeze
352
+ @@alphanumeric_chars = [('a'..'z'), ('A'..'Z'), ('0'..'9')].flat_map(&:to_a).freeze
304
353
  ##
305
354
  # Generates a single-character string
306
355
  # containing one of a..z, A..Z, 0..9
@@ -310,7 +359,7 @@ module PropCheck
310
359
  # >> Generators.alphanumeric_char.sample(5, size: 10, rng: Random.new(42))
311
360
  # => ["M", "Z", "C", "o", "Q"]
312
361
  def alphanumeric_char
313
- one_of(*@alphanumeric_chars.map(&method(:constant)))
362
+ one_of(*@@alphanumeric_chars.map(&method(:constant)))
314
363
  end
315
364
 
316
365
  ##
@@ -321,11 +370,13 @@ module PropCheck
321
370
  #
322
371
  # >> Generators.alphanumeric_string.sample(5, size: 10, rng: Random.new(42))
323
372
  # => ["ZCoQ", "8uM", "wkkx0JNx", "v0bxRDLb", "Gl5v8RyWA6"]
373
+ #
374
+ # Accepts the same options as `array`
324
375
  def alphanumeric_string(**kwargs)
325
376
  array(alphanumeric_char, **kwargs).map(&:join)
326
377
  end
327
378
 
328
- @printable_ascii_chars = (' '..'~').to_a.freeze
379
+ @@printable_ascii_chars = (' '..'~').to_a.freeze
329
380
 
330
381
  ##
331
382
  # Generates a single-character string
@@ -336,7 +387,7 @@ module PropCheck
336
387
  # >> Generators.printable_ascii_char.sample(size: 10, rng: Random.new(42))
337
388
  # => ["S", "|", ".", "g", "\\", "4", "r", "v", "j", "j"]
338
389
  def printable_ascii_char
339
- one_of(*@printable_ascii_chars.map(&method(:constant)))
390
+ one_of(*@@printable_ascii_chars.map(&method(:constant)))
340
391
  end
341
392
 
342
393
  ##
@@ -347,12 +398,14 @@ module PropCheck
347
398
  #
348
399
  # >> Generators.printable_ascii_string.sample(5, size: 10, rng: Random.new(42))
349
400
  # => ["S|.g", "rvjjw7\"5T!", "=", "!_[4@", "Y"]
401
+ #
402
+ # Accepts the same options as `array`
350
403
  def printable_ascii_string(**kwargs)
351
404
  array(printable_ascii_char, **kwargs).map(&:join)
352
405
  end
353
406
 
354
- @ascii_chars = [
355
- @printable_ascii_chars,
407
+ @@ascii_chars = [
408
+ @@printable_ascii_chars,
356
409
  [
357
410
  "\n",
358
411
  "\r",
@@ -375,7 +428,7 @@ module PropCheck
375
428
  # >> Generators.ascii_char.sample(size: 10, rng: Random.new(42))
376
429
  # => ["d", "S", "|", ".", "g", "\\", "4", "d", "r", "v"]
377
430
  def ascii_char
378
- one_of(*@ascii_chars.map(&method(:constant)))
431
+ one_of(*@@ascii_chars.map(&method(:constant)))
379
432
  end
380
433
 
381
434
  ##
@@ -386,12 +439,14 @@ module PropCheck
386
439
  #
387
440
  # >> Generators.ascii_string.sample(5, size: 10, rng: Random.new(42))
388
441
  # => ["S|.g", "drvjjw\b\a7\"", "!w=E!_[4@k", "x", "zZI{[o"]
442
+ #
443
+ # Accepts the same options as `array`
389
444
  def ascii_string(**kwargs)
390
445
  array(ascii_char, **kwargs).map(&:join)
391
446
  end
392
447
 
393
- @printable_chars = [
394
- @ascii_chars,
448
+ @@printable_chars = [
449
+ @@ascii_chars,
395
450
  "\u{A0}".."\u{D7FF}",
396
451
  "\u{E000}".."\u{FFFD}",
397
452
  "\u{10000}".."\u{10FFFF}"
@@ -406,7 +461,7 @@ module PropCheck
406
461
  # >> Generators.printable_char.sample(size: 10, rng: Random.new(42))
407
462
  # => ["吏", "", "", "", "", "", "", "", "", "Ȍ"]
408
463
  def printable_char
409
- one_of(*@printable_chars.map(&method(:constant)))
464
+ one_of(*@@printable_chars.map(&method(:constant)))
410
465
  end
411
466
 
412
467
  ##
@@ -417,6 +472,8 @@ module PropCheck
417
472
  #
418
473
  # >> Generators.printable_string.sample(5, size: 10, rng: Random.new(42))
419
474
  # => ["", "Ȍ", "𐁂", "Ȕ", ""]
475
+ #
476
+ # Accepts the same options as `array`
420
477
  def printable_string(**kwargs)
421
478
  array(printable_char, **kwargs).map(&:join)
422
479
  end
@@ -443,6 +500,8 @@ module PropCheck
443
500
  #
444
501
  # >> Generators.string.sample(5, size: 10, rng: Random.new(42))
445
502
  # => ["\u{A3DB3}𠍜\u{3F46A}\u{1AEBC}", "􍙦𡡹󴇒\u{DED74}𪱣\u{43E97}ꂂ\u{50695}􏴴\u{C0301}", "\u{4FD9D}", "\u{C14BF}\u{193BB}𭇋󱣼\u{76B58}", "𦐺\u{9FDDB}\u{80ABB}\u{9E3CF}𐂽\u{14AAE}"]
503
+ #
504
+ # Accepts the same options as `array`
446
505
  def string(**kwargs)
447
506
  array(char, **kwargs).map(&:join)
448
507
  end
@@ -19,10 +19,11 @@
19
19
  # wrapping the elements of an enumerable with hooks.
20
20
  class PropCheck::Hooks
21
21
  # attr_reader :before, :after, :around
22
- def initialize()
23
- @before = proc {}
24
- @after = proc {}
25
- @around = proc { |*args, &block| block.call(*args) }
22
+ def initialize(before: proc {}, after: proc {}, around: proc { |*args, &block| block.call(*args) })
23
+ @before = before
24
+ @after = after
25
+ @around = around
26
+ freeze
26
27
  end
27
28
 
28
29
  def wrap_enum(enumerable)
@@ -59,34 +60,40 @@ class PropCheck::Hooks
59
60
  # Adds `hook` to the `before` proc.
60
61
  # It is called after earlier-added `before` procs.
61
62
  def add_before(&hook)
62
- old_before = @before
63
- @before = proc {
64
- old_before.call
63
+ # old_before = @before
64
+ new_before = proc {
65
+ @before.call
65
66
  hook.call
66
67
  }
68
+ # self
69
+ self.class.new(before: new_before, after: @after, around: @around)
67
70
  end
68
71
 
69
72
  ##
70
73
  # Adds `hook` to the `after` proc.
71
74
  # It is called before earlier-added `after` procs.
72
75
  def add_after(&hook)
73
- old_after = @after
74
- @after = proc {
76
+ # old_after = @after
77
+ new_after = proc {
75
78
  hook.call
76
- old_after.call
79
+ @after.call
77
80
  }
81
+ # self
82
+ self.class.new(before: @before, after: new_after, around: @around)
78
83
  end
79
84
 
80
85
  ##
81
86
  # Adds `hook` to the `around` proc.
82
87
  # It is called _inside_ earlier-added `around` procs.
83
88
  def add_around(&hook)
84
- old_around = @around
85
- @around = proc do |&block|
86
- old_around.call do |*args|
89
+ # old_around = @around
90
+ new_around = proc do |&block|
91
+ @around.call do |*args|
87
92
  hook.call(*args, &block)
88
93
  end
89
94
  end
95
+ # self
96
+ self.class.new(before: @before, after: @after, around: new_around)
90
97
  end
91
98
 
92
99
  ##
@@ -7,9 +7,15 @@ require 'prop_check/property/shrinker'
7
7
  require 'prop_check/hooks'
8
8
  module PropCheck
9
9
  ##
10
- # Run properties
10
+ # Create and run property-checks.
11
+ #
12
+ # For simple usage, see `.forall`.
13
+ #
14
+ # For advanced usage, call `PropCheck::Property.new(...)` and then configure it to your liking
15
+ # using e.g. `#with_config`, `#before`, `#after`, `#around` etc.
16
+ # Each of these methods will return a new `Property`, so earlier properties are not mutated.
17
+ # This allows you to re-use configuration and hooks between multiple tests.
11
18
  class Property
12
-
13
19
  ##
14
20
  # Main entry-point to create (and possibly immediately run) a property-test.
15
21
  #
@@ -36,7 +42,6 @@ module PropCheck
36
42
  # of this class on before finally passing a block to it using `#check`.
37
43
  # (so `forall(Generators.integer) do |val| ... end` and forall(Generators.integer).check do |val| ... end` are the same)
38
44
  def self.forall(*bindings, **kwbindings, &block)
39
-
40
45
  property = new(*bindings, **kwbindings)
41
46
 
42
47
  return property.check(&block) if block_given?
@@ -61,19 +66,25 @@ module PropCheck
61
66
  yield(configuration)
62
67
  end
63
68
 
64
- attr_reader :bindings, :condition
65
-
66
69
  def initialize(*bindings, **kwbindings)
67
- raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty?
68
-
69
- # @bindings = bindings
70
- # @kwbindings = kwbindings
71
- @gen = gen_from_bindings(bindings, kwbindings)
72
- @condition = proc { true }
73
70
  @config = self.class.configuration
74
71
  @hooks = PropCheck::Hooks.new
72
+
73
+ @gen = gen_from_bindings(bindings, kwbindings) unless bindings.empty? && kwbindings.empty?
74
+ freeze
75
75
  end
76
76
 
77
+ # [:condition, :config, :hooks, :gen].each do |symbol|
78
+ # define_method(symbol) do
79
+ # self.instance_variable_get("@#{symbol}")
80
+ # end
81
+
82
+ # protected define_method("#{symbol}=") do |value|
83
+ # duplicate = self.dup
84
+ # duplicate.instance_variable_set("@#{symbol}", value)
85
+ # duplicate
86
+ # end
87
+
77
88
  ##
78
89
  # Returns the configuration of this property
79
90
  # for introspection.
@@ -91,11 +102,22 @@ module PropCheck
91
102
  # you can immediately pass a block to this method.
92
103
  # (so `forall(a: Generators.integer).with_config(verbose: true) do ... end` is the same as `forall(a: Generators.integer).with_config(verbose: true).check do ... end`)
93
104
  def with_config(**config, &block)
94
- @config = @config.merge(config)
105
+ duplicate = self.dup
106
+ duplicate.instance_variable_set(:@config, @config.merge(config))
107
+ duplicate.freeze
95
108
 
96
- return self.check(&block) if block_given?
109
+ return duplicate.check(&block) if block_given?
97
110
 
98
- self
111
+ duplicate
112
+ end
113
+
114
+ def with_bindings(*bindings, **kwbindings)
115
+ raise ArgumentError, 'No bindings specified!' if bindings.empty? && kwbindings.empty?
116
+
117
+ duplicate = self.dup
118
+ duplicate.instance_variable_set(:@gen, gen_from_bindings(bindings, kwbindings))
119
+ duplicate.freeze
120
+ duplicate
99
121
  end
100
122
 
101
123
  ##
@@ -107,14 +129,12 @@ module PropCheck
107
129
  # you might encounter a GeneratorExhaustedError.
108
130
  # Only filter if you have few inputs to reject. Otherwise, improve your generators.
109
131
  def where(&condition)
110
- # original_condition = @condition.dup
111
- # @condition = proc do |val|
112
- # call_splatted(val, &original_condition) && call_splatted(val, &condition)
113
- # # original_condition.call(val) && condition.call(val)
114
- # end
115
- @gen = @gen.where(&condition)
116
-
117
- self
132
+ raise ArgumentError, 'No generator bindings specified! #where should be called after `#forall` or `#with_bindings`.' unless @gen
133
+
134
+ duplicate = self.dup
135
+ duplicate.instance_variable_set(:@gen, @gen.where(&condition))
136
+ duplicate.freeze
137
+ duplicate
118
138
  end
119
139
 
120
140
 
@@ -124,8 +144,10 @@ module PropCheck
124
144
  # This is useful to add setup logic
125
145
  # When called multiple times, earlier-added hooks will be called _before_ `hook` is called.
126
146
  def before(&hook)
127
- @hooks.add_before(&hook)
128
- self
147
+ duplicate = self.dup
148
+ duplicate.instance_variable_set(:@hooks, @hooks.add_before(&hook))
149
+ duplicate.freeze
150
+ duplicate
129
151
  end
130
152
 
131
153
  ##
@@ -134,8 +156,10 @@ module PropCheck
134
156
  # This is useful to add teardown logic
135
157
  # When called multiple times, earlier-added hooks will be called _after_ `hook` is called.
136
158
  def after(&hook)
137
- @hooks.add_after(&hook)
138
- self
159
+ duplicate = self.dup
160
+ duplicate.instance_variable_set(:@hooks, @hooks.add_after(&hook))
161
+ duplicate.freeze
162
+ duplicate
139
163
  end
140
164
 
141
165
  ##
@@ -152,8 +176,10 @@ module PropCheck
152
176
  # it is possible for the code after `yield` not to be called.
153
177
  # So make sure that cleanup logic is wrapped with the `ensure` keyword.
154
178
  def around(&hook)
155
- @hooks.add_around(&hook)
156
- self
179
+ duplicate = self.dup
180
+ duplicate.instance_variable_set(:@hooks, @hooks.add_around(&hook))
181
+ duplicate.freeze
182
+ duplicate
157
183
  end
158
184
 
159
185
  ##
@@ -238,6 +264,8 @@ c.f. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-k
238
264
  end
239
265
 
240
266
  private def attempts_enum(binding_generator)
267
+ ap @hooks
268
+
241
269
  @hooks
242
270
  .wrap_enum(raw_attempts_enum(binding_generator))
243
271
  .lazy
@@ -10,7 +10,7 @@ module PropCheck
10
10
 
11
11
  def initialize(
12
12
  verbose: false,
13
- n_runs: 1_000,
13
+ n_runs: 100,
14
14
  max_generate_attempts: 10_000,
15
15
  max_shrink_steps: 10_000,
16
16
  max_consecutive_attempts: 30
@@ -1,3 +1,3 @@
1
1
  module PropCheck
2
- VERSION = '0.13.0'
2
+ VERSION = '0.14.0'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prop_check
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Qqwy/Wiebe-Marten Wijnja
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-03 00:00:00.000000000 Z
11
+ date: 2020-08-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_print