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.

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 (integers ~min_value:0 ~max_value:100 ()) in
    let y = draw tc (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 : ?p:float -> unit -> (bool, printable) generator

booleans ?p () creates a generator for boolean values, true with probability p (default 0.5). p must be between 0 and 1.

  let%hegel_test booleans_example tc =
    let b = draw tc (booleans ~p:0.9 ()) 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 (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 / max_value: unbounded. With allow_infinity = false the range is [-max_float, max_float].
  • 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

Bounds that only allow infinity to be generated while allow_infinity is false, such as ~min_value:infinity, raise Usage_error.

  let%hegel_test floats_example tc =
    let f = draw tc (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: add these characters even if the codepoint range or categories exclude them. Each must be encodable in codec and must not also appear in exclude_characters, or the draw raises Usage_error.
  • 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 (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 (characters ~codec:"ascii" ()) in
    assert (String.length c >= 1)
  ;;
val make_characters : of_char:(char -> 'a) -> sexp_of:('a -> Sexplib0.Sexp.t) -> unit -> ('a, printable) generator

make_characters ~of_char ~sexp_of () builds a generator for single characters over any representation 'a, covering the full native char range (codepoints 0-255, i.e. Latin-1).

val chars : unit -> (char, printable) generator

chars () creates a generator for single characters (codepoints 0-255, i.e. Latin-1) as native char values.

  let%hegel_test chars_example tc =
    let c = draw tc (chars ()) in
    assert (Char.code c >= 0 && Char.code c <= 0xFF)
  ;;
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 (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 (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 (lists (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
        (assoc_lists
           (text ~max_size:4 ())
           (integers ~min_value:0 ~max_value:9 ())
           ~max_size:5
           ())
    in
    assert (List.length m <= 5)
  ;;
val make_hash_tables : of_pairs:(('a * 'b) list -> 't) -> sexp_of_t: (('a -> Sexplib0.Sexp.t) -> ('b -> Sexplib0.Sexp.t) -> 't -> Sexplib0.Sexp.t) -> ('a, printable) generator -> ('b, printable) generator -> ?min_size:int -> ?max_size:int -> unit -> ('t, printable) generator

make_hash_tables ~of_pairs ~sexp_of_t keys values ?min_size ?max_size () builds a hash-table generator over any table type 't.

val hash_tables : ('a, printable) generator -> ('b, printable) generator -> ?min_size:int -> ?max_size:int -> unit -> (('a, 'b) Stdlib.Hashtbl.t, printable) generator

hash_tables keys values ?min_size ?max_size () creates a generator for Stdlib.Hashtbl.t tables over printable keys and values

  let%hegel_test hash_tables_example tc =
    let m =
      draw tc
        (hash_tables
           (text ~max_size:4 ())
           (integers ~min_value:0 ~max_value:9 ())
           ~max_size:5
           ())
    in
    assert (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.

  let%hegel_test sampled_from_example tc =
    let color = draw_silent tc (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. Raises Usage_error if generators is empty. 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
        (one_of
           [ integers ~min_value:0 ~max_value:9 ()
           ; 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 (optional (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
        (tuples2 (integers ~min_value:0 ~max_value:9 ()) (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
        (tuples3
           (integers ~min_value:0 ~max_value:9 ())
           (booleans ())
           (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
        (tuples4
           (integers ~min_value:0 ~max_value:9 ())
           (booleans ())
           (text ~max_size:4 ())
           (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 -> Sexplib0.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 =
      functions ~sexp_of_arg:[%sexp_of: int] ~returns:(integers ()) ()
    in
    let f = draw_silent tc f_gen in
    let xs = draw tc (lists (integers ()) ()) in
    assert (List.length (List.map f xs) = List.length xs)
  ;;
val functions2 : ?name:string -> ?sexp_of_arg1:('a -> Sexplib0.Sexp.t) -> ?sexp_of_arg2:('b -> Sexplib0.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 -> Sexplib0.Sexp.t) -> ?sexp_of_arg2:('b -> Sexplib0.Sexp.t) -> ?sexp_of_arg3:('c -> Sexplib0.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 (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 (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 (domains ~max_length:64 ()) in
    assert (String.length d <= 64)
  ;;
type date = {
  1. year : int;
  2. month : int;
  3. day : int;
}

A proleptic Gregorian calendar date. year is in [-999999, 999999], month in [1, 12], and day in [1, days-in-month].

type time = {
  1. hour : int;
  2. minute : int;
  3. second : int;
  4. nanosecond : int;
}

A time of day at nanosecond resolution. hour is in [0, 23], minute and second in [0, 59], and nanosecond in [0, 999999999].

val make_dates : of_date:(date -> 'a) -> sexp_of:('a -> Sexplib0.Sexp.t) -> ?min_date:date -> ?max_date:date -> unit -> ('a, printable) generator

make_dates ~of_date ~sexp_of ?min_date ?max_date () builds a generator for dates in [min_date, max_date] over any representation. of_date converts each drawn date to the desired representation. The default range is [0001-01-01, 9999-12-31].

  let core_dates =
    make_dates
      ~of_date:(fun { year; month; day } ->
        Core.Date.create_exn ~y:year ~m:(Core.Month.of_int_exn month) ~d:day)
      ~sexp_of:Core.Date.sexp_of_t
      ()
val make_times : of_time:(time -> 'a) -> sexp_of:('a -> Sexplib0.Sexp.t) -> ?min_time:time -> ?max_time:time -> unit -> ('a, printable) generator

make_times ~of_time ~sexp_of ?min_time ?max_time () builds a generator for times of day in [min_time, max_time] over any representation. of_time converts each drawn time to the desired representation. The default range is [00:00:00.000000000, 23:59:59.999999999].

val make_datetimes : of_datetime:((date * time) -> 'a) -> sexp_of:('a -> Sexplib0.Sexp.t) -> ?min_datetime:(date * time) -> ?max_datetime:(date * time) -> unit -> ('a, printable) generator

make_datetimes ~of_datetime ~sexp_of ?min_datetime ?max_datetime () builds a generator for naive datetimes in [min_datetime, max_datetime] over any representation. of_datetime converts each drawn (date, time) pair to the desired representation. The default range is [0001-01-01T00:00:00.000000000, 9999-12-31T23:59:59.999999999].

val dates : ?min_date:date -> ?max_date:date -> unit -> (string, printable) generator

dates ?min_date ?max_date () creates a generator for ISO 8601 YYYY-MM-DD date strings in [min_date, max_date]. The default range is [0001-01-01, 9999-12-31].

  let%hegel_test dates_example tc =
    let d = draw tc (dates ()) in
    assert (String.length d = 10)
  ;;
val times : ?min_time:time -> ?max_time:time -> unit -> (string, printable) generator

times ?min_time ?max_time () creates a generator for ISO 8601 HH:MM:SS.fffffffff time-of-day strings in [min_time, max_time]. The default range is [00:00:00.000000000, 23:59:59.999999999].

  let%hegel_test times_example tc =
    let t =
      draw tc (times ~min_time:{ hour = 9; minute = 0; second = 0; nanosecond = 0 } ())
    in
    assert (String.length t = 18 && String.compare t "09:00:00.000000000" >= 0)
  ;;
val datetimes : ?min_datetime:(date * time) -> ?max_datetime:(date * time) -> unit -> (string, printable) generator

datetimes ?min_datetime ?max_datetime () creates a generator for naive ISO 8601 YYYY-MM-DDTHH:MM:SS.fffffffff datetime strings in [min_datetime, max_datetime]. The default range is [0001-01-01T00:00:00.000000000, 9999-12-31T23:59:59.999999999].

  let%hegel_test datetimes_example tc =
    let dt = draw tc (datetimes ()) in
    assert (String.contains dt 'T')
  ;;
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 (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. A pattern that does not parse, or that no string matches, raises Usage_error when drawn.

  let%hegel_test from_regex_example tc =
    let s = draw tc (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 =
    composite (fun tc ->
      let x = draw_silent tc (integers ~min_value:0 ~max_value:9 ()) in
      let y = draw_silent tc (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 (map (fun x -> x * 2) (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 = integers ~min_value:0 ~max_value:5 () in
    let xs =
      draw_silent tc
        (flat_map
           (fun n ->
              lists
                (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 (filter (fun x -> x mod 2 = 0) (integers ~min_value:0 ~max_value:100 ()))
    in
    assert (even mod 2 = 0)
  ;;