lib/prop_check/generators.rb in prop_check-0.16.0 vs lib/prop_check/generators.rb in prop_check-0.17.0
- old
+ new
@@ -870,7 +870,77 @@
klass.new(*vals, **kwvals)
end
end
end
end
+
+ ##
+ # Helper to build recursive generators
+ #
+ # Given a `leaf_generator`
+ # and a block which:
+ # - is given a generator that generates subtrees.
+ # - it should return the generator for intermediate tree nodes.
+ #
+ # This is best explained with an example.
+ # Say we want to generate a binary tree of integers.
+ #
+ # If we have a struct representing internal nodes:
+ # ```ruby
+ # Branch = Struct.new(:left, :right, keyword_init: true)
+ # ```
+ # we can generate trees like so:
+ # ```ruby
+ # Generators.tree(Generators.integer) do |subtree_gen|
+ # G.instance(Branch, left: subtree_gen, right: subtree_gen)
+ # end
+ # ```
+ #
+ # As another example, consider generating lists of integers:
+ #
+ # >> G = PropCheck::Generators
+ # >> G.tree(G.integer) {|child_gen| G.array(child_gen) }.sample(5, size: 37, rng: Random.new(42))
+ # => [[7, [2, 3], -10], [[-2], [-2, [3]], [[2, 3]]], [], [0, [-2, -3]], [[1], -19, [], [1, -1], [1], [-1, -1], [1]]]
+ #
+ # And finally, here is how one could create a simple generator for parsed JSON data:
+ #
+ # ```ruby`
+ # G = PropCheck::Generators
+ # def json
+ # G.tree(G.one_of(G.boolean, G.real_float, G.ascii_string)) do |json_gen|
+ # G.one_of(G.array(json_gen), G.hash_of(G.ascii_string, json_gen))
+ # end
+ # end
+ # ```
+ #
+ def tree(leaf_generator, &block)
+ # Implementation is based on
+ # https://hexdocs.pm/stream_data/StreamData.html#tree/2
+ Generator.new do |size:, rng:, **other_kwargs|
+ nodes_on_each_level = random_pseudofactors(size.pow(1.1).to_i, rng)
+ result = nodes_on_each_level.reduce(leaf_generator) do |subtree_generator, nodes_on_this_level|
+ frequency(1 => subtree_generator,
+ 2 => block.call(subtree_generator).resize { |_size| nodes_on_this_level })
+ end
+
+ result.generate(size: size, rng: rng, **other_kwargs)
+ end
+ end
+
+ private def random_pseudofactors(size, rng)
+ return [size].to_enum if size < 2
+
+ Enumerator.new do |yielder|
+ loop do
+ factor = rng.rand(1..(Math.log2(size).to_i))
+ if factor == 1
+ yielder << size
+ break
+ else
+ yielder << factor
+ size /= factor
+ end
+ end
+ end
+ end
end
end