lib/prop_check/generators.rb in prop_check-0.6.1 vs lib/prop_check/generators.rb in prop_check-0.6.2
- old
+ new
@@ -1,5 +1,8 @@
+# coding: utf-8
+# frozen_string_literal: true
+
require 'prop_check/generator'
require 'prop_check/lazy_tree'
module PropCheck
##
# Contains common generators.
@@ -114,32 +117,45 @@
private def fraction(num_a, num_b, num_c)
num_a.to_f + num_b.to_f / (num_c.to_f.abs + 1.0)
end
##
- # Generates floating point numbers
+ # Generates floating-point numbers
# These start small (around 0)
# and become more extreme (large positive and large negative numbers)
#
+ # Will only generate 'reals',
+ # that is: no infinity, no NaN,
+ # no numbers testing the limits of floating-point arithmetic.
#
# Shrinks to numbers closer to zero.
#
- # TODO testing for NaN, Infinity?
- def float
- # integer.bind do |a|
- # integer.bind do |b|
- # integer.bind do |c|
- # Generator.wrap(fraction(a, b, c))
- # end
- # end
- # end
+ # >> Generators.real_float().sample(10, size: 10, rng: Random.new(42))
+ # => [-2.2, -0.2727272727272727, 4.0, 1.25, -3.7272727272727275, -8.833333333333334, -8.090909090909092, 1.1428571428571428, 0.0, 8.0]
+ def real_float
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]
##
+ # 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,
+ # to test the handling of floating-point edge cases.
+ # Approx. 1/100 generated numbers is a special one.
+ #
+ # 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))))
+ end
+
+ ##
# Picks one of the given generators in `choices` at random uniformly every time.
#
# Shrinks to values earlier in the list of `choices`.
#
# >> Generators.one_of(Generators.constant(true), Generators.constant(false)).sample(5, size: 10, rng: Random.new(42))
@@ -154,10 +170,13 @@
# Picks one of the choices given in `frequencies` at random every time.
# `frequencies` expects keys to be numbers
# (representing the relative frequency of this generator)
# and values to be generators.
#
+ # Side note: If you want to use the same frequency number for multiple generators,
+ # Ruby syntax requires you to send an array of two-element arrays instead of a hash.
+ #
# Shrinks to arbitrary elements (since hashes are not ordered).
#
# >> Generators.frequency(5 => Generators.integer, 1 => Generators.printable_ascii_char).sample(size: 10, rng: Random.new(42))
# => [4, -3, 10, 8, 0, -7, 10, 1, "E", 10]
def frequency(frequencies)
@@ -172,11 +191,11 @@
# Generates an array containing always exactly one value from each of the passed generators,
# in the same order as specified:
#
# Shrinks element generators, one at a time (trying last one first).
#
- # >> Generators.tuple(Generators.integer, Generators.float).call(10, Random.new(42))
+ # >> Generators.tuple(Generators.integer, Generators.real_float).call(10, Random.new(42))
# => [-4, 13.0]
def tuple(*generators)
Generator.new do |size, rng|
LazyTree.zip(generators.map do |generator|
generator.generate(size, rng)
@@ -189,11 +208,11 @@
# creates a generator that returns hashes
# with the same keys, and their corresponding values from their corresponding generators.
#
# Shrinks element generators.
#
- # >> Generators.fixed_hash(a: Generators.integer(), b: Generators.float(), c: Generators.integer()).call(10, Random.new(42))
+ # >> Generators.fixed_hash(a: Generators.integer(), b: Generators.real_float(), c: Generators.integer()).call(10, Random.new(42))
# => {:a=>-4, :b=>13.0, :c=>-3}
def fixed_hash(hash)
keypair_generators =
hash.map do |key, generator|
generator.map { |val| [key, val] }
@@ -240,19 +259,25 @@
##
# 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)))
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"]
def alphanumeric_string
array(alphanumeric_char).map(&:join)
end
@printable_ascii_chars = (' '..'~').to_a.freeze
@@ -270,11 +295,15 @@
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"]
def printable_ascii_string
array(printable_ascii_char).map(&:join)
end
@ascii_chars = [
@@ -293,19 +322,27 @@
].flat_map(&:to_a).freeze
##
# Generates a single-character string
# from the printable ASCII character set.
+ #
# 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)))
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"]
def ascii_string
array(ascii_char).map(&:join)
end
@printable_chars = [
@@ -319,30 +356,36 @@
# Generates a single-character printable string
# both ASCII characters and Unicode.
#
# 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)))
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))
+ # => ["", "Ȍ", "𐁂", "Ȕ", ""]
def printable_string
array(printable_char).map(&:join)
end
##
# Generates a single unicode character
# (both printable and non-printable).
#
# Shrinks towards characters with lower codepoints, e.g. ASCII
#
+ # >> Generators.printable_char.sample(size: 10, rng: Random.new(42))
+ # => ["吏", "", "", "", "", "", "", "", "", "Ȍ"]
def char
choose(0..0x10FFFF).map do |num|
[num].pack('U')
end
end
@@ -351,45 +394,71 @@
# Generates a string of unicode characters
# (which might contain both printable and non-printable characters).
#
# 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}"]
def string
array(char).map(&:join)
end
##
# Generates either `true` or `false`
#
# Shrinks towards `false`
- #
+ #
+ # >> Generators.boolean.sample(5, size: 10, rng: Random.new(42))
+ # => [false, true, false, false, false]
def boolean
one_of(constant(false), constant(true))
end
##
# Generates always `nil`.
#
# Does not shrink.
+ #
+ # >> Generators.nil.sample(5, size: 10, rng: Random.new(42))
+ # => [nil, nil, nil, nil, nil]
def nil
constant(nil)
end
##
# Generates `nil` or `false`.
#
# Shrinks towards `nil`.
#
+ # >> Generators.falsey.sample(5, size: 10, rng: Random.new(42))
+ # => [nil, false, nil, nil, nil]
def falsey
one_of(constant(nil), constant(false))
end
##
+ # Generates symbols consisting of lowercase letters and potentially underscores.
+ #
+ # Shrinks towards shorter symbols and the letter 'a'.
+ #
+ # >> Generators.simple_symbol.sample(5, size: 10, rng: Random.new(42))
+ # => [:tokh, :gzswkkxudh, :vubxlfbu, :lzvlyq__jp, :oslw]
+ def simple_symbol
+ alphabet = ('a'..'z').to_a
+ alphabet << '_'
+ array(one_of(*alphabet.map(&method(:constant))))
+ .map(&:join)
+ .map(&:to_sym)
+ end
+
+ ##
# Generates common terms that are not `nil` or `false`.
#
# Shrinks towards simpler terms, like `true`, an empty array, a single character or an integer.
#
+ # >> Generators.truthy.sample(5, size: 10, rng: Random.new(42))
+ # => [[4, 0, -3, 10, -4, 8, 0, 0, 10], -3, [5.5, -5.818181818181818, 1.1428571428571428, 0.0, 8.0, 7.857142857142858, -0.6666666666666665, 5.25], [], ["\u{9E553}\u{DD56E}\u{A5BBB}\u{8BDAB}\u{3E9FC}\u{C4307}\u{DAFAE}\u{1A022}\u{938CD}\u{70631}", "\u{C4C01}\u{32D85}\u{425DC}"]]
def truthy
one_of(constant(true),
constant([]),
char,
integer,
@@ -397,18 +466,21 @@
string,
array(integer),
array(float),
array(char),
array(string),
- hash(symbol, integer),
+ hash(simple_symbol, integer),
hash(string, integer),
hash(string, string)
)
end
##
# Generates whatever `other_generator` generates
# but sometimes instead `nil`.`
+ #
+ # >> Generators.nillable(Generators.integer).sample(20, size: 10, rng: Random.new(42))
+ # => [9, 10, 8, 0, 10, -3, -8, 10, 1, -9, -10, nil, 1, 6, nil, 1, 9, -8, 8, 10]
def nillable(other_generator)
frequency(9 => other_generator, 1 => constant(nil))
end
end
end