gleedoc
A doc test library for Gleam, inspired by Rust and Elixir’s doctest tooling.
Doc tests let you write executable examples in your documentation comments (///).
These examples are extracted, compiled, and run as part of your test suite, ensuring your documentation never goes out of date.
Disclaimer: This project contains many LLM-generated code, and I used LLMs to do research and design. But I (as a Gleam amateur) have tried my best to review line by line, adjust and refactor.
How it works
- Extract
///doc comments from your.gleamsource files. - Find fenced code blocks tagged with
gleaminside those comments. - Generate test modules in your
test/directory. - Run the generated tests with
gleam test.
How other languages do it
| Language | Approach | Key Difference from Gleam |
|---|---|---|
| Rust | cargo test compiles ```rust blocks from /// comments. No REPL needed. | Gleam follows this model closely. |
| Elixir | doctest Module parses iex> prompts from @doc strings. | Elixir has a REPL; Gleam does not. |
| Python | doctest parses >>> prompts from docstrings. | Python is interpreted; Gleam is compiled. |
Because Gleam is a compiled language with no built-in REPL, gleedoc adopts Rust’s approach: doc blocks are treated as standalone Gleam code that gets compiled and executed. If a block panics, the test fails.
Installation
gleam add gleedoc --dev
Usage
Write doc comments with gleam code blocks in your source files:
// src/math.gleam
/// Adds two numbers together.
///
/// ```gleam
/// let result = add(1, 2)
/// assert result == 3
/// ```
pub fn add(a: Int, b: Int) -> Int {
a + b
}
Then run gleedoc to generate tests:
gleam run -m gleedoc
This creates test/gleedoc/math_gleedoc_test.gleam containing:
// Generated by gleedoc - do not edit manually
import math.{add}
// From: src/math.gleam:4
pub fn add_1_test() {
let result = add(1, 2)
assert result == 3
}
Now run your tests as usual:
gleam test
Imports in generated tests
Each generated test file receives imports from three sources, merged and deduplicated automatically:
- The source module itself —
gleedocscans the module’s public names withglanceand generates a list of unqualified imports that include all public functions/types/constants, so you can call functions directly in your snippets. - The source module’s own top-level imports — any
importstatements at the top of the source file are carried over, so your snippets can use the same types and helpers the module itself uses without restating them. - Imports written inside the code block — you can always add an explicit
importline inside a snippet for anything extra.
For example, given this source file src/user.gleam:
import gleam/option.{type Option} // 1️⃣
/// Returns a greeting for the user.
///
/// ```gleam
/// import gleam/option.{Some} // 2️⃣
///
/// let name = Some("Alice")
/// assert greet(name) == "Hello, Alice!"
/// ```
pub fn greet(name: Option(String)) -> String { // 3️⃣
name
|> option.map(fn(n) { "Hello, " <> n <> "!" })
|> option.unwrap("")
}
The generated test file fixtures_user_gleedoc_test.gleam will contain imports merged from all three sources:
// Generated by gleedoc - do not edit manually
import fixtures/user.{greet} // source module public definitions (3️⃣)
import gleam/option.{type Option, Some} // source module imports (1️⃣) + gleam code block imports (2️⃣)
// From: test/fixtures/user.gleam:5
pub fn greet_1_test() {
let name = Some("Alice")
assert greet(name) == "Hello, Alice!"
}
If the same module is imported in multiple places (e.g. gleam/option appears in both the source file and a code block), the unqualified names from all of them are merged into a single import line.
There are more examples in
test/fixturesandtest/integration/gleedoc.
API
You can also use gleedoc programmatically from your test suite:
// test/gleedoc_setup.gleam
import gleedoc
pub fn main() {
let config = gleedoc.GleedocConfig(
output_dir: "test",
source_dir: "src",
)
case gleedoc.run(config) {
Ok(Nil) -> Nil
Error(snag) -> panic as snag.issue
}
}
Architecture
src/
gleedoc.gleam # Main entry point and CLI
gleedoc/
extract.gleam # Line-based doc comment extraction
parse.gleam # Markdown code block parsing
generate.gleam # Test file generation
scan.gleam # Public names and imports extraction with glance
Key dependencies
| Package | Role |
|---|---|
glance | Gleam source parser |
simplifile | Cross-target file I/O |
snag | Lightweight error handling |
Development
gleam run -m prepare_tests && gleam test
❗ Know issue: Some integration tests will fail on Windows due to line break incompatibility.
Contributing
Please kindly create an issue in your human voice, describe the feature request or bug clearly with reproduction steps, and ideally with a proposed solution before creating any PR.
Roadmap
Implemented
-
Extract
///doc comments from source files -
Parse
```gleamfenced code blocks -
Generate compatible
gleeunittest files - Multi-file source discovery
-
Use
glanceto extract public names for unqualified imports -
Cross-module imports:
importstatements in code blocks are merged inside the generated tests -
Single-command
gleam run -m gleedocCLI experience
Missing Features (compared to Rust, Elixir, and Python)
| Feature | Rust | Elixir | Python | gleedoc |
|---|---|---|---|---|
ignore / skip attribute | ✅ | ✅ | ✅ | ❌ |
no_run (compile only) | ✅ | ❌ | ❌ | ❌ |
should_panic | ✅ | ❌ | ❌ | ❌ |
Hidden setup lines (#) | ✅ | ❌ | ❌ | ❌ |
Output assertions (// ->) | ❌ | ✅ (iex>) | ✅ (>>>) | ❌ |
| Module-level doc tests | ✅ (//!) | ✅ | ✅ | ❌ |
compile_fail | ✅ | ❌ | ❌ | ❌ |
Multi-target (erlang / javascript) | ✅ (cfg) | ❌ | ❌ | ❌ |
| Incremental / cached generation | ✅ | ✅ | ✅ | ❌ |
| Source-mapped error reporting | ✅ | ✅ | ✅ | ❌ |
❗ Know Issues
-
Doesn’t work on Windows due to different path separators -
Generated tests will contain unused imports - Test file generation is not OS-agnostic (some types of tests would fail on Windows)