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, 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:
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 () creates a generator for boolean values.
let%hegel_test booleans_example tc =
let b = draw tc (Generators.booleans ()) 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 (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) generatorfloats ?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: falseexclude_max: falseallow_nan: true only when no bounds are setallow_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) 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: always include these characters even if excluded by other filtersexclude_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 (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) 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 (Generators.characters ~codec:"ascii" ()) in
assert (String.length c >= 1)
;;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) 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 (Generators.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 (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) 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
(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) generatorhash_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) generatorsampled_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
;;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)
;;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)
;;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
(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) 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
(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) 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
(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)
;;val functions :
?name:string ->
?sexp_of_arg:('a -> Core.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 =
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) 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 -> 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) 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 (Generators.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 (Generators.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 (Generators.domains ~max_length:64 ()) in
assert (String.length d <= 64)
;;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)
;;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))
;;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
;;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)
;;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 =
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) generatormap 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) 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 = 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)
;;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)
;;