StreamData v0.2.0 StreamData View Source
Functions to create and combine generators.
A generator is a StreamData
struct. Generators can be created through the
functions exposed in this module, like constant/1
, and by combining other
generators through functions like bind/2
.
Generators are used for mainly two purposes: to generate test data, and as the basis of property testing. When a generator is called internally, it will generate a value that is not a directly usable value. Instead, it’s a “wrapper” around a normal value that is optimized for property testing and, more specifically, for shrinking (see the section about shrinking below).
Note that values generated by generators are not unique.
Generation size
Generators have access to a generation parameter called the generation
size, which is a non-negative integer. This parameter is meant to bind the
data generated by each generator in a way that is completely up to the
generator. For example, a generator that generates integer can use the size
parameter to generate integers inside the -size..size
range. In a similar
way, a generator that generates lists could use this parameter to generate a
list with 0
to size
elements. The generation size parameter is “global”,
that is, complex generators that wrap other generators usually pass it down to
the inner generators when generating data.
When creating generators, they can access the generation size using the
sized/1
function. Generators can be resized to a fixed generation size using
resize/2
.
Enumeration
Generators implement the Enumerable
protocol. The implementation yields
simple values (not shrinkable wrappers as discussed above) when enumerating a
generator. The enumeration starts with a small generation size, which
increases when the enumeration continues (up to a fixed maximum size).
For example, to get an infinite stream of integers that starts with small
integers and progressively grows the boundaries, you can just use integer/0
:
Enum.take(StreamData.integer(), 10)
#=> [-1, 0, -3, 4, -4, 5, -1, -3, 5, 8]
Since generators are proper streams, functions from the Stream
module can be
used to stream values out of them. For example, to build an infinite stream of
positive even integers, you can do:
StreamData.integer()
|> Stream.filter(&(&1 > 0))
|> Stream.map(&(&1 * 2))
|> Enum.take(10)
#=> [4, 6, 4, 10, 14, 16, 4, 16, 36, 16]
As we mentioned above, you can see that in general a generator does not generate unique values.
Note that all generators are infinite streams that never terminate.
When using generators for test data, usually using them as enumerables is the easiest choice.
Shrinking
As we mentioned above, the wrapper values generated by generators internally
are optimized for shrinking. These wrapper values contain a generated
value (the one used when enumerating a generator) alongside a way to shrink
such value. For example, a value generated by the list_of/1
generator will
contain the actual list to use alongside a way to shrink that list, which in
this specific case means taking elements out of that list until it comes to
the empty list (which is the “smallest” shrinkable value for lists). Each
generator shrinks generated values with its own logic, which is documented in
the generator’s documentation.
Shrinking is used for property testing, where it’s important to find the
minimal value for which a property fails (since generated values are often
bigger and full of garbage). See PropertyTest
for more information.
Note that the generation size is not related in any way with shrinking: while intuitively one may think that shrinking just means decreasing the generation size, in reality the generation size is related to how generators generate values, while shrinking is bound to each generated value and is a way to shrink that particular value.
Special generators
Some Elixir types are implicitly converted to StreamData
generators when
used in property testing or composed. These types are:
atoms - they generated themselves. For example,
:foo
is equivalent toStreamData.constant(:foo)
.tuples of generators - they generated tuples where each value is a value generated by the corresponding generator, exactly like described in
tuple/1
. For example,{StreamData.integer(), StreamData.boolean()}
generates entries like{10, false}
.
Note that these terms are only implicitly converted to generators when
composing them. This means that these terms are not full-fledged generators:
for example, atoms cannot be enumerated directly as they don’t implement the
Enumerable
protocol. However, StreamData.map(:foo, &Atom.to_string/1)
can
be enumerated since :foo
is implicitly converted to a generator when passed
to a StreamData
function.
Link to this section Summary
Types
An opaque type that represents a StreamData
generator that generates values
of type a
Functions
Generates binaries
Binds each element generated by data
to a new generator returned by applying fun
Binds each element generated by data
and to a new generator returned by
applying fun
or filters the generated element
Generates bitstrings
Generates boolean values
Generates bytes
Checks the behaviour of a given function on values generated by data
A generator that always generates the given term
Filters the given generator data
according to the given predicate
function
Generates a list of fixed length where each element is generated from the
corresponding generator in data
Generates maps with fixed keys and generated values
Generates values from different generators with specified probability
Syntactic sugar to create generators
Generates integers bound by the generation size
Generates an integer in the given range
Generates iodata
Generates iolists
Generates keyword lists where values are generated by value_data
Generates lists where each values is generated by the given data
Maps the given function fun
over the given generator data
Generates maps with keys from key_data
and values from value_data
Generates lists of elements out of first
with a chance of them being
improper with the improper ending taken out of improper
Generates elements taken randomly out of enum
Constrains the given enum_data
to be non-empty
Generates non-empty improper lists where elements of the list are generated
out of first
and the improper ending out of improper
Generates values out of one of the given datas
Resize the given generated data
to have fixed generation size new_size
Scales the generation size of the given generator data
according to
size_changer
Returns the generator returned by calling fun
with the generation size
Generates a string of the given kind or from the given characters
Generates trees of values generated by leaf_data
Generates tuples where each element is taken out of the corresponding
generator in the tuple_datas
tuple
Generates uniformly distributed floats in the interval 0..1
Generates a list of elements generated by data
without duplicates (possibly
according to a given uniqueness function)
Generates atoms that don’t need to be quoted when written as literals
Makes the values generated by data
not shrink
Link to this section Types
An opaque type that represents a StreamData
generator that generates values
of type a
.
Note that while this type is opaque, a generator is still guaranteed to be a
StreamData
struct (in case you want to pattern match on it).
Link to this section Functions
Generates binaries.
The length of the generated binaries is limited by the generation size.
Options
:length
- (non-negative integer) sets the exact length of the generated binaries (same as inlist_of/3
).:min_length
- (non-negative integer) sets the minimum length of the generated binaries (same as inlist_of/3
). Ignored if:length
is present.:max_length
- (non-negative integer) sets the maximum length of the generated binaries (same as inlist_of/3
). Ignored if:length
is present.
Examples
Enum.take(StreamData.binary(), 3)
#=> [<<1>>, "", "@Q"]
Shrinking
Values generated by this generator shrink by becoming smaller binaries and by
having individual bytes that shrink towards 0
.
Binds each element generated by data
to a new generator returned by applying fun
.
This function is the basic mechanism for composing generators. It takes a
generator data
and invokes fun
with each element in data
. fun
must
return a new generator that is effectively used to generate items from
now on.
Examples
Say we wanted to create a generator that returns two-element tuples where the first element is a list, and the second element is a random element from that list. To do that, we can first generate a list and then bind a function to that list; this function will return the list and a random element from it.
StreamData.bind(StreamData.list_of(StreamData.integer()), fn list ->
StreamData.bind(StreamData.member_of(list), fn elem ->
StreamData.constant({list, elem})
end)
end)
Shrinking
The generator returned by bind/2
shrinks by first shrinking the value
generated by the inner generator and then by shrinking the outer generator
given as data
. When data
shrinks, fun
is once more applied on the
shrunk value value and returns a whole new generator, which will most likely
emit new items.
Binds each element generated by data
and to a new generator returned by
applying fun
or filters the generated element.
Works similarly to bind/2
but allows to filter out unwanted values. It takes
a generator data
and invokes fun
with each element generated by data
.
fun
must return one of:
{:cont, generator}
-generator
is then used to generate the next element:skip
- the value generated bydata
is filtered out and a new element is generated
Since this function acts as a filter as well, it behaves similarly to
filter/3
: when more than max_consecutive_failures
elements are filtered
out (that is, fun
returns :skip
), a StreamData.FilterTooNarrowError
is
raised.
Examples
Say we wanted to create a generator that generates two-element tuples where the first element is a list of integers with an even number of members and the second element is a member of that list. We can do that by generating a list and, if it has even length, taking an element out of it, otherwise filtering it out.
require Integer
list_data = StreamData.nonempty(StreamData.list_of(StreamData.integer()))
data =
StreamData.bind_filter(list_data, fn
list when Integer.is_even(length(list)) ->
inner_data = StreamData.bind(StreamData.member_of(list), fn member ->
StreamData.constant({list, member})
end)
{:cont, inner_data}
_odd_list ->
:skip
end)
Enum.at(data, 0)
#=> {[-6, -7, -4, 5, -9, 8, 7, -9], 5}
Shrinking
This generator shrinks like bind/2
but values that are skipped are not used
for shrinking (similarly to how filter/3
works).
Generates bitstrings.
The length of the generated bitstring is limited by the generation size.
Options
:length
- (non-negative integer) sets the exact length of the generated bitstrings (same as inlist_of/3
).:min_length
- (non-negative integer) sets the minimum length of the generated bitstrings (same as inlist_of/3
). Ignored if:length
is present.:max_length
- (non-negative integer) sets the maximum length of the generated bitstrings (same as inlist_of/3
). Ignored if:length
is present.
Examples
Enum.take(StreamData.bitstring(), 3)
#=> [<<0::size(1)>>, <<2::size(2)>>, <<5::size(3)>>]
Shrinking
Values generated by this generator shrink by becoming smaller bitstrings and
by having the individual bits go towards 0
.
Generates boolean values.
Examples
Enum.take(StreamData.boolean(), 3)
#=> [true, true, false]
Shrinking
Shrinks towards false
.
Generates bytes.
A byte is an integer between 0
and 255
.
Examples
Enum.take(StreamData.byte(), 3)
#=> [102, 161, 13]
Shrinking
Values generated by this generator shrink like integers, so towards bytes
closer to 0
.
Checks the behaviour of a given function on values generated by data
.
This function takes a generator and a function fun
and verifies that that
function “holds” for all generated data. fun
is called with each generated
value and can return one of:
{:ok, term} - means that the function “holds” for the given value.
term
can be anything and will be used for internal purposes byStreamData
.{:error, term}
- means that the function doesn’t hold for the given value.term
is the term that will be shrunk to find the minimal value for whichfun
doesn’t hold. See below for more information on shrinking.
When a value is found for which fun
doesn’t hold (returns {:error, term}
),
check_all/3
tries to shrink that value in order to find a minimal value that
still doesn’t satisfy fun
.
The return value of this function is one of:
{:ok, map}
- if all generated values satisfyfun
.map
is a map of metadata that contains no keys for now.{:error, map}
- if a generated value doesn’t satisfyfun
.map
is a map of metadata that contains the following keys::original_failure
- iffun
returned{:error, term}
for a generated value, this key in the map will beterm
.:shrunk_failure
- the value returned in{:error, term}
byfun
when invoked with the smallest failing value that was generated.:nodes_visited
- the number of nodes (a positive integer) visited in the shrinking tree in order to find the smallest value. See also the:max_shrinking_steps
option.
Options
This function takes the following options:
:initial_seed
- three-element tuple with three integers that is used as the initial random seed that drives the random generation. This option is required.:initial_size
- (non-negative integer) the initial generation size used to start generating values. The generation size is then incremented by1
on each iteration. See the “Generation size” section of the module documentation for more information on generation size. Defaults to1
.:max_runs
- (non-negative integer) the total number of elements to generate out ofdata
and check throughfun
. Defaults to100
.:max_shrinking_steps
- (non-negative integer) the maximum numbers of shrinking steps to perform in casecheck_all/3
finds an element that doesn’t satisfyfun
. Defaults to100
.
Examples
Let’s try out a contrived example: we want to verify that the integer/0
generator generates integers that are not 0
or multiples of 11
. This
verification is broken by design because integer/0
is likely to generate
multiples of 11
at some point, but it will show the capabilities of
check_all/3
. For the sake of the example, let’s say we want the values that
fail to be represented as strings instead of the original integers that
failed. We can implement what we described like this:
options = [initial_seed: :os.timestamp()]
{:error, metadata} = StreamData.check_all(StreamData.integer(), options, fn int ->
if int == 0 or rem(int, 11) != 0 do
{:ok, nil}
else
{:error, Integer.to_string(int)}
end
end)
metadata.nodes_visited
#=> 7
metadata.original_failure
#=> 22
metadata.shrunk_failure
#=> 11
As we can see, the function we passed to check_all/3
“failed” for int =
22
, and check_all/3
was able to shrink this value to the smallest failing
value, which in this case is 11
.
A generator that always generates the given term.
Examples
iex> Enum.take(StreamData.constant(:some_term), 3)
[:some_term, :some_term, :some_term]
Shrinking
This generator doesn’t shrink.
Filters the given generator data
according to the given predicate
function.
Only elements generated by data
that pass the filter are kept in the
resulting generator.
If the filter is too strict, it can happen that too few values generated by
data
satisfy it. In case more than max_consecutive_failures
consecutive
values don’t satisfy the filter, a StreamData.FilterTooNarrowError
will be
raised. Try to make sure that your filter takes out only a small subset of the
elements generated by data
. When possible, a good way to go around the
limitations of filter/3
is to instead transform the generated values in the
shape you want them instead of filtering out the ones you don’t want.
For example, a generator of odd numbers could be implemented as:
require Integer
odd_ints = StreamData.filter(StreamData.integer(), &Integer.is_odd/1)
Enum.take(odd_ints, 3)
#=> [1, 1, 3]
However, it will do more work and take more time to generate odd integers because it will have to filter out all the even ones that it generates. In this case, a better approach would be to generate integers and make sure they are odd:
odd_ints = StreamData.map(StreamData.integer(), &(&1 * 2 + 1))
Enum.take(odd_ints, 3)
#=> [1, 1, 3]
Shrinking
All the values that each generated value shrinks to satisfy predicate
as
well.
Generates a list of fixed length where each element is generated from the
corresponding generator in data
.
Examples
data = StreamData.fixed_list([StreamData.integer(), StreamData.binary()])
Enum.take(data, 3)
#=> [[1, <<164>>], [2, ".T"], [1, ""]]
Shrinking
Shrinks by shrinking each element in the generated list according to the corresponding generator. Shrunk lists never lose elements.
Generates maps with fixed keys and generated values.
data_map
is a map of fixed_key => data
pairs. Maps generated by this
generator will have the same keys as data_map
and values corresponding to
values generated by the generator under those keys.
Examples
data = StreamData.fixed_map(%{
integer: StreamData.integer(),
binary: StreamData.binary(),
})
Enum.take(data, 3)
#=> [%{binary: "", int: 1}, %{binary: "", int: -2}, %{binary: "R1^", int: -3}]
Shrinking
This generator shrinks by shrinking the values of the generated map.
Generates values from different generators with specified probability.
frequencies
is a list of {frequency, data}
where frequency
is an integer
and data
is a generator. The resulting generator will generate data from one
of the generators in frequency
, with probability frequency / vsum_of_frequencies
.
Examples
Let’s build a generator that returns a binary around 25% of times and a
integer around 75% of times. We’ll use integer/0
first so that generated values
will shrink towards integers.
ints_and_some_bins = StreamData.frequency([
{3, StreamData.integer()},
{1, StreamData.binary()},
])
Enum.take(ints_and_some_bins, 3)
#=> ["", -2, -1]
Shrinking
Each generated value is shrunk, and then this generator shrinks towards
values generated by generators earlier in the list of frequencies
.
Syntactic sugar to create generators.
This macro provides ad hoc syntax to write complex generators. Let’s see a
quick example to get a feel of how it works. Say we have a User
struct:
defmodule User do
defstruct [:name, :email]
end
We can create a generator of users like this:
email_generator = map({binary(), binary()}, fn left, right -> left <> "@" <> right end)
gen all name <- binary(),
email <- email_generator do
%User{name: name, email: email}
end
Everything between gen all
and do
is referred to as clauses. Clauses
are used to specify the values to generate to be used in the body. The newly
created generator will generated values that are the return value of the
do
body for the generated values from the clauses.
Clauses
As seen in the example above, clauses can be of three types:
value generation - they have the form
pattern <- generator
wheregenerator
must be a generator. These clauses take a value out ofgenerator
on each run and match it againstpattern
. Variables bound inpattern
can be then used throughout subsequent clauses and in thedo
body.binding - they have the form
pattern = expression
. They are exactly like assignment through the=
operator: ifpattern
doesn’t matchexpression
, an error is raised. They can be used to bind values for use in subsequent clauses and in thedo
body.filtering - they have the form
expression
. If a filtering clause returns a truthy value, then the set of generated values that appear before the filtering clause is considered valid and generation continues. If the filtering clause returns a falsey value, then the current value is considered invalid and a new value is generated. Note that filtering clauses should not filter out too many times; in case they do, aStreamData.FilterTooNarrowError
error is raised (same asfilter/3
).
Body
The return value of the body passed in the do
block is what is ultimately
generated by the generator return by this macro.
Shrinking
See the module documentation for more information on shrinking. Clauses affect shrinking in the following way:
Generates integers bound by the generation size.
Examples
Enum.take(StreamData.integer(), 3)
#=> [1, -1, -3]
Shrinking
Generated values shrink towards 0
.
Generates an integer in the given range
.
The generation size is ignored since the integer always lies inside range
.
Examples
Enum.take(StreamData.integer(4..8), 3)
#=> [6, 7, 7]
Shrinking
Shrinks towards with the smallest absolute value that still lie in range
.
Generates iodata.
Iodata are values of the t:iodata/0
type.
Examples
Enum.take(StreamData.iodata(), 3)
#=> [[""], <<198>>, [115, 172]]
Shrinking
Shrinks towards less nested iodata and ultimately towards smaller binaries.
Generates iolists.
Iolists are values of the t:iolist/0
type.
Examples
Enum.take(StreamData.iolist(), 3)
#=> [[164 | ""], [225], ["" | ""]]
Shrinking
Shrinks towards smaller and less nested lists and towards bytes instead of binaries.
Generates keyword lists where values are generated by value_data
.
Keys are always atoms.
Examples
Enum.take(StreamData.keyword_of(StreamData.integer()), 3)
#=> [[], [sY: 1], [t: -1]]
Shrinking
This generator shrinks equivalently to a list of key-value tuples generated by
list_of/1
, that is, by shrinking the values in each tuple and also reducing
the size of the generated keyword list.
Generates lists where each values is generated by the given data
.
Each generated list can contain duplicate elements. The length of the
generated list is bound by the generation size. If the generation size is 0
,
the empty list will always be generated. Note that the accepted options
provide finer control over the size of the generated list. See the “Options”
section below.
Options
:length
- (integer or range) if an integer, the exact length the generated lists should be; if a range, the range in which the length of the generated lists should be. If provided,:min_length
and:max_length
are ignored.:min_length
- (integer) the minimum length of the generated lists.:max_length
- (integer) the maximum length of the generated lists.
Examples
Enum.take(StreamData.list_of(StreamData.binary()), 3)
#=> [[""], [], ["", "w"]
Enum.take(StreamData.list_of(StreamData.integer(), length: 3), 3)
#=> [[0, 0, -1], [2, -1, 1], [0, 3, -3]]
Enum.take(StreamData.list_of(StreamData.integer(), max_length: 1), 3)
#=> [[1], [], []]
Shrinking
This generator shrinks by taking elements out of the generated list and also
by shrinking the elements of the generated list. Shrinking still respects any
possible length-related option: for example, if :min_length
is provided, all
shrinked list will have more than :min_length
elements.
Maps the given function fun
over the given generator data
.
Returns a new generator that returns elements from data
after applying fun
to them.
Examples
iex> data = StreamData.map(StreamData.integer(), &Integer.to_string/1)
iex> Enum.take(data, 3)
["1", "0", "3"]
Shrinking
This generator shrinks exactly like data
, but with fun
mapped over the
shrunk data.
Generates maps with keys from key_data
and values from value_data
.
Since maps require keys to be unique, this generator behaves similarly to
uniq_list_of/3
: if more than max_tries
duplicate keys are generated
consequently, it raises a StreamData.TooManyDuplicatesError
exception.
Examples
Enum.take(StreamData.map_of(StreamData.integer(), StreamData.boolean()), 3)
#=> [%{}, %{1 => false}, %{-2 => true, -1 => false}]
Shrinking
Shrinks towards smallest maps and towards shrinking keys and values according to the respective generators.
Generates lists of elements out of first
with a chance of them being
improper with the improper ending taken out of improper
.
Behaves similarly to nonempty_improper_list_of/2
but can generate empty
lists and proper lists as well.
Examples
data = StreamData.maybe_improper_list_of(StreamData.byte(), StreamData.binary())
Enum.take(data, 3)
#=> [[60 | "."], [], [<<212>>]]
Shrinking
Shrinks towards smaller lists and shrunk elements in those lists, and ultimately towards proper lists.
Generates elements taken randomly out of enum
.
enum
must be a non-empty and finite enumerable. If given an empty
enumerable, this function raises an error. If given an infinite enumerable,
this function will not terminate.
Examples
Enum.take(StreamData.member_of([:ok, 4, "hello"]), 3)
#=> [4, 4, "hello"]
Shrinking
This generator shrinks towards elements that appear earlier in enum
.
Constrains the given enum_data
to be non-empty.
enum_data
must be a generator that emits enumerables, such as lists
and maps. nonempty/1
will filter out enumerables that are empty
(Enum.empty?/1
returns true
).
Examples
Enum.take(StreamData.nonempty(StreamData.list_of(StreamData.integer())), 3)
#=> [[1], [-1, 0], [2, 1, -2]]
Generates non-empty improper lists where elements of the list are generated
out of first
and the improper ending out of improper
.
Examples
data = StreamData.nonempty_improper_list_of(StreamData.byte(), StreamData.binary())
Enum.take(data, 3)
#=> [["\f"], [56 | <<140, 137>>], [226 | "j"]]
Shrinking
Shrinks towards smaller lists (that are still non-empty, having the improper ending) and towards shrunk elements of the list and a shrunk improper ending.
Generates values out of one of the given datas
.
datas
must be a list of generators. The values generated by this generator
are values generated by generators in datas
, chosen each time at random.
Examples
data = StreamData.one_of([StreamData.integer(), StreamData.binary()])
Enum.take(data, 3)
#=> [-1, <<28>>, ""]
Shrinking
The generated value will be shrunk first according to the generator that
generated it, and then this generator will shrink towards earlier generators
in datas
.
Resize the given generated data
to have fixed generation size new_size
.
The new generator will ignore the generation size and always use new_size
.
See the “Generation size” section in the documentation for StreamData
for
more information about the generation size.
Examples
data = StreamData.resize(StreamData.integer(), 10)
Enum.take(data, 3)
#=> [4, -5, -9]
Scales the generation size of the given generator data
according to
size_changer
.
When generating data from data
, the generation size will be the result of
calling size_changer
with the generation size as its argument. This is
useful, for example, when a generator needs to grow faster or slower than
the default.
See the “Generation size” section in the documentation for StreamData
for
more information about the generation size.
Examples
Let’s create a generator that generates much smaller integers than integer/0
when size grows. We can do this by scaling the generation size to the
logarithm of the generation size.
data = StreamData.scale(StreamData.integer(), fn size ->
trunc(:math.log(size))
end)
Enum.take(data, 3)
#=> [0, 0, -1]
Another interesting example is creating a generator with a fixed maximum generation size. For example, say we want to generate binaries but we never want them to be larger than 64 bytes:
small_binaries = StreamData.scale(StreamData.binary(), fn size ->
min(size, 64)
end)
Returns the generator returned by calling fun
with the generation size.
fun
takes the generation size and has to return a generator, that can use
that size to its advantage.
See the “Generation size” section in the documentation for StreamData
for
more information about the generation size.
Examples
Let’s build a generator that generates integers in double the range integer/0
does:
data = StreamData.sized(fn size ->
StreamData.resize(StreamData.integer(), size * 2)
end)
Enum.take(data, 3)
#=> [0, -1, 5]
string(:ascii | :alphanumeric | Enumerable.t) :: t(String.t)
Generates a string of the given kind or from the given characters.
kind_or_chars
can be:
:ascii
- strings containing only ASCII characters are generated. Such strings shrink towards lower codepoints.:alphanumeric
- strings containing only alphanumeric characters (?a..?z
,?A..?Z
,?0..?9
) are generated. Such strings shrink towards?a
following the order shown previously.a list of characters - strings with only characters present in the given list are generated. Such strings shrink towards characters that appear earlier in the list.
Examples
Enum.take(StreamData.string(:ascii), 3)
#=> ["c", "9A", ""]
Enum.take(StreamData.string(Enum.concat([?a..?c, ?l..?o])), 3)
#=> ["c", "oa", "lb"]
Shrinking
Shrinks towards smaller strings and as described in the description of the
possible values of kind_or_chars
above.
Generates trees of values generated by leaf_data
.
subtree_fun
is a function that takes a generator and returns a generator
that “combines” that generator. This generator will pass leaf_data
to
subtree_fun
when it needs to go “one level deeper” in the tree. Note that
raw values from leaf_data
can sometimes be generated.
This is best explained with an example. Say that we want to generate binary
trees of integers, and that we represent binary trees as either an integer (a
leaf) a %Branch{}
struct:
defmodule Branch do
defstruct [:left, :right]
end
We can start off by creating a generator that generates branches given the
generator that generates the content of each node (integer/0
in our case):
defmodule MyTree do
def branch_data(child_data) do
StreamData.map({child_data, child_data}, fn {left, right} ->
%Branch{left: left, right: right}
end)
end
end
Now, we can generate trees by simply using branch_data
as the subtree_fun
,
and integer/0
as leaf_data
:
tree_data = StreamData.tree(StreamData.integer(), &MyTree.branch_data/1)
Enum.at(StreamData.resize(tree_data, 10), 0)
#=> %Branch{left: %Branch{left: 4, right: -1}, right: -2}
Examples
A common example is nested lists:
data = StreamData.tree(StreamData.integer(), &StreamData.list_of/1)
Enum.at(StreamData.resize(data, 10), 0)
#=> [[], '\t', '\a', [1, 2], -3, [-7, [10]]]
Shrinking
Shrinks values and shrinks towards less deep trees.
Generates tuples where each element is taken out of the corresponding
generator in the tuple_datas
tuple.
Examples
data = StreamData.tuple({StreamData.integer(), StreamData.binary()})
Enum.take(data, 3)
#=> [{-1, <<170>>}, {1, "<"}, {1, ""}]
Shrinking
Shrinks by shrinking each element in the generated tuple according to the corresponding generator.
Generates uniformly distributed floats in the interval 0..1
.
Note that if you want to have more complex float values, such as negative
values or bigger values, you can transform this generator. For example, to
have floats in the interval 0..10
, you can use map/2
:
StreamData.map(StreamData.uniform_float(), &(&1 * 10))
To have sometimes negative floats, you can for example use bind/2
:
StreamData.bind(StreamData.boolean(), fn negative? ->
if negative? do
StreamData.map(StreamData.uniform_float(), &(-&1))
else
StreamData.uniform_float()
end
end)
Examples
Enum.take(StreamData.uniform_float(), 3)
#=> [0.5122356680893687, 0.7387020706272481, 0.9613874981766901]
Shrinking
Values generated by this generator do not shrink.
Generates a list of elements generated by data
without duplicates (possibly
according to a given uniqueness function).
This generator will generate lists where each list is unique according to the
value returned by applying the given uniqueness function to each element
(similarly to how Enum.uniq_by/2
works). If more than the value of the
:max_tries
option consecutive elements are generated that are considered
duplicates according to the uniqueness function, a
StreamData.TooManyDuplicatesError
error is raised. For this reason, try to
make sure to not make the uniqueness function return values out of a small
value space. The uniqueness function and the max number of tries can be
customized via options.
Options
:uniq_fun
- (a function of arity one) a function that is called with each generated element and whose return value is used as the value to compare with other values for uniqueness (similarly toEnum.uniq_by/2
).:max_tries
- (non-negative integer) the maximum number of times that this generator tries to generate the next element of the list before giving up and raising aStreamData.TooManyDuplicatesError
in case it can’t find a unique element to generate. Note that the generation size often affects this: for example, if you have a generator likeuniq_list_of(integer(), min_length: 4)
and you start generating elements out of it with a generation size of1
, it will fail by definition becauseinteger/0
generates in-size..size
so it would only generate in a set ([-1, 0, 1]
) with three elements. Useresize/2
orscale/2
to manipulate the size (for example by setting a minimum generation size of3
) in such cases.:length
- (non-negative integer) same as inlist_of/3
.:min_length
- (non-negative integer) same as inlist_of/3
.:max_length
- (non-negative integer) same as inlist_of/3
.
Examples
data = StreamData.uniq_list_of(StreamData.integer())
Enum.take(data, 3)
#=> [[1], [], [2, 3, 1]]
Shrinking
This generator shrinks like list_of/1
, but the shrunk values are unique
according to the :uniq_fun
option as well.
Generates atoms that don’t need to be quoted when written as literals.
Examples
Enum.take(StreamData.unquoted_atom(), 3)
#=> [:xF, :y, :B_]
Shrinking
Shrinks towards smaller atoms in the ?a..?z
character set.
Makes the values generated by data
not shrink.
Examples
Let’s build a generator of bytes (integers in the 0..255
) range. We can
build this on top of integer/1
, but for our purposes, it doesn’t make sense for
a byte to shrink towards 0
:
byte = StreamData.unshrinkable(StreamData.integer(0..255))
Enum.take(byte, 3)
#=> [190, 181, 178]
Shrinking
The generator returned by unshrinkable/1
generates the same values as data
,
but such values will not shrink.