Module Hegel.Generators

Generators for composable test data generation.

This module provides a composable generator API for property-based testing. Generators produce typed OCaml values.

All examples in this documentation assume open Hegel, and refer to generators as Generators.foo.

The usual way to build a compound value is to Hegel.draw its parts in sequence:

  type point = { x : int; y : int }

  let%hegel_test points_stay_in_range tc =
    let x = draw tc (Generators.integers ~min_value:0 ~max_value:100 ()) in
    let y = draw tc (Generators.integers ~min_value:0 ~max_value:100 ()) in
    let p = { x; y } in
    require tc ~msg:"point coordinates stay in range"
      (p.x <= 100 && p.y <= 100)
  ;;

Generator combinators are higher-order functions that take generators and/or functions as input and assemble them into a single generator:

Every generator carries a phantom type 'p recording whether it holds a printer. Primitive generators are printable and may be drawn with Hegel.draw, which prints the drawn value on a failing replay.

type ('a, 'p) generator

A generator producing values of type 'a. The phantom 'p is printable when the generator carries a printer (and so may be drawn with Hegel.draw) and unprintable otherwise.

type printable

Phantom witness that a generator carries a printer; see generator.

type unprintable

Phantom witness that a generator carries no printer; see generator.

A generator is unprintable whenever the result type is the caller's rather than the engine's, so Hegel has no printer to attach. This covers map, flat_map, sampled_from, just, composite, and the generators produced by [@@deriving hegel_generator]. An unprintable generator cannot be drawn with Hegel.draw. There are two ways to use an unprintable generator:

Primitive generators

val booleans : unit -> (bool, printable) generator

booleans () creates a generator for boolean values.

  let%hegel_test booleans_example tc =
    let b = draw tc (Generators.booleans ()) in
    assert (b = true || b = false)
  ;;
val integers : ?min_value:int -> ?max_value:int -> unit -> (int, printable) generator

integers ?min_value ?max_value () creates a generator for integers within the given bounds.

Defaults:

  • min_value: OCaml native int min
  • max_value: OCaml native int max
  let%hegel_test integers_example tc =
    let n = draw tc (Generators.integers ~min_value:1 ~max_value:6 ()) in
    assert (n >= 1 && n <= 6)
  ;;
val floats : ?min_value:float -> ?max_value:float -> ?exclude_min:bool -> ?exclude_max:bool -> ?allow_nan:bool -> ?allow_infinity:bool -> unit -> (float, printable) generator

floats ?min_value ?max_value ?exclude_min ?exclude_max ?allow_nan ?allow_infinity () creates a generator for floating-point values.

Defaults:

  • min_value: 64-bit float min (only when both allow_nan and allow_infinity are false)
  • max_value: 64-bit float max (only when both allow_nan and allow_infinity are false)
  • exclude_min: false
  • exclude_max: false
  • allow_nan: true only when no bounds are set
  • allow_infinity: true when at most one bound is set
  let%hegel_test floats_example tc =
    let f = draw tc (Generators.floats ~min_value:0.0 ~max_value:1.0 ()) in
    assert (Float.compare f 0.0 >= 0)
  ;;
val text : ?min_size:int -> ?max_size:int -> ?codec:string -> ?min_codepoint:int -> ?max_codepoint:int -> ?categories:string list -> ?exclude_categories:string list -> ?include_characters:string -> ?exclude_characters:string -> ?alphabet:string -> unit -> (string, printable) generator

text ?min_size ?max_size ?codec ?min_codepoint ?max_codepoint ?categories ?exclude_categories ?include_characters ?exclude_characters ?alphabet () creates a generator for Unicode text strings.

Character filtering options restrict which characters may appear:

  • codec: restrict to characters encodable in this codec (e.g. "ascii", "utf-8", "latin-1")
  • min_codepoint, max_codepoint: restrict Unicode codepoint range
  • categories: whitelist of Unicode general categories (e.g. ["L"; "Nd"]). Mutually exclusive with exclude_categories.
  • exclude_categories: blacklist of Unicode general categories. Mutually exclusive with categories.
  • include_characters: always include these characters even if excluded by other filters
  • exclude_characters: always exclude these characters
  • alphabet: fixed set of allowed characters. Mutually exclusive with all individual character filtering parameters.

Surrogate codepoints (category Cs) are always excluded since OCaml strings are conventionally UTF-8.

  let%hegel_test text_example tc =
    let s = draw tc (Generators.text ~min_size:1 ~max_size:8 ~codec:"ascii" ()) in
    assert (String.length s >= 1)
  ;;
val characters : ?codec:string -> ?min_codepoint:int -> ?max_codepoint:int -> ?categories:string list -> ?exclude_categories:string list -> ?include_characters:string -> ?exclude_characters:string -> unit -> (string, printable) generator

characters ?codec ?min_codepoint ?max_codepoint ?categories ?exclude_categories ?include_characters ?exclude_characters () creates a generator for single Unicode characters (as single-character UTF-8 strings).

Accepts the same character filtering options as text except min_size, max_size, and alphabet. Surrogate codepoints (category Cs) are always excluded since OCaml strings are conventionally UTF-8.

  let%hegel_test characters_example tc =
    let c = draw tc (Generators.characters ~codec:"ascii" ()) in
    assert (String.length c >= 1)
  ;;
val binary : ?min_size:int -> ?max_size:int -> unit -> (string, printable) generator

binary ?min_size ?max_size () creates a generator for binary byte strings.

  let%hegel_test binary_example tc =
    let bytes = draw tc (Generators.binary ~min_size:0 ~max_size:16 ()) in
    assert (String.length bytes <= 16)
  ;;
val just : 'a -> ('a, unprintable) generator

just value creates a generator that always produces value. The output type is the caller's, so the result carries no printer.

  let%hegel_test just_example tc =
    let x = draw_silent tc (Generators.just 42) in
    assert (x = 42)
  ;;

Collection generators

val lists : ('a, printable) generator -> ?min_size:int -> ?max_size:int -> ?unique:bool -> unit -> ('a list, printable) generator

lists elements ?min_size ?max_size ?unique () creates a generator for lists of elements. When unique is true, elements will be distinct.

  let%hegel_test lists_example tc =
    let xs =
      draw tc (Generators.lists (Generators.integers ~min_value:0 ~max_value:9 ()) ~max_size:10 ())
    in
    assert (List.length xs <= 10)
  ;;
val assoc_lists : ('a, printable) generator -> ('b, printable) generator -> ?min_size:int -> ?max_size:int -> unit -> (('a * 'b) list, printable) generator

assoc_lists keys values ?min_size ?max_size () creates a generator for association lists over printable keys and values: (key, value) pairs, in generation order, whose keys are unique.

  let%hegel_test assoc_lists_example tc =
    let m =
      draw tc
        (Generators.assoc_lists
           (Generators.text ~max_size:4 ())
           (Generators.integers ~min_value:0 ~max_value:9 ())
           ~max_size:5
           ())
    in
    assert (List.length m <= 5)
  ;;
val hash_tables : ('a, printable) generator -> ('b, printable) generator -> ?min_size:int -> ?max_size:int -> unit -> (('a, 'b) Core.Hashtbl.t, printable) generator

hash_tables keys values ?min_size ?max_size () creates a generator for polymorphic hash tables over printable keys and values, with entries generated exactly as assoc_lists generates its pairs.

  let%hegel_test hash_tables_example tc =
    let m =
      draw tc
        (Generators.hash_tables
           (Generators.text ~max_size:4 ())
           (Generators.integers ~min_value:0 ~max_value:9 ())
           ~max_size:5
           ())
    in
    assert (Core.Hashtbl.length m <= 5)
  ;;
val sampled_from : 'a list -> ('a, unprintable) generator

sampled_from options creates a generator that samples from a non-empty list of values. Sampling is not uniform: the engine's bounded-integer draw deliberately over-weights boundary and "interesting" indices, so the first element (and, to a lesser extent, the last) is drawn noticeably more often than the middle ones, matching the sibling Hegel client libraries. The output type is the caller's, so the result carries no printer.

  let%hegel_test sampled_from_example tc =
    let color = draw_silent tc (Generators.sampled_from [ `Red; `Green; `Blue ]) in
    ignore color
  ;;
val one_of : ('a, printable) generator list -> ('a, printable) generator

one_of generators creates a generator that picks from one of the given printable generators. Requires at least one generator. On a failing replay, the drawn value prints through the printer of the branch it was drawn from. The recorded branch is per generator value: if one one_of generator is drawn several times before printing (e.g. as the element generator of lists), every value prints through the most recently drawn branch's printer.

  let%hegel_test one_of_example tc =
    let n =
      draw tc
        (Generators.one_of
           [ Generators.integers ~min_value:0 ~max_value:9 ()
           ; Generators.integers ~min_value:90 ~max_value:99 ()
           ])
    in
    assert (n >= 0)
  ;;
val optional : ('a, printable) generator -> ('a option, printable) generator

optional gen creates a generator that produces either None or Some value from the printable gen.

  let%hegel_test optional_example tc =
    let o = draw tc (Generators.optional (Generators.integers ~min_value:0 ~max_value:9 ())) in
    match o with
    | None -> ()
    | Some n -> assert (n >= 0)
  ;;

Tuple generators

val tuples2 : ('a, printable) generator -> ('b, printable) generator -> ('a * 'b, printable) generator

tuples2 g1 g2 creates a generator for 2-element tuples of printable components.

  let%hegel_test tuples2_example tc =
    let n, b =
      draw tc
        (Generators.tuples2 (Generators.integers ~min_value:0 ~max_value:9 ()) (Generators.booleans ()))
    in
    assert (n >= 0 && (b || not b))
  ;;
val tuples3 : ('a, printable) generator -> ('b, printable) generator -> ('c, printable) generator -> ('a * 'b * 'c, printable) generator

tuples3 g1 g2 g3 creates a generator for 3-element tuples of printable components.

  let%hegel_test tuples3_example tc =
    let a, b, c =
      draw tc
        (Generators.tuples3
           (Generators.integers ~min_value:0 ~max_value:9 ())
           (Generators.booleans ())
           (Generators.text ~max_size:4 ()))
    in
    assert (a >= 0 && (b || not b) && String.length c >= 0)
  ;;
val tuples4 : ('a, printable) generator -> ('b, printable) generator -> ('c, printable) generator -> ('d, printable) generator -> ('a * 'b * 'c * 'd, printable) generator

tuples4 g1 g2 g3 g4 creates a generator for 4-element tuples of printable components.

  let%hegel_test tuples4_example tc =
    let a, b, c, d =
      draw tc
        (Generators.tuples4
           (Generators.integers ~min_value:0 ~max_value:9 ())
           (Generators.booleans ())
           (Generators.text ~max_size:4 ())
           (Generators.floats ~min_value:0.0 ~max_value:1.0 ()))
    in
    assert (a >= 0 && (b || not b) && String.length c >= 0 && Float.compare d 0.0 >= 0)
  ;;

Function generators

val functions : ?name:string -> ?sexp_of_arg:('a -> Core.Sexp.t) -> returns:('b, _) generator -> unit -> ('a -> 'b, unprintable) generator

functions ?name ?sexp_of_arg ~returns () creates a generator for functions 'a -> 'b whose results are drawn from returns.

The result carries no printer (its output type is a function), so draw it with Hegel.draw_silent. Applying the drawn function to an argument draws a result from returns the first time that argument is seen and memoizes it.

On the failing final replay each top-level application prints as name arg = result (applications nested inside a span are suppressed). name is ?name when you pass it. Otherwise, the draw-site binding name inside a let%hegel_test, else "function".

The memo table keys on the argument itself (structural hash/equality), so distinct arguments always get independent results. sexp_of_arg only renders the argument in the shown pair. An omitted sexp_of_arg or a non-printable returns shows <opaque>.

  let%hegel_test map_length_preserved tc =
    let f_gen =
      Generators.functions ~sexp_of_arg:Core.Int.sexp_of_t ~returns:(Generators.integers ()) ()
    in
    let f = draw_silent tc f_gen in
    let xs = draw tc (Generators.lists (Generators.integers ()) ()) in
    assert (List.length (List.map ~f xs) = List.length xs)
  ;;
val functions2 : ?name:string -> ?sexp_of_arg1:('a -> Core.Sexp.t) -> ?sexp_of_arg2:('b -> Core.Sexp.t) -> returns:('c, _) generator -> unit -> ('a -> 'b -> 'c, unprintable) generator

functions2 ?name ?sexp_of_arg1 ?sexp_of_arg2 ~returns () creates a generator for curried two-argument functions 'a -> 'b -> 'c.

Sugar over functions keyed on the argument pair: the two arguments form one memo key and are shown uncurried as name (arg1 arg2) = result. Draw it with draw_silent.

val functions3 : ?name:string -> ?sexp_of_arg1:('a -> Core.Sexp.t) -> ?sexp_of_arg2:('b -> Core.Sexp.t) -> ?sexp_of_arg3:('c -> Core.Sexp.t) -> returns:('d, _) generator -> unit -> ('a -> 'b -> 'c -> 'd, unprintable) generator

functions3 ?name ?sexp_of_arg1 ?sexp_of_arg2 ?sexp_of_arg3 ~returns () creates a generator for curried three-argument functions 'a -> 'b -> 'c -> 'd.

Like functions2, keyed on the argument triple and shown uncurried as name (arg1 arg2 arg3) = result. Draw it with draw_silent.

Format generators

val emails : unit -> (string, printable) generator

emails () creates a generator for valid email address strings.

Addresses follow RFC 5321/5322: a local part of 1 to 64 characters from the RFC 5322 atext set, an @, and a domain from domains, with the overall address length capped at 254 octets (RFC 5321 §4.5.3.1.3).

  let%hegel_test emails_example tc =
    let e = draw tc (Generators.emails ()) in
    assert (String.contains e '@')
  ;;
val urls : unit -> (string, printable) generator

urls () creates a generator for valid URL strings.

URLs follow RFC 3986, of the form scheme://domain[:port]/path[#fragment] with scheme one of http/https, the domain drawn from domains (up to 255 characters), an optional port in 1, 65535, zero or more /-separated path segments of up to 100 characters each, and an optional fragment of up to 100 characters. Path and fragment characters are percent-encoded.

  let%hegel_test urls_example tc =
    let u = draw tc (Generators.urls ()) in
    assert (String.length u > 0)
  ;;
val domains : ?max_length:int -> unit -> (string, printable) generator

domains ?max_length () creates a generator for domain name strings.

Domains are RFC 1035 fully-qualified domain names: a top-level domain sampled from the IANA TLD list followed by up to 126 dot-separated labels, each 1 to 63 characters matching [a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? (punycode xn-- labels reserved by RFC 5890 are excluded). Generated domains never exceed max_length (default 255, per RFC 1035 §2.3.4); when provided, max_length must be in 4, 255.

  let%hegel_test domains_example tc =
    let d = draw tc (Generators.domains ~max_length:64 ()) in
    assert (String.length d <= 64)
  ;;
val dates : unit -> (Core.Date.t, printable) generator

dates () creates a generator for calendar dates as Core.Date.t values, with year in [1, 9999] and calendar-valid month/day.

  let%hegel_test dates_example tc =
    let d = draw tc (Generators.dates ()) in
    assert (Core.Date.year d >= 1 && Core.Date.year d <= 9999)
  ;;
val times : unit -> (Core.Time_ns.Ofday.t, printable) generator

times () creates a generator for times of day as Core.Time_ns.Ofday.t values with microsecond precision.

  let%hegel_test times_example tc =
    let t = draw tc (Generators.times ()) in
    assert (Core.Time_ns.Ofday.(t >= start_of_day && t < start_of_next_day))
  ;;
val datetimes : unit -> (Core.Date.t * Core.Time_ns.Ofday.t, printable) generator

datetimes () creates a generator for naive datetimes as (Core.Date.t, Core.Time_ns.Ofday.t) pairs, combining dates and times.

  let%hegel_test datetimes_example tc =
    let d, _t = draw tc (Generators.datetimes ()) in
    assert (Core.Date.year d >= 1)
  ;;
val ip_addresses : ?version:[ `V4 | `V6 ] -> unit -> (Ipaddr.t, printable) generator

ip_addresses ?version () creates a generator for typed Ipaddr.t IP addresses. version selects IPv4 (`V4, RFC 791) or IPv6 (`V6, RFC 4291); when omitted, either version is generated. Render a drawn address with Ipaddr.to_string (RFC 5952 canonical form for v6).

  let%hegel_test ip_addresses_example tc =
    match draw tc (Generators.ip_addresses ~version:`V4 ()) with
    | Ipaddr.V4 _ as ip -> assert (String.contains (Ipaddr.to_string ip) '.')
    | Ipaddr.V6 _ -> assert false
  ;;
val from_regex : string -> ?fullmatch:bool -> unit -> (string, printable) generator

from_regex pattern ?fullmatch () creates a generator for strings matching a regular expression pattern, written in the syntax of Python's re module. When fullmatch is true (the default) the whole string must match pattern; otherwise a match anywhere suffices.

  let%hegel_test from_regex_example tc =
    let s = draw tc (Generators.from_regex "[a-z]+" ()) in
    assert (String.length s >= 1)
  ;;

Generator combinators

val composite : (test_case -> 'a) -> ('a, unprintable) generator

composite generate_fn builds a generator from an imperative generate_fn that draws sub-values from the test case and assembles a result, useful when a value is easiest to describe by drawing its parts in sequence.

  let point =
    Generators.composite (fun tc ->
      let x = draw_silent tc (Generators.integers ~min_value:0 ~max_value:9 ()) in
      let y = draw_silent tc (Generators.integers ~min_value:0 ~max_value:9 ()) in
      x, y)
  ;;
val map : ('a -> 'b) -> ('a, 'p) generator -> ('b, unprintable) generator

map f gen transforms values from gen using f.

  let%hegel_test map_example tc =
    let even =
      draw_silent tc (Generators.map (fun x -> x * 2) (Generators.integers ~min_value:0 ~max_value:9 ()))
    in
    assert (even mod 2 = 0)
  ;;
val flat_map : ('a -> ('b, 'q) generator) -> ('a, 'p) generator -> ('b, unprintable) generator

flat_map f gen creates a dependent generator. f receives the generated value and returns a generator whose value is the final result.

  let%hegel_test flat_map_example tc =
    let len_gen = Generators.integers ~min_value:0 ~max_value:5 () in
    let xs =
      draw_silent tc
        (Generators.flat_map
           (fun n ->
              Generators.lists
                (Generators.integers ~min_value:0 ~max_value:9 ())
                ~min_size:n
                ~max_size:n
                ())
           len_gen)
    in
    assert (List.length xs <= 5)
  ;;
val filter : ('a -> bool) -> ('a, 'p) generator -> ('a, 'p) generator

filter predicate gen filters values from gen using predicate, keeping gen's printability. Tries up to three times and rejects the test case if all attempts fail.

  let%hegel_test filter_example tc =
    let even =
      draw tc (Generators.filter (fun x -> x mod 2 = 0) (Generators.integers ~min_value:0 ~max_value:100 ()))
    in
    assert (even mod 2 = 0)
  ;;