Mdexp Test Suite
This book contains the test suite for mdexp, organized as documentation.
Each test file is written in OCaml with mdexp directives, and the corresponding markdown is generated automatically. The tests verify that the examples work correctly, while the generated documentation shows what mdexp produces.
Structure
- Examples - Various mdexp features demonstrated through test files
- Toplevel - OCaml toplevel integration for REPL-style documentation
Building
The markdown files are generated by running:
dune runtest
To view this book:
cd repo/mdexp/test
mdbook serve --open
Syntax Highlighting Test
Here’s a Rust code fragment to test if syntax highlighting works:
fn main() {
let message = "Hello, World!";
println!("{}", message);
}
And an OCaml code fragment:
let greet name =
Printf.printf "Hello, %s!\n" name
let () = greet "World"
String Utilities for OCaml
A collection of common string manipulation functions.
Core Functions
capitalize
Capitalize the first character of a string:
let capitalize s =
if String.length s = 0
then s
else (
let first = String.uppercase_ascii (String.sub s ~pos:0 ~len:1) in
let rest = String.sub s ~pos:1 ~len:(String.length s - 1) in
first ^ rest)
;;
split_on_char
Split a string on a given character:
let split_on_char sep s =
let rec aux acc start len =
if start >= String.length s
then if len > 0 then String.sub s ~pos:(start - len) ~len :: acc else acc
else if s.[start] = sep
then (
let word = if len > 0 then [ String.sub s ~pos:(start - len) ~len ] else [] in
aux (word @ acc) (start + 1) 0)
else aux acc (start + 1) (len + 1)
in
List.rev (aux [] 0 0)
;;
Usage Examples
Let’s see how these functions work in practice.
Testing capitalize
First, we test the basic case of capitalizing a lowercase word:
let%expect_test "capitalize - basic case" =
let result = capitalize "hello" in
print_endline result;
[%expect {| Hello |}]
;;
Empty strings should be handled gracefully:
let%expect_test "capitalize - empty string" =
let result = capitalize "" in
print_endline result;
[%expect {| |}]
;;
Already capitalized strings should remain unchanged:
let%expect_test "capitalize - already capitalized" =
let result = capitalize "WORLD" in
print_endline result;
[%expect {| WORLD |}]
;;
Testing split_on_char
The function correctly splits comma-separated values:
let%expect_test "split_on_char - comma separated" =
let words = split_on_char ',' "a,b,c" in
List.iter words ~f:print_endline;
[%expect
{|
a
b
c |}]
;;
Splitting an empty string returns an empty list:
let%expect_test "split_on_char - empty string" =
let words = split_on_char ',' "" in
print_endline (string_of_int (List.length words));
[%expect {| 0 |}]
;;
It works with different separators like spaces:
let%expect_test "split_on_char - space separated" =
let words = split_on_char ' ' "hello world" in
List.iter words ~f:print_endline;
[%expect
{|
hello
world |}]
;;
Consecutive separators produce empty segments that are skipped:
let%expect_test "split_on_char - consecutive separators" =
let words = split_on_char ',' "a,,b" in
List.iter words ~f:print_endline;
[%expect
{|
a
b
|}]
;;
Literate Example: Complex String Processing
This example demonstrates a more complex use case where we combine both functions to process a formatted string. We’ll alternate between explanation and code to create a narrative flow.
First, let’s start with a comma-separated list that needs processing:
let input = "alice,bob,charlie" in
print_endline ("Input: " ^ input);
[%expect {| Input: alice,bob,charlie |}];
Now we split the string into individual names:
let names = split_on_char ',' input in
print_endline ("Found " ^ string_of_int (List.length names) ^ " names");
[%expect {| Found 3 names |}];
Next, we capitalize each name to ensure proper formatting:
let capitalized_names = List.map names ~f:capitalize in
List.iter capitalized_names ~f:(fun name -> print_endline ("- " ^ name));
[%expect
{|
- Alice
- Bob
- Charlie |}];
Finally, let’s verify that our processing worked correctly by checking the first and last names:
let first_name = List.hd capitalized_names in
let last_name = List.hd (List.rev capitalized_names) in
print_endline ("First: " ^ first_name ^ ", Last: " ^ last_name);
[%expect {| First: Alice, Last: Charlie |}];
Testing Block Comment Closing Behavior
This test demonstrates the various ways to end mdexp prose blocks in OCaml.
Closing with standalone marker
This paragraph has the closing marker on its own line below.
Explicit closing with @mdexp.end
This paragraph uses an explicit end directive.
Directive followed by closing
This demonstrates the common pattern: end prose with a directive.
let (_ : string) = "Hello, World!"
Single-Line Directives Test (OCaml)
This is a single-line block comment with inline prose.
Multi-Line Block with Initial Content
This block started with “## Multi-Line Block…” as initial content, and continues on subsequent lines.
Single-Line Block Comment
Multi-Line with Initial Content
This continues on the next line because the block comment didn’t close on the first line.
Code Block with Blank Comment Lines Test (OCaml)
This test verifies that blank comment lines within code blocks don’t prematurely end the block in OCaml files.
Function with blank lines in comments
The following code has blank comment lines that should be preserved:
let calculate x y =
let sum = x + y in
let result = sum * 2 in
result
Multiple functions with blank lines
let first () =
let a = 1 in
let b = 2 in
a + b
let second () =
let c = 3 in
c
Snapshot Block Mode Test
This tests snapshot extraction with block mode enabled.
print_endline "single line without newline";
Another section to verify spacing.
print_endline "another single line";
Table Examples (OCaml)
This file demonstrates creating markdown tables using the print-table library with ppx_expect snapshot testing in OCaml.
Basic Data Types
We’ll use some simple data structures to demonstrate table rendering.
Simple People Table
A basic table showing people with their ages and cities.
let%expect_test "people table - markdown format" =
let open Print_table.O in
let columns : person Column.t list =
[ Column.make ~header:"Name" (fun p -> Cell.text p.person_name)
; Column.make ~header:"Age" ~align:Right (fun p -> Cell.text (Int.to_string p.age))
; Column.make ~header:"City" (fun p -> Cell.text p.city)
]
in
let rows =
[ { person_name = "Alice"; age = 30; city = "New York" }
; { person_name = "Bob"; age = 25; city = "London" }
; { person_name = "Charlie"; age = 35; city = "Tokyo" }
]
in
let print_table = Print_table.make ~columns ~rows in
let output = Print_table.to_string_markdown print_table in
print_endline output;
| Name | Age | City |
|---|---|---|
| Alice | 30 | New York |
| Bob | 25 | London |
| Charlie | 35 | Tokyo |
Product Inventory Table
A table displaying product information with prices and stock levels.
let%expect_test "product inventory - markdown format" =
let open Print_table.O in
let columns =
[ Column.make ~header:"Product" (fun p -> Cell.text p.product_name)
; Column.make ~header:"Price" ~align:Right (fun p ->
Cell.text (Printf.sprintf "$%.2f" p.price))
; Column.make ~header:"Stock" ~align:Center (fun p ->
Cell.text (Int.to_string p.stock))
]
in
let rows =
[ { product_name = "Widget"; price = 19.99; stock = 150 }
; { product_name = "Gadget"; price = 29.50; stock = 75 }
; { product_name = "Doohickey"; price = 5.25; stock = 300 }
]
in
let print_table = Print_table.make ~columns ~rows in
let output = Print_table.to_string_markdown print_table in
print_endline output;
| Product | Price | Stock |
|---|---|---|
| Widget | $19.99 | 150 |
| Gadget | $29.50 | 75 |
| Doohickey | $5.25 | 300 |
Alignment Examples
Demonstrating different column alignments (left, center, right).
type align_demo =
{ left : string
; center : string
; right : string
}
let%expect_test "alignment demonstration - markdown format" =
let open Print_table.O in
let columns =
[ Column.make ~header:"Left" ~align:Left (fun d -> Cell.text d.left)
; Column.make ~header:"Center" ~align:Center (fun d -> Cell.text d.center)
; Column.make ~header:"Right" ~align:Right (fun d -> Cell.text d.right)
]
in
let rows =
[ { left = "A"; center = "B"; right = "C" }
; { left = "Short"; center = "Medium"; right = "Long text" }
; { left = "X"; center = "Y"; right = "Z" }
]
in
let print_table = Print_table.make ~columns ~rows in
let output = Print_table.to_string_markdown print_table in
print_endline output;
| Left | Center | Right |
|---|---|---|
| A | B | C |
| Short | Medium | Long text |
| X | Y | Z |
Edge Cases
Testing edge cases like single-row tables.
type single_row =
{ id : int
; value : string
}
let%expect_test "single row table - markdown format" =
let open Print_table.O in
let columns =
[ Column.make ~header:"ID" ~align:Right (fun r -> Cell.text (Int.to_string r.id))
; Column.make ~header:"Value" (fun r -> Cell.text r.value)
]
in
let rows = [ { id = 42; value = "The Answer" } ] in
let print_table = Print_table.make ~columns ~rows in
let output = Print_table.to_string_markdown print_table in
print_endline output;
| ID | Value |
|---|---|
| 42 | The Answer |
Snapshot Examples
This file demonstrates snapshot extraction with both single-line and multi-line formats.
Single-Line Snapshot
A simple single-line snapshot:
let%expect_test "single-line snapshot" =
let greeting = "Hello, World!" in
print_endline greeting;
Hello, World!
Multi-Line Snapshot
A multi-line snapshot with multiple lines:
let%expect_test "multi-line snapshot" =
let lines = [ "First line"; "Second line"; "Third line" ] in
List.iter ~f:print_endline lines;
First line Second line Third line
OCaml Toplevel Integration
This module provides a way to run OCaml code in a toplevel and capture the output for documentation. This is useful for:
- Documenting code fragments that don’t compile in isolation
- Showing REPL-style interactions with type information
- Demonstrating error messages
- Using libraries without requiring full compilation
Why a Custom Toplevel?
mdexp supports running code blocks in a toplevel via ppx_expect. The toplevel
process is started with an empty environment (env:[]) which suppresses
initialization messages, and kept alive to allow sequential evaluations.
Features:
- Dune integration - The toplevel rebuilds when dependencies change
- Watch mode - Works with
dune build -w - Preloaded libraries - Project libraries are available without
#require - Verified output - Snapshots ensure examples stay correct
Setup
The dune file defines a custom toplevel with preloaded libraries:
(toplevel
(name mdexp_toplevel)
(libraries mdexp_stdlib))
The test library depends on the toplevel executable and uses unix:
(library
(name mdexp_toplevel_test)
(inline_tests
(deps mdexp_toplevel.exe))
(libraries mdexp_stdlib unix)
...)
Implementation
The wrapper starts a toplevel process with an empty environment, which suppresses all initialization messages. The process is kept alive across evaluations, allowing sequential code blocks to share definitions.
Examples
Basic Evaluation
Run simple OCaml expressions and see their values with type information:
let x = 1 + 1;;
val x : int = 2
Side Effects
Code with side effects shows both the output and the return value:
print_endline "Hello, World!";;
Hello, World!
- : unit = ()
Type Errors
Error messages include location information, useful for explaining what goes wrong with invalid code:
let x = 1 + "hello";;
[1mLine 1, characters 12-19[0m:
1 | let x = 1 + "hello";;
[1;31m^^^^^^^[0m
[1;31mError[0m: This constant has type [1mstring[0m but an expression was expected of type
[1mint[0m
Using Preloaded Libraries
The custom toplevel has libraries preloaded. No #require needed:
open Mdexp_stdlib;;
List.map [1;2;3] ~f:(fun x -> x * 2);;
- : int list = [2; 4; 6]
Sequential Evaluations
Definitions from earlier evaluations are available in later ones, since the toplevel process is kept alive:
Type Errors on Stdout
Type errors are reported on stdout by the toplevel, not stderr:
Integration with mdexp
The @mdexp.snapshot directive extracts the output above into the
generated markdown. This creates documentation with verified,
reproducible REPL examples.
To add more libraries to the toplevel, update the dune stanza:
(toplevel
(name my_toplevel)
(libraries my_project_lib))
The toplevel will be rebuilt automatically when dependencies change.