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
##