prop_check 0.13.0 → 0.14.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: 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