1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
|
{0 qcheck-lin}
{1 Content}
- {!module-Lin} is a base module for specifying sequential consistency tests.
- {!module-Lin_domain} exposes a functor that allows to test a library under
parallel usage (with domains).
- {!module-Lin_thread} exposes a functor that allows to test a library under
concurrent usage (with threads).
- {!module-Lin_effect} exposes a functor that allows to test a library under
concurrent usage (with effects).
{1 Overview: what is [qcheck-lin]?}
[qcheck-lin] is a testing library based on [QCheck] for sequential consistency.
A parallel or concurrent program is said to be sequentially consistent if "the
result of any execution is the same as if the operations of all the processors
were executed in some sequential order, and the operations of each individual
processor appear in this sequence in the order specified by its
program."{{!section-ref}{^ 1}}
According to a library description, [qcheck-lin] generates random programs
using the functionalities of this library and runs them, records the results
and checks whether the observed results are linearizable by reconciling them
with a sequential execution.
[qcheck-lin] offers an embedded domain specific language to easily describe
signatures succinctly. It provides three types of tests:
- a parallel one, generating and running parallel programs with two domains and
testing for sequential consistency,
- another concurrent one, generating and running parallel programs with two
threads and testing for sequential consistency,
- a concurrent one using effects, generating and running parallel programs with
two fibers and testing for sequential consistency.
{1 Example: how to test a library?}
Suppose we want to implement a small mutable set library, our main focus being
to have a constant time [cardinal] operation. We will be using {!Stdlib.Set} for
the content, keeping track of the cardinality when adding and removing
elements.
Of course, we will be using [qcheck-lin] for testing!
Our library reads like that:
{[
module type S = sig
type elt
type t
val empty : unit -> t
val mem : elt -> t -> bool
val add : elt -> t -> unit
val remove : elt -> t -> unit
val cardinal : t -> int
end
module Lib : sig
module Make : functor (Ord : Set.OrderedType) -> S with type elt = Ord.t
end = struct
module Make (Ord : Set.OrderedType) = struct
module S = Set.Make (Ord)
type elt = Ord.t
type t = { mutable content : S.t; mutable cardinal : int }
let empty () = { content = S.empty; cardinal = 0 }
let mem a t = S.mem a t.content
let add a t =
if not (mem a t) then begin
t.content <- S.add a t.content;
t.cardinal <- t.cardinal + 1
end
let remove a t =
if mem a t then (
t.content <- S.remove a t.content;
t.cardinal <- t.cardinal - 1)
let cardinal t = t.cardinal
end
end
]}
{2 Writing a specification}
In order to test it for sequential consistency, [qcheck-lin] needs a
lightweight specification of our library's interface. This specification takes
the form of a module matching the {{!Lin.Spec}[Spec]} signature. Then
[qcheck-lin] does all the heavy lifting for us!
This specification exposes one type and three values:
- type [t] which is the main type of our library, here the mutable set.
- [init] that tells [qcheck-lin] how to create an initial value of type [t].
- [cleanup] that tells [qcheck-lin] how to clean up after the tests (which is
necessary when [t] uses resources that must be released, such as opened
files, network connections, etc.).
- [api] which is a list of the library's functions we want to include in the
tests. These functions are encoded using a custom embedded domain specific
language.
{[
open Lin
module LibInt = Lib.Make (Int)
module Spec : Spec = struct
type t = LibInt.t
let init = LibInt.empty
let cleanup _ = ()
let api =
let int = nat_small in
[
val_ "mem" LibInt.mem (int @-> t @-> returning bool);
val_ "add" LibInt.add (int @-> t @-> returning unit);
val_ "remove" LibInt.remove (int @-> t @-> returning unit);
val_ "cardinal" LibInt.cardinal (t @-> returning int);
]
end
]}
Let's have a closer look at the [api] value. This is where [qcheck-lin] gets
the information about the functions we want to include in our tests. [api] is a
list of values. These values should be constructed using either the
{{!Lin.val_}[val_]} function or the {{!Lin.val_freq}[val_freq]} one.
In the example, we use [val_], but let's first describe [val_freq].
[val_freq] takes four arguments:
- an [int] that is a weight used by [QCheck] for generation. It allows to skew
the distribution of the function in the generated programs.
- its name as a [string] so that it can be printed in the output.
- the function itself, so that it can be called in the tests.
- an encoding of its type which is used to generate arguments to the function.
[val_] is a specialization of [val_freq], giving the same weight to all the
elements.
Note that the domain specific language brings some static guarantees about the
type encoding. If we make a mistake, we will know at compile time.
{2 Running the tests}
Now we are set to run our first [qcheck-lin] tests!
We will be testing our library for parallel usage. The functor
{!module-Lin_domain.Make} takes the [Spec] as argument and exposes two
functions:
- {{!Lin_domain.Make.lin_test}[lin_test]} to build a positive [QCheck] test,
- {{!Lin_domain.Make.neg_lin_test}[neg_lin_test]} to build a negative [QCheck] test.
Here, we expect the test to succeed, so we will use the first one.
{[
module LibDomain = Lin_domain.Make (Spec)
let _ =
QCheck_base_runner.run_tests ~verbose:true
[ LibDomain.lin_test ~count:1000 ~name:"Lin parallel tests" ]
]}
And the test fails...
{[
$ dune exec ./mutable_set.exe
random seed: 429006728
generated error fail pass / total time test name
[✗] 1 0 1 0 / 1000 1.9s Lin parallel tests
--- Failure --------------------------------------------------------------------
Test Lin parallel tests failed (41 shrink steps):
|
|
.---------------------.
| |
add 0 t add 0 t
cardinal t
+++ Messages ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Messages for test Lin parallel tests:
Results incompatible with sequential execution
|
|
.------------------------------------.
| |
add 0 t : () add 0 t : ()
cardinal t : 2
================================================================================
failure (1 tests failed, 0 tests errored, ran 1 tests)
]}
In the case of a failing test, [qcheck-lin] prints the counterexample
it has found, after some shrinking steps, as is customary in property-based
testing {i à la} QuickCheck. The counterexample is a program with a sequential
prefix and two parallel suffixes. The counterexample is given twice: the first
time with just the function calls; and the second time, each call is paired
with its result.
Here, the counterexample is composed of an empty sequential prefix and two
spawned processes. The first one adds 0 to the set, while the second one also
adds 0 to the set and then asks for the cardinality of the said set.
In terms of sequential interleaving, there are only three possibilities. If we
prefix the function call by [Left] and [Right] depending on the process in
which they are, those sequential interleavings are:
1. [Left.add 0 t; Right.add 0 t; Right.cardinal t]
2. [Right.add 0 t; Left.add 0 t; Right.cardinal t]
3. [Right.add 0 t; Right.cardinal t; Left.add 0 t]
None of them can explain the value 2 returned by [cardinal t].
What happened is that both calls to [add] have checked whether [0] was already
an element of the set at the same time (or at least before the other one had a
chance to add it). It was not. So, according to our implementation, both calls
added the element and incremented the [cardinal] field.
One way to make our library safe for parallel usage is to protect the set with
a mutex.
{[
module type S = sig
type elt
type t
val empty : unit -> t
val mem : elt -> t -> bool
val add : elt -> t -> unit
val remove : elt -> t -> unit
val cardinal : t -> int
end
module Lib : sig
module Make : functor (Ord : Set.OrderedType) -> S with type elt = Ord.t
end = struct
module Make (Ord : Set.OrderedType) = struct
module S = Set.Make (Ord)
type elt = Ord.t
type t = { mutable content : S.t; mutable cardinal : int; mutex : Mutex.t }
let empty () = { content = S.empty; cardinal = 0; mutex = Mutex.create () }
let mem_non_lock a t = S.mem a t.content
let mem a t =
Mutex.lock t.mutex;
let b = S.mem a t.content in
Mutex.unlock t.mutex;
b
let add a t =
Mutex.lock t.mutex;
if not (mem_non_lock a t) then being
t.content <- S.add a t.content;
t.cardinal <- t.cardinal + 1
end;
Mutex.unlock t.mutex
let remove a t =
Mutex.lock t.mutex;
if mem_non_lock a t then begin
t.content <- S.remove a t.content;
t.cardinal <- t.cardinal - 1
end;
Mutex.unlock t.mutex
let cardinal t =
Mutex.lock t.mutex;
let c = t.cardinal in
Mutex.unlock t.mutex;
c
end
end
]}
Once this is done, we can use exactly the same specification and tests. The
successful output looks like the following:
{[
$ dune exec ./mutable_set_lock.exe
random seed: 162610433
generated error fail pass / total time test name
[✓] 1000 0 0 1000 / 1000 48.0s Lin parallel tests
================================================================================
success (ran 1 tests)
]}
{1 [Lin] in a bit more detail}
Underneath the hood [Lin] uses QCheck and OCaml's pseudo-random number
generator from the [Random] module to generate arbitrary [cmd]
sequences and arbitrary input argument data to each call. To recreate
a problematic test run, one therefore needs to generate the same
pseudo-random test case input, by passing the same randomness seed.
By running the [Lin] tests using [QCheck_base_runner.run_tests_main]
from QCheck, it is possible to pass a seed as a command line argument
as follows:
{[
$ dune exec ./mutable_set.exe -- -s 429006728
]}
Despite generating and thus running the same test case input, one may
still experience different behaviours on subsequent reruns of the
resulting test, because of CPU scheduling and other factors. This may
materialize as different counterexamples being printed or as one run
failing the test whereas another run passes it. {!Lin_domain} uses the
{!Util.repeat} combinator to repeat each test case 50 times to
address the issue and help increase reproducibility.
{1 Current limitations}
[Lin] comes with a number of limitations which we plan to address in
future releases. Currently {{!Lin.Spec.api}[Spec.api]} descriptions:
- support only one {{!Lin.Spec.t}[Spec.t]} - namely the one resulting
from {{!Lin.Spec.init}[Spec.init]},
- do not support composing {{!Lin.Spec.t}[Spec.t]} with other type
combinators such as {!Lin.list} and {!Lin.option} - this restriction
is expressed with the {!Lin.noncombinable} type parameter in {!Lin.t},
- do not support using the result of {!Lin.int_bound} as both an
argument type and as a result type - you can use {!Lin.int} for the
latter,
- do not support specifying function values using arrow syntax [t1 @-> t2],
- do not support specifying tuple values using product syntax [t1 * t2].
The later two can however be addressed by writing a custom argument
generator using {!Lin.gen}.
{1:ref References}
1. Lamport, {i How to Make a Multiprocessor Computer That Correctly Executes
Multiprocess Program}, 1979, DOI: 10.1109/TC.1979.1675439
|