lib/prop_check/generators.rb in prop_check-0.13.0 vs lib/prop_check/generators.rb in prop_check-0.14.0

- old
+ new

@@ -7,11 +7,11 @@ ## # Contains common generators. # Use this module by including it in the class (e.g. in your test suite) # where you want to use them. module Generators - extend self + module_function ## # Always returns the same value, regardless of `size` or `rng` (random number generator state) # # No shrinking (only considers the current single value `val`). @@ -144,11 +144,11 @@ tuple(integer, integer, integer).map do |a, b, c| fraction(a, b, c) end end - @special_floats = [Float::NAN, Float::INFINITY, -Float::INFINITY, Float::MAX, Float::MIN, 0.0.next_float, 0.0.prev_float] + @@special_floats = [Float::NAN, Float::INFINITY, -Float::INFINITY, Float::MAX, Float::MIN, 0.0.next_float, 0.0.prev_float] ## # Generates floating-point numbers # Will generate NaN, Infinity, -Infinity, # as well as Float::EPSILON, Float::MAX, Float::MIN, # 0.0.next_float, 0.0.prev_float, @@ -157,11 +157,11 @@ # # Shrinks to smaller, real floats. # >> Generators.float().sample(10, size: 10, rng: Random.new(42)) # => [4.0, 9.555555555555555, 0.0, -Float::INFINITY, 5.5, -5.818181818181818, 1.1428571428571428, 0.0, 8.0, 7.857142857142858] def float - frequency(99 => real_float, 1 => one_of(*@special_floats.map(&method(:constant)))) + frequency(99 => real_float, 1 => one_of(*@@special_floats.map(&method(:constant)))) end ## # Picks one of the given generators in `choices` at random uniformly every time. # @@ -239,10 +239,16 @@ # Accepted keyword arguments: # # `empty:` When false, behaves the same as `min: 1` # `min:` Ensures at least this many elements are generated. (default: 0) # `max:` Ensures at most this many elements are generated. When nil, an arbitrary count is used instead. (default: nil) + # `uniq:` When `true`, ensures that all elements in the array are unique. + # When given a proc, uses the result of this proc to check for uniqueness. + # (matching the behaviour of `Array#uniq`) + # If it is not possible to generate another unique value after the configured `max_consecutive_attempts` + # an `PropCheck::Errors::GeneratorExhaustedError` will be raised. + # (default: `false`) # # # >> Generators.array(Generators.positive_integer).sample(5, size: 1, rng: Random.new(42)) # => [[2], [2], [2], [1], [2]] # >> Generators.array(Generators.positive_integer).sample(5, size: 10, rng: Random.new(42)) @@ -250,29 +256,72 @@ # # >> Generators.array(Generators.positive_integer, empty: true).sample(5, size: 1, rng: Random.new(1)) # => [[], [2], [], [], [2]] # >> Generators.array(Generators.positive_integer, empty: false).sample(5, size: 1, rng: Random.new(1)) # => [[2], [1], [2], [1], [1]] + # + # >> Generators.array(Generators.boolean, uniq: true).sample(5, rng: Random.new(1)) + # => [[true, false], [false, true], [true, false], [false, true], [false, true]] - - def array(element_generator, min: 0, max: nil, empty: true) + def array(element_generator, min: 0, max: nil, empty: true, uniq: false) min = 1 if min.zero? && !empty + uniq = proc { |x| x } if uniq == true - res = proc do |count| - count = min + 1 if count < min - count += 1 if count == min && min != 0 - generators = (min...count).map do - element_generator.clone - end + if max.nil? + nonnegative_integer.bind { |count| make_array(element_generator, min, count, uniq) } + else + make_array(element_generator, min, max, uniq) + end + end - tuple(*generators) + private def make_array(element_generator, min, count, uniq) + amount = min if count < min + amount = min if count == min && min != 0 + amount ||= (count - min) + + # Simple, optimized implementation: + return make_array_simple(element_generator, amount) unless uniq + + # More complex implementation that filters duplicates + make_array_uniq(element_generator, min, amount, uniq) + end + + private def make_array_simple(element_generator, amount) + generators = amount.times.map do + element_generator.clone end - if max.nil? - nonnegative_integer.bind(&res) - else - res.call(max) + tuple(*generators) + end + + private def make_array_uniq(element_generator, min, amount, uniq_fun) + Generator.new do |**kwargs| + arr = [] + uniques = Set.new + count = 0 + (0..).lazy.map do + elem = element_generator.clone.generate(**kwargs) + if uniques.add?(uniq_fun.call(elem.root)) + arr.push(elem) + count = 0 + else + count += 1 + end + + if count > kwargs[:max_consecutive_attempts] + if arr.size >= min + # Give up and return shorter array in this case + amount = min + else + raise Errors::GeneratorExhaustedError, "Too many consecutive elements filtered by 'uniq:'." + end + end + end + .take_while { arr.size < amount } + .force + + LazyTree.zip(arr).map { |array| array.uniq(&uniq_fun) } end end ## # Generates a hash of key->values, @@ -298,63 +347,67 @@ def hash_of(key_generator, value_generator, **kwargs) array(tuple(key_generator, value_generator), **kwargs) .map(&:to_h) end - @alphanumeric_chars = [('a'..'z'), ('A'..'Z'), ('0'..'9')].flat_map(&:to_a).freeze + @@alphanumeric_chars = [('a'..'z'), ('A'..'Z'), ('0'..'9')].flat_map(&:to_a).freeze ## # Generates a single-character string # containing one of a..z, A..Z, 0..9 # # Shrinks towards lowercase 'a'. # # >> Generators.alphanumeric_char.sample(5, size: 10, rng: Random.new(42)) # => ["M", "Z", "C", "o", "Q"] def alphanumeric_char - one_of(*@alphanumeric_chars.map(&method(:constant))) + one_of(*@@alphanumeric_chars.map(&method(:constant))) end ## # Generates a string # containing only the characters a..z, A..Z, 0..9 # # Shrinks towards fewer characters, and towards lowercase 'a'. # # >> Generators.alphanumeric_string.sample(5, size: 10, rng: Random.new(42)) # => ["ZCoQ", "8uM", "wkkx0JNx", "v0bxRDLb", "Gl5v8RyWA6"] + # + # Accepts the same options as `array` def alphanumeric_string(**kwargs) array(alphanumeric_char, **kwargs).map(&:join) end - @printable_ascii_chars = (' '..'~').to_a.freeze + @@printable_ascii_chars = (' '..'~').to_a.freeze ## # Generates a single-character string # from the printable ASCII character set. # # Shrinks towards ' '. # # >> Generators.printable_ascii_char.sample(size: 10, rng: Random.new(42)) # => ["S", "|", ".", "g", "\\", "4", "r", "v", "j", "j"] def printable_ascii_char - one_of(*@printable_ascii_chars.map(&method(:constant))) + one_of(*@@printable_ascii_chars.map(&method(:constant))) end ## # Generates strings # from the printable ASCII character set. # # Shrinks towards fewer characters, and towards ' '. # # >> Generators.printable_ascii_string.sample(5, size: 10, rng: Random.new(42)) # => ["S|.g", "rvjjw7\"5T!", "=", "!_[4@", "Y"] + # + # Accepts the same options as `array` def printable_ascii_string(**kwargs) array(printable_ascii_char, **kwargs).map(&:join) end - @ascii_chars = [ - @printable_ascii_chars, + @@ascii_chars = [ + @@printable_ascii_chars, [ "\n", "\r", "\t", "\v", @@ -373,27 +426,29 @@ # Shrinks towards '\n'. # # >> Generators.ascii_char.sample(size: 10, rng: Random.new(42)) # => ["d", "S", "|", ".", "g", "\\", "4", "d", "r", "v"] def ascii_char - one_of(*@ascii_chars.map(&method(:constant))) + one_of(*@@ascii_chars.map(&method(:constant))) end ## # Generates strings # from the printable ASCII character set. # # Shrinks towards fewer characters, and towards '\n'. # # >> Generators.ascii_string.sample(5, size: 10, rng: Random.new(42)) # => ["S|.g", "drvjjw\b\a7\"", "!w=E!_[4@k", "x", "zZI{[o"] + # + # Accepts the same options as `array` def ascii_string(**kwargs) array(ascii_char, **kwargs).map(&:join) end - @printable_chars = [ - @ascii_chars, + @@printable_chars = [ + @@ascii_chars, "\u{A0}".."\u{D7FF}", "\u{E000}".."\u{FFFD}", "\u{10000}".."\u{10FFFF}" ].flat_map(&:to_a).freeze @@ -404,21 +459,23 @@ # Shrinks towards characters with lower codepoints, e.g. ASCII # # >> Generators.printable_char.sample(size: 10, rng: Random.new(42)) # => ["吏", "", "", "", "", "", "", "", "", "Ȍ"] def printable_char - one_of(*@printable_chars.map(&method(:constant))) + one_of(*@@printable_chars.map(&method(:constant))) end ## # Generates a printable string # both ASCII characters and Unicode. # # Shrinks towards shorter strings, and towards characters with lower codepoints, e.g. ASCII # # >> Generators.printable_string.sample(5, size: 10, rng: Random.new(42)) # => ["", "Ȍ", "𐁂", "Ȕ", ""] + # + # Accepts the same options as `array` def printable_string(**kwargs) array(printable_char, **kwargs).map(&:join) end ## @@ -441,9 +498,11 @@ # # Shrinks towards characters with lower codepoints, e.g. ASCII # # >> Generators.string.sample(5, size: 10, rng: Random.new(42)) # => ["\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}"] + # + # Accepts the same options as `array` def string(**kwargs) array(char, **kwargs).map(&:join) end ##