Hegel.GeneratorsGenerators 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:
map transforms every generated value with a function.flat_map builds a dependent generator, where a later generator is chosen from an earlier value. For example, draw a length n, then a list of exactly n elements.filter keeps only values satisfying a predicate.one_of and sampled_from choose among alternatives.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.
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.
Phantom witness that a generator carries a printer; see generator.
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:
Hegel.draw_silent, which produces the value but records nothing for the failing-replay output.Hegel.with_printer to obtain a printable generator that can be drawn from with Hegel.drawbooleans ?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)
;;integers ?min_value ?max_value () creates a generator for integers within the given bounds.
Defaults:
min_value: OCaml native int minmax_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) generatorfloats ?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: falseexclude_max: falseallow_nan: true only when no bounds are setallow_infinity: true when at most one bound is setBounds 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) generatortext ?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 rangecategories: 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 charactersalphabet: 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) generatorcharacters ?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) generatormake_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).
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)
;;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) generatorjust 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)
;;val lists :
('a, printable) generator ->
?min_size:int ->
?max_size:int ->
?unique:bool ->
unit ->
('a list, printable) generatorlists 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) generatorassoc_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) generatormake_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) generatorhash_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) generatorsampled_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
;;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)
;;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)
;;val tuples2 :
('a, printable) generator ->
('b, printable) generator ->
('a * 'b, printable) generatortuples2 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) generatortuples3 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) generatortuples4 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)
;;val functions :
?name:string ->
?sexp_of_arg:('a -> Sexplib0.Sexp.t) ->
returns:('b, _) generator ->
unit ->
('a -> 'b, unprintable) generatorfunctions ?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) generatorfunctions2 ?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) generatorfunctions3 ?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.
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 '@')
;;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)
;;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)
;;A proleptic Gregorian calendar date. year is in [-999999, 999999], month in [1, 12], and day in [1, days-in-month].
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) generatormake_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) generatormake_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) generatormake_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].
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)
;;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) generatordatetimes ?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')
;;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
;;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)
;;val composite : (test_case -> 'a) -> ('a, unprintable) generatorcomposite 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) generatormap 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) generatorflat_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)
;;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)
;;