Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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;
NameAgeCity
Alice30New York
Bob25London
Charlie35Tokyo

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;
ProductPriceStock
Widget$19.99150
Gadget$29.5075
Doohickey$5.25300

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;
LeftCenterRight
ABC
ShortMediumLong text
XYZ

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;
IDValue
42The 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:

  1. Dune integration - The toplevel rebuilds when dependencies change
  2. Watch mode - Works with dune build -w
  3. Preloaded libraries - Project libraries are available without #require
  4. 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";;
Line 1, characters 12-19:
1 | let x = 1 + "hello";;
                ^^^^^^^
Error: This constant has type string but an expression was expected of type
         int

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.