My agentic slop goes here. Not intended for anyone else!
1expect-test - a Cram-like framework for OCaml 2============================================= 3 4 5# Introduction 6 7Expect-test is a framework for writing tests in OCaml, similar to 8[Cram](https://bitheap.org/cram/). 9 10Expect-tests mimic the (now less idiomatic) 11[inline test](https://github.com/janestreet/ppx_inline_test) 12framework in providing a 13`let%expect_test` construct. 14 15The body of an expect-test can contain output-generating code, interleaved with 16`[%expect]` extension expressions to denote the expected output. 17 18When run, expect-tests pass iff the output [_matches_](#matching-behavior) the expected 19output. If a test fails, the `inline_tests_runner` outputs a diff and creates a file with 20the suffix ".corrected" containing the actual output. 21 22Here is an example expect-test in `foo.ml`: 23 24<!-- $MDX file=./test/negative-tests/for-mdx/foo.ml,part=addition --> 25```ocaml 26open! Core 27 28let%expect_test "addition" = 29 printf "%d" (1 + 2); 30 [%expect {| 4 |}] 31;; 32``` 33 34When the test runs, the `inline_tests_runner` creates `foo.ml.corrected` with contents: 35 36<!-- $MDX file=./test/negative-tests/for-mdx/foo.ml.corrected.expected,part=addition --> 37```ocaml 38open! Core 39 40let%expect_test "addition" = 41 printf "%d" (1 + 2); 42 [%expect {| 3 |}] 43;; 44``` 45 46`inline_tests_runner` also outputs: 47 48<!-- $MDX file=./test/negative-tests/for-mdx/test-output.expected --> 49``` 50------ foo.ml 51++++++ foo.ml.corrected 52File "foo.ml", line 6, characters 0-1: 53 |open! Core 54 | 55 |let%expect_test "addition" = 56 | printf "%d" (1 + 2); 57-| [%expect {| 4 |}] 58+| [%expect {| 3 |}] 59 |;; 60 | 61``` 62 63Diffs are shown in color if the `-use-color` flag is passed to the inline test runner 64executable. 65 66# Common usage 67 68Each `[%expect]` block matches all the output generated since the previous `[%expect]` 69block (or the beginning of the test). In this way, when multiple `[%expect]` blocks are 70interleaved with test code, they can help show which part of the test produced which 71output. 72 73The following test: 74 75<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml,part=interleaved --> 76```ocaml 77let%expect_test "interleaved" = 78 let l = [ "a"; "b"; "c" ] in 79 printf "A list [l]\n"; 80 printf "It has length %d\n" (List.length l); 81 [%expect {| A list [l] |}]; 82 List.iter l ~f:print_string; 83 [%expect 84 {| 85 It has length 3 86 abc 87 |}] 88;; 89``` 90 91is rewritten as 92 93<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml.corrected.expected,part=interleaved --> 94```ocaml 95let%expect_test "interleaved" = 96 let l = [ "a"; "b"; "c" ] in 97 printf "A list [l]\n"; 98 printf "It has length %d\n" (List.length l); 99 [%expect 100 {| 101 A list [l] 102 It has length 3 103 |}]; 104 List.iter l ~f:print_string; 105 [%expect {| abc |}] 106;; 107``` 108 109When there is "trailing" output at the end of a `let%expect_test` (output that has yet to 110be matched by some `[%expect]` block), a new `[%expect]` block is appended to the test 111with the trailing output: 112 113<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml,part=trailing --> 114```ocaml 115let%expect_test "trailing output" = 116 print_endline "Hello"; 117 [%expect {| Hello |}]; 118 print_endline "world" 119;; 120``` 121 122becomes: 123 124<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml.corrected.expected,part=trailing --> 125```ocaml 126let%expect_test "trailing output" = 127 print_endline "Hello"; 128 [%expect {| Hello |}]; 129 print_endline "world"; 130 [%expect {| world |}] 131;; 132``` 133 134# Matching behavior 135 136You might have noticed that the contents of the `[%expect]` blocks are not _exactly_ the 137program output; in some of the examples above, they contain a different number of leading 138and trailing newlines, and are indented to match the code indentation. We say the contents 139of a block `[%expect str]` (where `str` is some string literal) _match_ the output at that 140block if the output, after we format it to standardize indentation and other whitespace, 141is identical to the contents of `str` 142after it has been similarly formatted 143. 144 145The formatting applied depends on the type of delimiter used in `str` (i.e. whether it a 146`"quoted string"` or a `{xxx| delimited string |xxx}`). To summarize: 147 148* Output containing only whitespace is formatted as `[%expect {| |}]` or `[%expect ""]`. 149* Output where only one line contains non-whitespace characters is formatted onto a single 150 line, as `[%expect {| output |}]` or `[%expect "output"]`. 151* Output where multiple lines contain non-whitespace characters is formatted so that: 152 - There is no trailing whitespace on lines with content. 153 - The relative indentation of the lines is preserved. 154 - In `{| delimited strings |}`, the least-indented line with content (the "left margin" 155 of the output) is aligned to be two spaces past the indentation of the `[%expect]` 156 block. 157 - In `"quoted string"`, the least-indented line is indented by exactly one space (this 158 plays the nicest with `ocamlformat`'s existing decisions about how to format string 159 literals). 160 - There is one empty line before and one empty line after the contents. 161 162 163Here is an example containing several cases of output that are subject to distinct 164formatting rules and how they appear in `[%expect]` and `[%expect_exact]` blocks: 165 166<details> 167<summary>Expand examples</summary> 168 169<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml.corrected.expected,part=matching --> 170```ocaml 171let%expect_test "matching behavior --- no content" = 172 printf " "; 173 [%expect {| |}]; 174 printf " "; 175 [%expect ""]; 176 printf " "; 177 [%expect_exact {| |}]; 178 printf " "; 179 [%expect_exact " "] 180;; 181 182let%expect_test "matching behavior --- one line of content" = 183 printf "\n This is one line\n\n"; 184 [%expect {| This is one line |}]; 185 printf "\n This is one line\n\n"; 186 [%expect "This is one line"]; 187 printf "\n This is one line\n\n"; 188 [%expect_exact 189 {| 190 This is one line 191 192|}]; 193 printf "\n This is one line\n\n"; 194 [%expect_exact "\n This is one line\n\n"] 195;; 196 197let%expect_test "matching behavior --- multiple lines of content" = 198 printf 199 {| 200Once upon a midnight dreary, 201 while I pondered, weak and weary, 202Over many a quaint and curious 203 volume of forgotten lore |}; 204 [%expect 205 {| 206 Once upon a midnight dreary, 207 while I pondered, weak and weary, 208 Over many a quaint and curious 209 volume of forgotten lore 210 |}]; 211 printf 212 {| 213Once upon a midnight dreary, 214 while I pondered, weak and weary, 215Over many a quaint and curious 216 volume of forgotten lore |}; 217 [%expect 218 " \n\ 219 \ Once upon a midnight dreary,\n\ 220 \ while I pondered, weak and weary,\n\ 221 \ Over many a quaint and curious\n\ 222 \ volume of forgotten lore\n\ 223 \ "]; 224 printf 225 {| 226Once upon a midnight dreary, 227 while I pondered, weak and weary, 228Over many a quaint and curious 229 volume of forgotten lore |}; 230 [%expect_exact 231 {| 232Once upon a midnight dreary, 233 while I pondered, weak and weary, 234Over many a quaint and curious 235 volume of forgotten lore |}]; 236 printf 237 {| 238Once upon a midnight dreary, 239 while I pondered, weak and weary, 240Over many a quaint and curious 241 volume of forgotten lore |}; 242 [%expect_exact 243 "\n\ 244 Once upon a midnight dreary,\n\ 245 \ while I pondered, weak and weary,\n\ 246 Over many a quaint and curious\n\ 247 \ volume of forgotten lore "] 248;; 249``` 250</details> 251 252Expect-test is by default permissive about this formatting, so that a 253`[%expect]` block that is correct modulo formatting is 254accepted. However, passing `-expect-test-strict-indentation=true` to 255the ppx driver makes the test runner issue corrections for blocks that 256do not satisfy the indentation rules. 257For example, the following: 258 259<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml,part=bad-format --> 260```ocaml 261let%expect_test "bad formatting" = 262 printf "a\n b"; 263 [%expect 264 {| 265a 266 b |}] 267;; 268``` 269is corrected to: 270 271<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml.corrected.expected,part=bad-format --> 272```ocaml 273let%expect_test "bad formatting" = 274 printf "a\n b"; 275 [%expect 276 {| 277 a 278 b 279 |}] 280;; 281``` 282 283(to add the required indentation and trailing newline) 284 285 286# Reachability 287 288## Expects reached from multiple places 289 290A `[%expect]` extension can be encountered multiple times if it is in e.g. a functor or a 291function: 292 293<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml,part=function --> 294```ocaml 295let%expect_test "function" = 296 let f output = 297 print_string output; 298 [%expect {| hello world |}] 299 in 300 f "hello world"; 301 f "hello world" 302;; 303``` 304 305The test passes if the `[%expect]` block matches the output each time it is encountered, 306as described in the section on [matching behavior](#matching-behavior). 307 308If the outputs are not consistent, then the corrected file contains a report of all of the 309outputs that were captured, in the order that they were captured at runtime. 310 311For example, calling `f` in the snippet above with inconsistent arguments will produce: 312 313<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml.corrected.expected,part=broken-function --> 314```ocaml 315let%expect_test "function" = 316 let f output = 317 print_string output; 318 [%expect 319 {| 320 (* expect_test: Test ran multiple times with different test outputs *) 321 ============================ Output 1 / 4 ============================ 322 hello world 323 ============================ Output 2 / 4 ============================ 324 goodbye world 325 ============================ Output 3 / 4 ============================ 326 once upon 327 a midnight dreary 328 ============================ Output 4 / 4 ============================ 329 hello world 330 |}] 331 in 332 f "hello world"; 333 f "goodbye world"; 334 f "once upon\na midnight dreary"; 335 f "hello world" 336;; 337``` 338 339 340## Unreached expects 341 342Every `[%expect]` and `[%expect_exact]` block in a `let%expect_test` must be reached at 343least once if that test is ever run. Failure for control flow to reach a block is _not_ 344treated like recording empty output at a block. The extension expression 345`[%expect.unreachable]` is used to indicate that some part of an expect test shouldn't be 346reached; if control flow reaches that point anyway, the corrected file replaces the 347`[%expect.unreachable]` with a plain old expect containing the output collected until that 348point. Conversely, if control flow never reaches some `[%expect]` block, that block is 349turned into a `[%expect.unreachable]`. For example: 350 351<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml,part=unreachable --> 352```ocaml 353let%expect_test "unreachable" = 354 let interesting_bool = 3 > 5 in 355 printf "%b\n" interesting_bool; 356 if interesting_bool 357 then [%expect {| true |}] 358 else ( 359 printf "don't reach\n"; 360 [%expect.unreachable]) 361;; 362``` 363becomes: 364 365<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml.corrected.expected,part=unreachable --> 366```ocaml 367let%expect_test "unreachable" = 368 let interesting_bool = 3 > 5 in 369 printf "%b\n" interesting_bool; 370 if interesting_bool 371 then [%expect.unreachable] 372 else ( 373 printf "don't reach\n"; 374 [%expect 375 {| 376 false 377 don't reach 378 |}]) 379;; 380``` 381 382Note that, for an expect block that is sometimes reachable and sometimes not, that block 383passes if the output captured at that block matches every time the block is encountered. 384For example, the following test passes: 385 386<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml.corrected.expected,part=sometimes-reachable --> 387```ocaml 388module Test (B : sig 389 val interesting_opt : int option 390 end) = 391struct 392 let%expect_test "sometimes reachable" = 393 match B.interesting_opt with 394 | Some x -> 395 printf "%d\n" x; 396 [%expect {| 5 |}] 397 | None -> [%expect {| |}] 398 ;; 399end 400 401module _ = Test (struct 402 let interesting_opt = Some 5 403 end) 404 405module _ = Test (struct 406 let interesting_opt = None 407 end) 408 409module _ = Test (struct 410 let interesting_opt = Some 5 411 end) 412``` 413 414# Exceptions 415 416When an exception is raised by the body of an expect-test, the `inline_test_runner` shows 417it (and, if relevant, any output generated by the test that had not yet been captured) in 418a `[@@expect.uncaught_exn]` attribute attached to the corresponding `let%expect_test`. 419`[%expect]` blocks in the test are treated according to the usual rules: those reached 420before the exception is raised capture output as usual, and those that "would have" been 421reached after are marked as unreachable: 422 423<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml,part=exn --> 424```ocaml 425let%expect_test "exception" = 426 Printexc.record_backtrace false; 427 printf "start!"; 428 [%expect {| |}]; 429 let sum = 2 + 2 in 430 if sum <> 3 431 then ( 432 printf "%d" sum; 433 failwith "nope"); 434 printf "done!"; 435 [%expect {| done! |}] 436;; 437``` 438 439becomes: 440 441<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml.corrected.expected,part=exn --> 442```ocaml 443let%expect_test "exception" = 444 Printexc.record_backtrace false; 445 printf "start!"; 446 [%expect {| start! |}]; 447 let sum = 2 + 2 in 448 if sum <> 3 449 then ( 450 printf "%d" sum; 451 failwith "nope"); 452 printf "done!"; 453 [%expect.unreachable] 454[@@expect.uncaught_exn 455 {| 456 (Failure nope) 457 Trailing output 458 --------------- 459 4 460 |}] 461;; 462``` 463 464Unlike `[%expect]` blocks, which might be reached on some runs of a test and not others, a 465test with an `[@@expect.uncaught_exn]` attribute _must_ raise every time it is run. 466Changing the `None` branch of the functorized test from [before](#unreached-expects) to 467raise gives: 468 469<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml.corrected.expected,part=sometimes-raises --> 470```ocaml 471module Test' (B : sig 472 val interesting_opt : int option 473 end) = 474struct 475 let%expect_test "sometimes raises" = 476 match B.interesting_opt with 477 | Some x -> 478 printf "%d\n" x; 479 [%expect {| 5 |}] 480 | None -> failwith "got none!" 481 [@@expect.uncaught_exn 482 {| 483 (* expect_test: Test ran multiple times with different uncaught exceptions *) 484 =============================== Output 1 / 3 ================================ 485 <expect test ran without uncaught exception> 486 =============================== Output 2 / 3 ================================ 487 (Failure "got none!") 488 =============================== Output 3 / 3 ================================ 489 <expect test ran without uncaught exception> 490 |}] 491 ;; 492end 493 494module _ = Test' (struct 495 let interesting_opt = Some 5 496 end) 497 498module _ = Test' (struct 499 let interesting_opt = None 500 end) 501 502module _ = Test' (struct 503 let interesting_opt = Some 5 504 end) 505``` 506 507 508# Output capture 509 510The extension point `[%expect.output]` evaluates to a `string` with the output that would 511have been captured had an `[%expect]` node been there instead. 512 513One idiom for testing non-deterministic output is to capture the output using 514`[%expect.output]` and post-process it: 515 516<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml.corrected.expected,part=output-capture --> 517```ocaml 518(* Suppose we want to test code that attaches a timestamp to everything it prints *) 519let print_message s = printf "%s: %s\n" (Time_float.to_string_utc (Time_float.now ())) s 520 521let%expect_test "output capture" = 522 (* A simple way to clean up the non-determinism is to 'X' all digits *) 523 let censor_digits s = String.map s ~f:(fun c -> if Char.is_digit c then 'X' else c) in 524 print_message "Hello"; 525 [%expect.output] |> censor_digits |> print_endline; 526 [%expect {| XXXX-XX-XX XX:XX:XX.XXXXXXZ: Hello |}]; 527 print_message "world"; 528 [%expect.output] |> censor_digits |> print_endline; 529 [%expect {| XXXX-XX-XX XX:XX:XX.XXXXXXZ: world |}] 530;; 531``` 532 533Other uses of `[%expect.output]` include: 534 535* Sorting lines of output printed in nondeterministic order. 536* Passing output that is known to be a sexp to `t_of_sexp` and performing tests on the 537 resulting structure. 538* Performing some sort of additional validation on the output before printing it to a 539 normal `[%expect]` block. 540 541# Configuration 542 543Expect-test exposes hooks for configuring how the bodies of expect tests are run, which 544can be used to set up and tear down test environments, sanitize output, or embed 545`[%expect]` expressions in a monadic computation, like a `Deferred.t`. 546 547Each `let%expect_test` reads these configurations from the module named 548`Expect_test_config` in the scope of that let binding. The default module in scope defines 549no-op hooks that the user can override. To do so, first include the existing 550`Expect_test_config`, then override a subset of the following interface: 551 552```ocaml 553module type Expect_test_config = sig 554 (** The type of the expression on the RHS of a [let%expect_test] 555 binding is [unit IO.t] *) 556 module IO : sig 557 type 'a t 558 559 val return : 'a -> 'a t 560 end 561 562 (** Run an IO operation until completion *) 563 val run : (unit -> unit IO.t) -> unit 564 565 (** [sanitize] can be used to map all output strings, e.g. for cleansing. *) 566 val sanitize : string -> string 567 568 (** This module type actually contains other definitions, but they 569 are for internal testing of [ppx_expect] only. *) 570end 571``` 572 573For example, `Async` exports an `Expect_test_config` equivalent to: 574 575```ocaml skip 576module Expect_test_config = struct 577 include Expect_test_config 578 579 module IO = Async_kernel.Deferred 580 581 let run f = Async_unix.Thread_safe.block_on_async_exn f 582end 583``` 584 585If we want to consistently apply the same sanitization to all of the output in our expect 586test, like we did in the timestamp example above, we can override 587`Expect_test_config.sanitize`. This cleans up the testing code and removes the need to use 588`[%expect.output]`. 589 590<!-- $MDX file=./test/negative-tests/for-mdx/mdx_cases.ml.corrected.expected,part=sanitization --> 591```ocaml 592(* Suppose we want to test code that attaches a timestamp to everything it prints *) 593let print_message s = printf "%s: %s\n" (Time_float.to_string_utc (Time_float.now ())) s 594 595module Expect_test_config = struct 596 include Expect_test_config 597 598 (* A simple way to clean up the non-determinism is to 'X' all digits *) 599 let sanitize s = String.map s ~f:(fun c -> if Char.is_digit c then 'X' else c) 600end 601 602let%expect_test "sanitization" = 603 print_message "Hello"; 604 [%expect {| XXXX-XX-XX XX:XX:XX.XXXXXXZ: Hello |}]; 605 print_message "world"; 606 [%expect {| XXXX-XX-XX XX:XX:XX.XXXXXXZ: world |}] 607;; 608``` 609 610# Debugging deadlocks & hanging tests 611 612 613One common pitfall while using expect-test is that it hurts the inspectability of code 614that fails to terminate. Normally, adding prints to understand how code behaves interacts 615well with expect-test; when the tests finish, the test framework produces a corrected 616file, where the output has been inserted to the interleaved `[%expect]` extension points. 617However, if a test never finishes, no corrected file is produced at all, and all of the 618useful debug output produced by the program seemingly vanishes into oblivion. 619 620As of December 2024, it is possible to see the output collected by a running expect-test 621program in real-time using the `-verbose` flag[^1]. 622 623For example, the output of the following OCaml program (`test_loops.ml`) 624 625```ocaml skip 626let rec loop () = loop () 627 628let%expect_test "doesn't finish" = 629 print_endline "about to enter an infinite loop"; 630 loop () 631;; 632``` 633 634can be seen by running 635 636 637``` 638$ ./inline_tests_runner -verbose -only-test test_loops.ml 639File "test_loops.ml", line X, characters X-X: doesn't finish 640about to enter an infinite loop 641``` 642 643despite the fact that the test never finishes or produces a corrected file. 644 645When `-verbose` is used, running the `inline_tests_runner` executable to completion 646still produces a corrected file, so the `-verbose` flag need not be incompatible with 647normal workflows. 648 649 650 651# Build system integration 652 653Follow the same rules as for 654[ppx_inline_test](https://github.com/janestreet/ppx_inline_test?tab=readme-ov-file#building-and-running-the-tests-outside-of-jane-street-with-dune). 655 656 657[^1]: This behavior is only supported on `inline_tests_runner` executables built for 658 native code on Linux systems. It is not supported on Windows or `inline_tests_runner`s 659 compiled to WASM or javascript.