StreamData v0.4.3 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.

Similar to the Stream module, the functions in this module return a lazy construct. We can get values out of a generator by enumerating the generator. Generators always generate an infinite stream of values (which are randomized most of the time).

For example, to get an infinite stream of integers that starts with small integers and progressively grows the boundaries, you can use integer/0:

Enum.take(StreamData.integer(), 10)
#=> [-1, 0, -3, 4, -4, 5, -1, -3, 5, 8]

As you can see above, values emitted by a generator are not unique.

In many applications of generators, the longer the generator runs the larger the generated values will be. For integers, a larger integer means a bigger number. For lists, it may mean a list with more elements. This is controlled by a parameter that we call the generation size (see the "Generation size" section below).

StreamData is often used to generate random values. It is also the foundation for property-based testing. See ExUnitProperties for more information.

Enumeration

Generators implement the Enumerable protocol. The enumeration starts with a small generation size, which increases when the enumeration continues (up to a fixed maximum size).

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]

Generators that are manipulated via the Stream and Enum modules are no longer shrinkable (see the section about shrinking below). If you want generation through the Enumerable protocol to be reproducible, see seeded/2.

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. During composition, it is common for the "parent generator" to pass the size to the composed generators.

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.

Shrinking

StreamData generators are also shrinkable. The idea behind shrinking is to find the simplest value that respects a certain condition. For example, during property-based tests, we use shrinking to find the integer closest to 0 or the smallest list that makes a test fail. By reporting the simplest data structure that triggers an error, the failure becomes easier to understand and reproduce.

Each generator has its own logic to shrink values. Those are outlined in each generator documentation.

Note that the generation size is not related in any way to shrinking: while intuitively one may think that shrinking just means decreasing the generation size, in reality the shrinking rule is bound to each generated value. One way to look at it is that shrinking a list is always the same, regardless of its generated sized.

Special generators

Some Elixir types are implicitly converted to StreamData generators when composed or used in property-based testing. These types are:

  • atoms - they generate themselves. For example, :foo is equivalent to StreamData.constant(:foo).

  • tuples of generators - they generate 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 must be explicitly converted to StreamData generators. 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.constant(:foo) is enumerable as it has been wrapped in 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 atoms of various kinds

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 floats according to the given options

Generates values from different generators with specified probability

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

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

Generates maps with fixed but optional keys and generated values

Generates positive integers bound by the generation size

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

Makes the given generator data always used the same given seed when generating

Returns the generator returned by calling fun with the generation size

Generates a string of the given kind or from the given characters

Generates any term

Generates trees of values generated by leaf_data and subtree_fun

Generates tuples where each element is taken out of the corresponding generator in the tuple_datas tuple

Generates a list of elements generated by data without duplicates (possibly according to a given uniqueness function)

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.

Link to this section Functions

Generates atoms of various kinds.

kind can be:

  • :alphanumeric - this generates alphanumeric atoms that don't need to be quoted when written as literals. For example, it will generate :foo but not :"foo bar".

  • :operator - this generates Elixir operators (such as :<> or :+). These don't need to be quoted when written as literals.

  • :alias - generates Elixir aliases like Foo or Foo.Bar.Baz.

These are some of the most common kinds of atoms usually used in Elixir applications. If you need completely arbitrary atoms, you can use a combination of map/2, String.to_atom/1, and string-focused generators to transform arbitrary strings into atoms:

printable_atom = StreamData.map(StreamData.string(:printable), &String.to_atom/1)

Examples

Enum.take(StreamData.atom(:alphanumeric), 3)
#=> [:xF, :y, :B_]

Shrinking

Shrinks towards smaller atoms and towards "simpler" letters (like towards only alphabet letters).

Link to this function

binary(options \\ []) View Source
binary(keyword()) :: t(binary())

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 in list_of/2).

  • :min_length - (non-negative integer) sets the minimum length of the generated binaries (same as in list_of/2). Ignored if :length is present.

  • :max_length - (non-negative integer) sets the maximum length of the generated binaries (same as in list_of/2). 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.

Link to this function

bind(data, fun) View Source
bind(t(a), (a -> t(b))) :: t(b) when a: term(), b: term()

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 non-empty 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(), min_length: 1), 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.

Link to this function

bind_filter(data, fun, max_consecutive_failures \\ 10) View Source
bind_filter(t(a), (a -> {:cont, t(b)} | :skip), non_neg_integer()) ::
  t(b)
when a: term(), b: term()

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 by data 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. See the documentation for filter/3 for suggestions on how to avoid such errors.

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.list_of(StreamData.integer(), min_length: 1)

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).

Link to this function

bitstring(options \\ []) View Source
bitstring(keyword()) :: t(bitstring())

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 in list_of/2).

  • :min_length - (non-negative integer) sets the minimum length of the generated bitstrings (same as in list_of/2). Ignored if :length is present.

  • :max_length - (non-negative integer) sets the maximum length of the generated bitstrings (same as in list_of/2). 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.

Link to this function

check_all(data, options, fun) View Source
check_all(t(a), Keyword.t(), (a -> {:ok, term()} | {:error, b})) ::
  {:ok, map()} | {:error, map()}
when a: term(), b: term()

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 by StreamData.

  • {: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 which fun 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, ok_map} - if all generated values satisfy fun. ok_map is a map of metadata that contains no keys for now.

  • {:error, error_map} - if a generated value doesn't satisfy fun. error_map is a map of metadata that contains the following keys:

    • :original_failure - if fun returned {:error, term} for a generated value, this key in the map will be term.

    • :shrunk_failure - the value returned in {:error, term} by fun 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.

    • :successful_runs - the number of successful runs before a failing value was found.

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 by 1 on each iteration. See the "Generation size" section of the module documentation for more information on generation size. Defaults to 1.

  • :max_runs - (non-negative integer) the total number of elements to generate out of data and check through fun. Defaults to 100.

  • :max_run_time - (non-negative integer) the total number of time (in milliseconds) to run a given check for. This is not used by default, so unless a value is given, then the length of the check will be determined by :max_runs. If both :max_runs and :max_run_time are given, then the check will finish at whichever comes first, :max_runs or :max_run_time.

  • :max_shrinking_steps - (non-negative integer) the maximum numbers of shrinking steps to perform in case check_all/3 finds an element that doesn't satisfy fun. Defaults to 100.

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.

Link to this function

constant(term) View Source
constant(a) :: t(a) when a: var

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.

Link to this function

filter(data, predicate, max_consecutive_failures \\ 25) View Source
filter(t(a), (a -> as_boolean(term())), non_neg_integer()) :: t(a)
when a: term()

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. There are a few ways you can avoid risking StreamData.FilterTooNarrowError errors.

  • Try to make sure that your filter filters out only a small subset of the elements generated by data. For example, having something like StreamData.filter(StreamData.integer(), &(&1 != 0)) is usually fine because only a very tiny part of the generation space (integers) is being filtered out.

  • Keep an eye on how the generation size affects the generator being filtered. For example, take something like StreamData.filter(StreamData.positive_integer(), &(&1 not in 1..5). While it seems like this filter is not that strict (as we're filtering out only a handful of numbers out of all natural numbers), this filter will fail with small generation sizes. Since positive_integer/0 returns an integer between 0..size, if size is small (for example, less than 10) then the probabilitty of generating many consecutive values in 1..5 is high.

  • Try to restructure your generator so that instead of generating many values and taking out the ones you don't want, you instead generate values and turn all of them into values that are suitable. A good example is a generator for even integers. You could write it as

    def even_integers() do
      StreamData.filter(StreamData.integer(), &Integer.is_even/1)
    end

    but this would generate many unused values, increasing likeliness of StreamData.FilterTooNarrowError errors and performing inefficiently. Instead, you can use map/2 to turn all integers into even integers:

    def even_integers() do
      StreamData.map(StreamData.integer(), &(&1 * 2))
    end

Shrinking

All the values that each generated value shrinks to satisfy predicate as well.

Link to this function

fixed_list(datas) View Source
fixed_list([t(a)]) :: t([a]) when a: term()

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.

Link to this function

fixed_map(data) View Source
fixed_map(map() | keyword()) :: t(map())

Generates maps with fixed keys and generated values.

data_map is a map or keyword list 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: "", integer: 1}, %{binary: "", integer: -2}, %{binary: "R1^", integer: -3}]

Shrinking

This generator shrinks by shrinking the values of the generated map.

Link to this function

float(options \\ []) View Source
float(keyword()) :: t(float())

Generates floats according to the given options.

The complexity of the generated floats grows proportionally to the generation size.

Options

  • :min - (float) if present, the generated floats will be greater than or equal to this value.

  • :max - (float) if present, the generated floats will be less than or equal to this value.

If neither of :min or :max is provided, then unbounded floats will be generated.

Shrinking

Values generated by this generator will shrink towards simpler floats. Such values are not guaranteed to shrink towards smaller or larger values (but they will never violate the :min or :max options).

Link to this function

frequency(frequencies) View Source
frequency([{pos_integer(), t(a)}]) :: t(a) when a: term()

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.

Generates integers bound by the generation size.

Examples

Enum.take(StreamData.integer(), 3)
#=> [1, -1, -3]

Shrinking

Generated values shrink towards 0.

Link to this function

integer(range) View Source
integer(Range.t()) :: t(integer())

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

Link to this function

keyword_of(value_data) View Source
keyword_of(t(a)) :: t(keyword(a)) when a: term()

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.

The same as calling list_of/2 with [] as options.

Link to this function

list_of(data, options) View Source
list_of(t(a), keyword()) :: t([a]) when a: term()

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.

Link to this function

map(data, fun) View Source
map(t(a), (a -> b)) :: t(b) when a: term(), b: term()

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.

Link to this function

map_of(key_data, value_data, options \\ []) View Source
map_of(t(key), t(value), keyword()) :: t(%{optional(key) => value})
when key: term(), value: term()

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/2: if more than max_tries duplicate keys are generated consequently, it raises a StreamData.TooManyDuplicatesError exception.

Options

  • :length - (non-negative integer) same as in list_of/2.

  • :min_length - (non-negative integer) same as in list_of/2.

  • :max_length - (non-negative integer) same as in list_of/2.

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.

Link to this function

maybe_improper_list_of(first, improper) View Source
maybe_improper_list_of(t(a), t(b)) :: t(maybe_improper_list(a, b))
when a: term(), b: term()

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.

Link to this function

member_of(enum) View Source
member_of(Enumerable.t()) :: t(term())

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.

Link to this function

nonempty(enum_data) View Source
nonempty(t(Enumerable.t())) :: t(Enumerable.t())

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]]
Link to this function

nonempty_improper_list_of(first, improper) View Source
nonempty_improper_list_of(t(a), t(b)) ::
  t(nonempty_improper_list(a, b))
when a: term(), b: term()

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.

Link to this function

one_of(datas) View Source
one_of([t(a)]) :: t(a) when a: term()

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.

Link to this function

optional_map(data) View Source
optional_map(map() | keyword()) :: t(map())

Generates maps with fixed but optional keys and generated values.

data_map is a map or keyword list of fixed_key => data pairs. Maps generated by this generator will have a subset of the keys of data_map and values corresponding to the values generated by the generator unders those keys.

Examples

data = StreamData.optional_map(%{
  integer: StreamData.integer(),
  binary: StreamData.binary(),
})
Enum.take(data, 3)
#=> [%{binary: "", int: 1}, %{int: -2}, %{binary: "R1^"}]

Shrinking

This generator shrinks by first shrinking the map by taking out keys until the map is empty, and then by shrinking the generated values.

Link to this function

positive_integer() View Source
positive_integer() :: t(pos_integer())

Generates positive integers bound by the generation size.

Examples

Enum.take(StreamData.positive_integer(), 3)
#=> [1, 1, 3]

Shrinking

Generated values shrink towards 1.

Link to this function

resize(data, new_size) View Source
resize(t(a), size()) :: t(a) when a: term()

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]
Link to this function

scale(data, size_changer) View Source
scale(t(a), (size() -> size())) :: t(a) when a: term()

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)
Link to this function

seeded(data, seed) View Source
seeded(t(a), integer()) :: t(a) when a: term()

Makes the given generator data always used the same given seed when generating.

This function is useful when you want a generator to have a predictable generating behaviour. It's especially useful when using a generator with the Enumerable protocol since you can't set the seed specifically in that case (while you can with check_all/3 for example).

seed must be an integer.

Examples

int = StreamData.seeded(StreamData.integer(), 10)

Enum.take(int, 3)
#=> [-1, -2, 1]
Enum.take(int, 4)
#=> [-1, -2, 1, 2]
Link to this function

sized(fun) View Source
sized((size() -> t(a))) :: t(a) when a: term()

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]
Link to this function

string(kind_or_codepoints, options \\ []) View Source

Generates a string of the given kind or from the given characters.

kind_or_codepoints 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.

  • :printable - printable strings (String.printable?/1 returns true) are generated. Such strings shrink towards lower codepoints.

  • a range - strings with characters from the range are generated. Such strings shrink towards characters that appear earlier in the range.

  • a list of ranges or single codepoints - strings with characters from the ranges or codepoints are generated. Such strings shrink towards earlier elements of the given list and towards the beginning of ranges.

Options

See the documentation of list_of/2 for the possible values of options.

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_codepoints above.

Link to this function

term() View Source
term() :: t(simple | [simple] | %{optional(simple) => simple} | tuple())
when simple: boolean() | integer() | binary() | float() | atom() | reference()

Generates any term.

The terms that this generator can generate are simple terms or compound terms. The simple terms are:

Compound terms are terms that contain other terms (which are generated recursively with term/0):

Examples

Enum.take(StreamData.term(), 3)
#=> [0.5119003572251588, {{true, ""}}, :WJg]

Shrinking

The terms generated by this generator shrink based on the generator used to create them (see the list of possible generated terms above).

Link to this function

tree(leaf_data, subtree_fun) View Source
tree(t(a), (child_data :: t(a | b) -> t(b))) :: t(a | b)
when a: term(), b: term()

Generates trees of values generated by leaf_data and subtree_fun.

leaf_data generates the leaf nodes. subtree_fun is a function that is called by tree, if an inner node of the tree shall be generated. It takes a generator child_gen for child nodes and returns a generator for an inner node using child_gen to go "one level deeper" in the tree. The frequency between leaves and inner nodes is 1:2.

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) or a %Branch{} struct:

defmodule Branch do
  defstruct [:left, :right]
end

Now, we can generate trees by using the integer() generator to generate the leaf nodes. Then we can use the subtree_fun function to generate inner nodes (that is, %Branch{} structs or integer()s).

tree_data =
  StreamData.tree(StreamData.integer(), fn child_data ->
    StreamData.map({child_data, child_data}, fn {left, right} ->
      %Branch{left: left, right: right}
    end)
  end)

Enum.at(StreamData.resize(tree_data, 10), 0)
#=> %Branch{left: %Branch{left: 4, right: -1}, right: -2}

Examples

A common example is a nested list:

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.

Link to this function

tuple(tuple_datas) View Source
tuple(tuple()) :: t(tuple())

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.

Link to this function

uniq_list_of(data, options \\ []) View Source
uniq_list_of(t(a), keyword()) :: t([a]) when a: term()

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 to Enum.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 a StreamData.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 like uniq_list_of(integer(), min_length: 4) and you start generating elements out of it with a generation size of 1, it will fail by definition because integer/0 generates in -size..size so it would only generate in a set ([-1, 0, 1]) with three elements. Use resize/2 or scale/2 to manipulate the size (for example by setting a minimum generation size of 3) in such cases.

  • :length - (non-negative integer) same as in list_of/2.

  • :min_length - (non-negative integer) same as in list_of/2.

  • :max_length - (non-negative integer) same as in list_of/2.

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.

Link to this function

unshrinkable(data) View Source
unshrinkable(t(a)) :: t(a) when a: term()

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.