Ppx_expect#

Ppx_expect is a framework for writing tests for OCaml code printing textual output.

There is pretty comprehensive documentation about Inline expectation tests and the ppx_expect tool for writing them:

Here, we will just cover enough to get started using them inside the Octez codebase.

How to run tests#

If you want to run tests and see whether they succeed, build the standard runtest alias:

dune runtest src/lib_stdlib
# or dune build @@src/lib_stdlib/runtest

The result would look like this:

File "src/lib_stdlib/bloomer.ml", line 97, characters 0-817: random_read_writes (0.006 sec)
File "src/lib_stdlib/bloomer.ml", line 123, characters 0-1339: peek and poke work with bits = [1 .. Sys.int_size - 7] (0.012 sec)
File "src/lib_stdlib/bloomer.ml", line 163, characters 0-739: sequential_read_writes (0.021 sec)
File "src/lib_stdlib/bloomer.ml", line 182, characters 0-705: read_over_write (0.001 sec)
File "src/lib_stdlib/bloomer.ml", line 282, characters 0-835: consistent_add_mem_countdown (0.275 sec)
File "src/lib_stdlib/bloomer.ml", line 309, characters 0-1531: consistent_add_countdown_count (2.079 sec)
File "src/lib_stdlib/bloomer.ml", line 456, characters 4-137 (11.966 sec)
File "src/lib_stdlib/bloomer.ml", line 461, characters 4-1695: <<match Sys.getenv_opt "BLOOMER_TEST_GNUPLOT_PA[...]>> (0.000 sec)

A test can fail because it generates an output different from what is expected or because it raises an exception. Failures are reported as a diff in both cases.

Different output:

File "src/proto_alpha/lib_client/limit.ml", line 1, characters 0-0:
------ src/proto_alpha/lib_client/limit.ml
++++++ src/proto_alpha/lib_client/limit.ml.corrected
File "src/proto_alpha/lib_client/limit.ml", line 57, characters 0-1:
 |      if eq x y then Result.return_some x
 |      else error_with "Limit.join: error (%s)" where
 |
 |let%expect_test "join" =
 |  let pp_print_err fmt = function
 |    | Result.Error _ -> Format.pp_print_string fmt "error"
 |    | Ok x ->
 |        Format.(
 |          pp_print_option
 |            ~none:(fun fmt () -> pp_print_string fmt "None")
 |            pp_print_bool)
 |          fmt
 |          x
 |  in
 |  let print x = Format.fprintf Format.std_formatter "%a" pp_print_err x in
 |  print (join ~where:__LOC__ Bool.equal (Some true) (Some true)) ;
-|  [%expect {| false |}] ;
+|  [%expect {| true |}] ;
 |  print (join ~where:__LOC__ Bool.equal None None) ;
 |  [%expect {| None |}] ;
 |  print (join ~where:__LOC__ Bool.equal None (Some true)) ;
 |  [%expect {| true |}] ;
 |  print (join ~where:__LOC__ Bool.equal (Some true) None) ;
 |  [%expect {| true |}] ;
 |  print (join ~where:__LOC__ Bool.equal (Some true) (Some false)) ;
 |  [%expect {| error |}]
 |
 |let get ~when_unknown = function
 |  | None -> error_with "Limit.get: %s" when_unknown
 |  | Some x -> ok x
 |
 |let%expect_test "get" =
 |  let pp_print_err fmt = function
 |    | Result.Error _ -> Format.fprintf fmt "error"

Exception raised:

File "src/lib_stdlib/bloomer.ml", line 1, characters 0-0:
------ src/lib_stdlib/bloomer.ml
++++++ src/lib_stdlib/bloomer.ml.corrected
File "src/lib_stdlib/bloomer.ml", line 309, characters 0-1:
 |    let bloomer = create ~hash ~index_bits ~hashes ~countdown_bits in
 |    let rec init n acc =
 |      if n = 0 then acc
 |      else
 |        let x = Random.int (1 lsl 29) in
 |        add bloomer x ;
 |        assert (mem bloomer x) ;
 |        init (n - 1) (x :: acc)
 |    in
 |    let all = init 1000 [] in
 |    for _ = 0 to (1 lsl countdown_bits) - 2 do
 |      List.iter (fun x -> assert (mem bloomer x)) all ;
 |      countdown bloomer
 |    done ;
 |    List.iter (fun x -> assert (not (mem bloomer x))) all
 |  done
+|[@@expect.uncaught_exn {|
+|  (* CR expect_test_collector: This test expectation appears to contain a backtrace.
+|     This is strongly discouraged as backtraces are fragile.
+|     Please change this test to not include a backtrace. *)
+|
+|  "Assert_failure src/lib_stdlib/bloomer.ml:287:4"
+|  Raised at Tezos_stdlib__Bloomer.(fun) in file "src/lib_stdlib/bloomer.ml", line 287, characters 4-18
+|  Called from Expect_test_collector.Make.Instance_io.exec in file "collector/expect_test_collector.ml", line 262, characters 12-19 |}]

How to add tests#

Ppx_expect is based on ppx_inline_test machinery. That is, it collects tests automatically for you.

To add a new test, make sure you have the following in your library stanza in the dune file:

(inline_tests)
(preprocess (pps ppx_expect))

In the manifest, just add the following argument to the library containing expect tests:

~inline_tests:ppx_expect

Adding new tests is then just a matter of adding let%expect_test at top level:

let%expect_test "optional name" =
  print_endline "hello world";
  [%expect {||}]

Running test with the example above will fail and show you a diff between your source and the corrected one:

 |let%expect_test "optional name" =
 |  print_endline "hello world";
-|  [%expect {||}]
+|  [%expect {| hello world |}]

If you agree with the diff, just ask dune to promote the source and you’re done:

dune runtest --auto-promote

Where to put the tests#

Expect tests can live next to the implementation or in a different library dedicated to tests (e.g. if you don’t want to polute your source/binary or if your want to only test the exposed API).

Integration with Lwt#

Ppx_expect can be used in combination with Lwt, see the package description. This integration has not been tested on the Octez codebase yet, hence some work will be needed to a have specific support for the codebase.