Module Monolith
Monolith offers facilities for testing an OCaml library by comparing its implementation (known as the candidate implementation) against a reference implementation.
For general information on Monolith and its workflow, please consult README.md and the paper Strong Automated Testing of OCaml Libraries, which describes the use and the design of Monolith in somewhat greater depth than this documentation.
This documentation is split up into the following parts:
- Facilities for Generating Data.
- Facilities for Displaying Data and Code.
- Combinators for constructing Specifications.
- Functions for setting up and starting the Monolith Engine.
- Miscellaneous runtime Support functions.
Generating Data
module Gen : sig ... end
The submodule
Gen
offers facilities for generating values of many common types.
type 'a gen
= 'a Gen.gen
A value of type
'a gen
is a generator of values of type'a
.
Displaying Data and Code
type document
= PPrint.document
We use the PPrint library to pretty-print OCaml code.
module Print : sig ... end
The submodule
Print
offers facilities for printing OCaml values and expressions.
type 'a printer
= 'a Print.printer
A value of type
'a gen
is a printer of values of type'a
.
type appearance
A value of type
appearance
is a printable representation an OCaml expression. One can think of the typeappearance
almost as synonym for the typedocument
. In particular, the functiondocument
is an injection ofdocument
intoappearance
. The typeappearance
offers a few bells and whistles that the typedocument
does not have; these include the ability to print applications in a customized manner.
val constant : string -> appearance
constant
is an injection ofstring
intoappearance
. The appearanceconstant c
is printed as the stringc
(without quotes).
val document : document -> appearance
document
is an injection ofdocument
intoappearance
. The appearancedocument doc
is printed as the documentdoc
.
val infix : string -> appearance
infix op
is an appearance for an OCaml infix operator namedop
, whereop
is the name of the operator, without parentheses. This appearance is set up so that an application of it to exactly two actual arguments is printed infix. An application of it to more or fewer than two actual arguments is printed using normal application syntax.
type 'a code
= 'a * appearance
A value of type
'a code
is a pair of a value of type'a
and a printable representation of this value. Several specification combinators, includingconstructible
,deconstructible
,declare_abstract_type
,map_into
, andmap_outof
expect an argument of type'a code
.
Specifications
type (_, _) spec
A specification of type
('r, 'c) spec
describes a value whose type on the reference side is'r
and whose type on the candidate side is'c
.
val constructible : (unit -> 't code) -> ('t, 't) spec
constructible generate
describes a basic constructible type, that is, a type't
that is equipped with a generator.The function
generate
must have typeunit -> 't code
, which means that it must produce a pair of a value and a printable representation of this value. (See also the combinatorconstructible_
, which has slightly different requirements.)It is worth noting that
constructible
can be used, if desired, to construct a value whose type is a function type.When a value must be constructed, the function
generate
is applied once, and the value thus obtained is used both on the reference side and on the candidate side. This explains why the return type of this combinator is('t, 't) spec
.This specification is constructible.
val easily_constructible : 't gen -> 't printer -> ('t, 't) spec
easily_constructible generate print
describes a basic constructible type, that is, a type't
that is equipped with a generator.It is a special case of
constructible
. It is less powerful, but is easier to use.The function
generate
must have type't gen
. The functionprint
must have type't printer
. These functions are combined to obtain a generator of typeunit -> 't code
, which is used in a call toconstructible
.This specification is constructible.
val deconstructible : ?equal:('t -> 't -> bool) code -> 't printer -> ('t, 't) spec
deconstructible ~equal print
describes a basic deconstructible type, that is, a type't
that is equipped with an equality test and with a printer.The equality test
equal
is used to compare the values produced by the reference implementation and by the candidate implementation. (The reference value is the first argument; the candidate value is the second argument.)The argument
equal
is optional. If it is omitted, then OCaml's generic equality function(=)
is used.Because the reference value and the candidate value are expected to have the same type, the return type of this combinator is
('t, 't) spec
.This specification is deconstructible.
val declare_abstract_type : ?check:('r -> ('c -> unit) code) -> ?var:string -> unit -> ('r, 'c) spec
declare_abstract_type()
declares a new abstract type, whose values on the reference side have type'r
and whose values on the candidate side have type'c
.An abstract type is usually implemented in different ways in the reference implementation and in the candidate implementation. For instance, a sequence may be represented as a linked list in the reference implementation and as a resizeable array in the candidate implementation.
The optional parameter
check
is a well-formedness check. If present, this function is applied by Monolith, after every operation, to every data structure of this abstract type that is currently at hand. This allows checking after every operation that every data structure remains well-formed.The
check
function is applied to two arguments, namely, the reference data structure of type'r
and the candidate data structure of type'c
.If all is well, then
check
should return()
. If something is wrong, thencheck
should raise an exception, such asAssertion_failure _
.It is up to the user to decide how thorough (and how costly) the well-formedness check should be. Checking that the candidate data structure seems well-formed, while ignoring the reference data structure, is a possibility. Checking that the candidate data structure is well-formed and is in agreement with the reference data structure is the most comprehensive check that can be performed.
The optional parameter
var
is a base name that is used for variables of this abstract type.This specification is constructible and deconstructible.
Because
declare_abstract_type
declares an abstract type as a side effect, it cannot be used under a dependent arrow^>>
. It is recommended to use it at the top level only.
val unit : (unit, unit) spec
unit
represents the base typeunit
.This specification is constructible and deconstructible.
val bool : (bool, bool) spec
bool
represents the base typebool
.This specification is constructible and deconstructible.
val int : (int, int) spec
int
represents the basic deconstructible typeint
.This specification is deconstructible. It is not constructible, because it usually does not make sense to generate an integer in the huge interval
[min_int, max_int]
.
val int_within : int gen -> (int, int) spec
int_within range
is the basic typeint
, equipped with the generatorrange
.This specification is constructible and deconstructible.
val semi_open_interval : int -> int -> (int, int) spec
semi_open_interval i j
isint_within (Gen.semi_open_interval i j)
.
val closed_interval : int -> int -> (int, int) spec
closed_interval i j
isint_within (Gen.closed_interval i j)
.
val lt : int -> (int, int) spec
lt j
isint_within (Gen.lt j)
.
val le : int -> (int, int) spec
le j
isint_within (Gen.le j)
.
val sequential : unit -> (int, int) spec
sequential()
isint_within (Gen.sequential())
.
val exn : (exn, exn) spec
exn
represents the base typeexn
.This specification is deconstructible, but not constructible.
val override_exn_eq : ((exn -> exn -> bool) -> exn -> exn -> bool) -> unit
override_exn_eq f
overrides the notion of equality that is associated with the typeexn
. This notion of equality is used to compare an exception raised by the reference implementation with an exception raised by the candidate implementation. By default, it is OCaml's generic equality(=)
, which means that both implementations must raise exactly the same exceptions.override_exn_eq
allows relaxing this requirement. The functionf
receives the current notion of equality as an argument and is expected to return a new notion of equality, which is installed in place of the previous one.
val ignored : ('r, 'c) spec
ignored
describes a result that should be ignored.This specification is deconstructible, but not constructible.
val (***) : ('r1, 'c1) spec -> ('r2, 'c2) spec -> ('r1 * 'r2, 'c1 * 'c2) spec
***
is the pair type constructor.When applied to constructible specifications, it produces a constructible specification. When applied to deconstructible specifications, it produces a deconstructible specification.
val option : ('r, 'c) spec -> ('r option, 'c option) spec
option
is the option type constructor.When applied to a constructible specification, it produces a constructible specification. When applied to a deconstructible specification, it produces a deconstructible specification.
val result : ('r1, 'c1) spec -> ('r2, 'c2) spec -> (('r1, 'r2) Stdlib.result, ('c1, 'c2) Stdlib.result) spec
result
is the result type constructor.When applied to constructible specifications, it produces a constructible specification. When applied to deconstructible specifications, it produces a deconstructible specification.
val list : ?length:int gen -> ('r, 'c) spec -> ('r list, 'c list) spec
list ~length
is the list type constructor.When applied to a constructible specification, it produces a constructible specification. When applied to a deconstructible specification, it produces a deconstructible specification.
When this specification is used in construction mode, the generator
length
controls the length of the lists that are generated. Thelength
parameter is optional; if is omitted,Gen.lt 16
is used. When this specification is used in deconstruction mode, thelength
parameter is irrelevant.
type 'r diagnostic
=
|
Valid of 'r
|
Invalid of document -> document
Using the combinator
nondet
allows the reference implementation to have access to the candidate result of type'c
produced by the candidate implementation. It must then produce a diagnostic of type'r diagnostic
.The diagnostic
Valid r
means that the candidate result is acceptable and that the corresponding reference result isr
.The diagnostic
Invalid cause
means that the candidate result is wrong. The functioncause
is an explanation why the candidate result is unacceptable. This function is applied to the name of a variable, such asobserved
, which stands for the candidate result. It is expected to produce a piece of OCaml code that pinpoints the problem. This code could be an OCaml assertion that the observed result does not satisfy, such asassert (observed > 0)
. It could also be just a comment, such as(* candidate finds -2, whereas a positive number was expected *)
. Or, it could be a combination of both. The modulePrint
can help construct such OCaml code.
type 'r nondet
= 'r -> 'r diagnostic
In the common case where
'r
and'c
are the same type, the following type abbreviation is useful. Then, using the combinatornondet
changes the type of the reference implementation from'r
to'r nondet
.
val nondet : ('r, 'c) spec -> ('c -> 'r diagnostic, 'c) spec
nondet spec
describes the result of an operation whose specification is nondeterministic. It indicates that several results are permitted. Therefore, one cannot expect the reference implementation to produce "the" expected result. Instead, Monolith must run the candidate implementation first, and allow the reference implementation to have access to the resultc
produced by the candidate. The reference implementation is then expected to return a result of type'r diagnostic
. If the reference implementation returnsValid r
, then the deconstruction process continues: that is, the reference resultr
and the candidate resultc
are deconstructed in the manner specified byspec
.When
'r
and'c
are in fact the same type, the type ofnondet
can be written under the form('t, 't) spec -> ('t nondet, 't) spec
.nondet
must be applied to a deconstructible specification, and produces a deconstructible specification.
val (^>) : ('r1, 'c1) spec -> ('r2, 'c2) spec -> ('r1 -> 'r2, 'c1 -> 'c2) spec
^>
is the ordinary arrow combinator. It describes a function of one argument. By using it several times, one can also describe curried functions of several arguments, which are common in OCaml. This combinator imposes the absence of exceptions: the reference and candidate implementations are expected to not raise any exception.In an application
domain ^> codomain
, the specificationdomain
must be constructible, whilecodomain
must be either a function specification or deconstructible. The specificationdomain ^> codomain
is neither constructible nor deconstructible.
val (^?>) : ('r1, 'c1) spec -> ('r2, 'c2) spec -> ('r1 -> 'c2 -> 'r2 diagnostic, 'c1 -> 'c2) spec
^?>
is the nondeterministic arrow combinator.spec1 ^?> spec2
is a short-hand forspec1 ^> nondet spec2
.The (de)constructibility constraints are the same as with an ordinary arrow
(^>)
.
val (^!>) : ('r1, 'c1) spec -> ('r2, 'c2) spec -> ('r1 -> 'r2, 'c1 -> 'c2) spec
^!>
is the exceptional arrow combinator. It describes a function that may raise an exception, and it requests that this exception be caught and observed. This implies that the reference and candidate implementations are expected to raise an exception under the same circumstances, and are expected to raise comparable exceptions, according to the notion of equality that exists at typeexn
. This notion of equality can be customized by usingoverride_exn_eq
.The (de)constructibility constraints are the same as with an ordinary arrow
(^>)
.
val (^!?>) : ('r1, 'c1) spec -> ('r2, 'c2) spec -> ('r1 -> ('c2, exn) Stdlib.result -> ('r2, exn) Stdlib.result diagnostic, 'c1 -> 'c2) spec
^!?>
is an arrow combinator that combines the exception effect and the nondeterminism effect. It describes a function that may raise an exception. Furthermore, it allows nondeterminism: the candidate implementation is allowed to decide whether it wishes to return normally or to raise an exception; it is also allowed to decide what exception it wishes to raise. To deal with this flexibility, the behavior of the candidate implementation is reified as a value of type('c2, exn) result
, which the reference implementation receives as an argument. The reference implementation is then expected to either accept or reject this candidate behavior, which it does by returning a diagnostic. If it decides to accept this behavior, then it must return its own behavior as a value of type('r2, exn) result
. The reference implementation must never raise an exception.The (de)constructibility constraints are the same as with an ordinary arrow
(^>)
.
val (^>>) : ('r1, 'c1) spec -> ('r1 -> ('r2, 'c2) spec) -> ('r1 -> 'r2, 'c1 -> 'c2) spec
^>>
is the dependent arrow constructor. It describes a function of one argument and allows naming (the reference side of) this argument, so that this argument can be referred to in the codomain.For example, the specification of a
get
function in a sequence might beseq ^>> fun s -> lt (length s) ^> element
. The first argument, a sequence, receives the names
. This allows specifying that the second argument, an integer, must lie in the semi-open interval[0, length s)
. Here, the variables
denotes the data structure built by the reference implementation, solength
must be the reference-side function that maps a sequence to its length.The (de)constructibility constraints are the same as with an ordinary arrow
(^>)
.
val (%) : ('r -> bool) -> ('r, 'c) spec -> ('r, 'c) spec
%
is the subset constructor. It restricts the set of arguments that can be passed to an operation; in other words, it expresses a precondition. For example, to express the fact that the operationpop
must be applied to a nonempty stack, one can usenonempty % stack
, where the reference-side functionnonempty
tests whether a stack is nonempty, andstack
is the abstract type of stacks.%
must be applied to a constructible specification, and produces a constructible specification.
val map_outof : ('r1 -> 'r2) -> ('c1 -> 'c2) code -> ('r1, 'c1) spec -> ('r2, 'c2) spec
map_outof
specifies that a transformation must be applied to a value. The user must provide the reference side of the transformation, the candidate side of the transformation, and a specification of the input type of the transformation. It is typically used to transform an argument before passing it to an operation.map_outof
must be applied to a constructible specification, and produces a constructible specification.
val map_into : ('r1 -> 'r2) -> ('c1 -> 'c2) code -> ('r2, 'c2) spec -> ('r1, 'c1) spec
map_into
specifies that a transformation must be applied to a value. The user must provide the reference side of the transformation, the candidate side of the transformation, and a specification of the output type of the transformation. It is typically used to transform the result of an operation, or an operation itself.map_into
must be applied to a deconstructible specification, and produces a deconstructible specification.
val flip : ('r1 -> 'r2 -> 'r3, 'c1 -> 'c2 -> 'c3) spec -> ('r2 -> 'r1 -> 'r3, 'c2 -> 'c1 -> 'c3) spec
flip
exchanges the first two arguments of a curried function.
val rot2 : ('r1 -> 'r2 -> 'r3, 'c1 -> 'c2 -> 'c3) spec -> ('r2 -> 'r1 -> 'r3, 'c2 -> 'c1 -> 'c3) spec
rot2
moves the second argument of a curried function to the first position. It is synonymous withflip
.
val rot3 : ('r3 -> 'r1 -> 'r2 -> 'r4, 'c3 -> 'c1 -> 'c2 -> 'c4) spec -> ('r1 -> 'r2 -> 'r3 -> 'r4, 'c1 -> 'c2 -> 'c3 -> 'c4) spec
rot3
moves the third argument of a curried function to the first position. It is synonymous withflip
.
val curry : ('r1 -> 'r2 -> 'r3, 'c1 -> 'c2 -> 'c3) spec -> (('r1 * 'r2) -> 'r3, ('c1 * 'c2) -> 'c3) spec
curry
transforms a function that expects a pair into a function that expects two separate arguments.
val uncurry : (('r1 * 'r2) -> 'r3, ('c1 * 'c2) -> 'c3) spec -> ('r1 -> 'r2 -> 'r3, 'c1 -> 'c2 -> 'c3) spec
uncurry
transforms a function that expects two separate arguments into a function that expects a pair.
val ifpol : ('r, 'c) spec -> ('r, 'c) spec -> ('r, 'c) spec
The conditional specification
ifpol neg pos
is interpreted as the specificationneg
when it is used in construction mode (that is, when it describes the input of an operation) and as the specificationpos
when it is used in deconstruction mode (that is, when it describes the output of an operation).ifpol
is a low-level combinator that is typically used to define higher-level abstractions.In an application
ifpol neg pos
, the specificationneg
must be constructible, the specificationpos
must be deconstructible, and the result is both constructible and deconstructible.
val declare_semi_abstract_type : ('r, 'c) spec -> ('r, 'c) spec
The function call
declare_semi_abstract_type spec
declares a new abstract typet
and equips it with one operation, namely a one-way conversion fromt
tospec
, whose implementation is the identity function.This is typically used to disguise a function type as an abstract type. For instance, if an operation has type
a -> b -> c
and if one wishes Monolith to perform a partial application, then one should declareb ^> c
as a semi-abstract typet
and use the specificationa ^> t
.declare_semi_abstract_type
must be applied to a deconstructible specification, and produces a deconstructible specification.Because
declare_semi_abstract_type
declares an abstract type as a side effect, it cannot be used under a dependent arrow^>>
. It is recommended to use it at the top level only.
val declare_seq : ?length:int gen -> ('r, 'c) spec -> ('r Stdlib.Seq.t, 'c Stdlib.Seq.t) spec
declare_seq ~length
is a persistent sequence type constructor.When applied to a constructible specification, it produces a constructible specification. When applied to a deconstructible specification, it produces a deconstructible specification.
When this specification is used in construction mode, the generator
length
controls the length of the sequences that are generated. Thelength
parameter is optional; if is omitted,Gen.lt 16
is used. When this specification is used in deconstruction mode, thelength
parameter is irrelevant.When this specification is used in deconstruction mode, it is treated as an abstract type, equipped with an operation that demands the first element of the sequence. It is possible for a sequence to be demanded several times.
A sequence is not allowed to raise an exception when it is demanded: such an event is considered an error.
Because
declare_seq
declares an abstract type as a side effect, it cannot be used under a dependent arrow^>>
. It is recommended to use it at the top level only.
val declare_affine_seq : ?length:int gen -> ('r, 'c) spec -> ('r Stdlib.Seq.t, 'c Stdlib.Seq.t) spec
declare_affine_seq ~length
is an affine sequence type constructor.When applied to a constructible specification, it produces a constructible specification. When applied to a deconstructible specification, it produces a deconstructible specification.
When this specification is used in construction mode, the generator
length
controls the length of the sequences that are generated. Thelength
parameter is optional; if is omitted,Gen.lt 16
is used. When this specification is used in deconstruction mode, thelength
parameter is irrelevant.When this specification is used in deconstruction mode, it is treated as an abstract type, equipped with an operation that demands the first element of the sequence. An affine sequence is demanded at most once.
A sequence is not allowed to raise an exception when it is demanded: such an event is considered an error.
Because
declare_affine_seq
declares an abstract type as a side effect, it cannot be used under a dependent arrow^>>
. It is recommended to use it at the top level only.
Engine
val declare : string -> ('r, 'c) spec -> 'r -> 'c -> unit
declare name spec reference candidate
declares the existence of an operation. Its parameters are the name of this operation, its specification, and its implementations on the reference side and on the candidate side.declare
can be called either beforemain
is invoked or from the functionprologue
that is passed as an argument tomain
. It cannot be used under a dependent arrow^>>
.
val main : ?prologue:(unit -> unit) -> int -> unit
main fuel
sets up and starts the engine.main
parses the command line and sets up the engine to draw data from the file whose name is supplied on the command line. If no file name is supplied, data is drawn from/dev/urandom
.The parameter
fuel
is the maximum length of a run (expressed as a number of operations). A small value, such as 5, 10, or 20, is typically used.An optional
prologue
can be supplied. If present, the functionprologue
is invoked once at the beginning of each run. It may invoke data generation functions in the moduleGen
, declare operations, and produce output usingdprintf
. Its purpose is to determine the global parameters of the run, if desired. For instance, if the library under test is a bounded stack and takes the form of a functor that expects a boundn
, then the prologue can choose a value ofn
, apply the functor, and declare the operations thus obtained. The demodemos/working/stack_prologue
illustrates this.
val dprintf : ('a, Stdlib.Buffer.t, unit) Stdlib.format -> 'a
dprintf
is analogous toprintf
. Its output is actually printed to the standard output channel only if a scenario that leads to a problem is discovered. In that case, it is printed at the beginning of the scenario.This can be exploited, for instance, to print a number of global settings that have been made in the prologue.
dprintf
can be called either beforemain
is invoked or from the functionprologue
that is passed as an argument tomain
. It cannot be used under a dependent arrow^>>
.
exception
PleaseBackOff
The exception
PleaseBackOff
can be raised by the reference implementation of an operation to indicate that this operation (or this particular choice of arguments for this operation) should not be exercised. The reason could be that it is not permitted, or that it has not yet been implemented. This exception causes Monolith to silently back off and try another operation.The reference implementation must not perform any side effect before raising this exception.
exception
Unimplemented
The exception
Unimplemented
can be raised by the candidate implementation of an operation to indicate that this operation (or this particular choice of arguments for this operation) should not be exercised. This exception causes Monolith to silently abandon the current scenario and investigate other scenarios.
Support
module Support : sig ... end