How It Works
DocTest follows a simple pipeline: parse, generate, execute, compare.
Pipeline Overview
1. Parse
DocTest scans markdown files for fenced PHP code blocks (```php). For each block, it:
- Extracts the raw PHP code
- Parses attributes from the fence info string (e.g.,
ignore,throws,group="name") - Finds associated assertions (HTML comments after the block, or
// =>result comments inside the code) - Strips Shiki-specific markers (
[!code ++],[!code --], line highlights)
2. Generate
Each code block is transformed into a self-contained PHP script with instrumentation:
- Output-capturing assertions get wrapped in
ob_start()/ob_get_clean() - Expression assertions (
Expect) are evaluated and the boolean result is captured - Result comment assertions (
// => value) usevar_exportto compare values - All results are collected and written to
STDERRas JSON
3. Execute
The generated script runs in an isolated PHP process via proc_open:
- Each block gets its own process (no shared state between blocks)
- Configurable timeout (default: 30 seconds)
- Configurable memory limit (default: 256M)
- Exit code and output are captured
Exception: grouped blocks (same group attribute) run in a single process to share state.
4. Compare
The executor evaluates results from the process:
- Output assertions compare captured output against expected values
- Wildcard patterns (
{{int}},{{date}}, etc.) are converted to regex - JSON assertions decode and compare structures (key order doesn't matter)
- Output is normalized (trailing whitespace trimmed, line endings unified)
A diff is generated for failed comparisons.
Process Isolation
Every code block runs in its own PHP process. This means:
- A fatal error in one block doesn't affect others
- Memory leaks are contained
- Each block starts with a clean state
- No implicit dependencies between examples
Grouped Execution
Blocks with matching group attributes execute in a single process:
setupblocks run first (in document order)- Group blocks run in document order, sharing variables
teardownblocks run last
This enables examples that build on each other, like database operations.