executable-stories-rust
Rust adapter for executable-stories.
Provides a Story API for Rust tests and writes raw story metadata for downstream report generation.
Install
The crate name is executable-stories, so the import path is executable_stories (hyphens become underscores):
[]
= "0.1"
Or: cargo add --dev executable-stories.
Usage
use Story;
Status
The story records pass or fail on drop. A failing assertion panics, and a
story dropped mid-unwind records fail, so passing tests need no extra call.
One case escapes that: a #[test] returning Result fails by returning Err,
which never panics. Wrap the fallible call so the story sees it:
story.fail() sets the status directly if you would rather branch yourself.
Features
- BDD steps and aliases on
Story - Feature declarations:
declare_feature!at module scope heads every scenario in the file - Planned scenarios:
Story::planned("…")records behaviour that is specified but not built - Rich docs through
DocEntry:note,tag,kv,json_doc,code,table,link,section,mermaid,screenshot,video,html,state,custom - Timing helpers (
start_timer/end_timer) - Optional OTel trace integration (
otelfeature) with trace URL templates - Raw run writer API for formatter compatibility
OTel methods and trace-URL templating are gated behind the optional otel Cargo feature. Without it, those methods compile as no-ops. Enable with executable-stories = { version = "0.1", features = ["otel"] }.
State snapshots
story.state(label, value) captures a JSON-serializable snapshot of "what the world looks like" at the current step. Consecutive snapshots with the same label are diffed by the report renderer (storyboard frames); the adapter only serializes and emits. Pass None for an anonymous state lane (the label field is omitted).
story.given;
story.state;
story.when;
story.state;
Output
The first Story registers a process-exit hook, so .executable-stories/raw-run.json
lands under the project root after the last test in the binary finishes. Nothing to wire
up. Call write_results() directly to control when the file is written, and set
EXECUTABLE_STORIES_OUTPUT to change the path: a relative path resolves against the
project root, an absolute one is used as given. The file is renamed into place, so a
reader never sees a half-written run.
Cargo compiles every file under tests/ into its own binary, and each one writes the same
path — as do doctests, which rustdoc runs as processes of its own. Keep story tests in a
single file, or give each binary its own EXECUTABLE_STORIES_OUTPUT. Formatting the raw run
updates one canonical report per source under reports/by-file/.
Each run also records what produced it:
startedAtMs/finishedAtMs— when the binary ran, which is what stamps a scenario's freshness in the reportgitSha— the commit the run describes, from CI's environment orgit rev-parse HEADpackageVersion— the adapter version that wrote the runrunScope—"filtered"when the test binary's arguments narrowed the run with a positional name filter,--skip, or--ignored; otherwise"full"
Rust exposes no assertion counter. Use story.expect_step("claim", || { ... }) to
declare assertion evidence. A plain story.then() followed by assert! remains
unobserved rather than being treated as zero.
Then feed the raw-run JSON to executable-stories-formatters for report generation:
# The path is optional — `format` defaults to .executable-stories/raw-run.json
# Diagnose the run JSON if a report won't generate (schema drift, empty run)
Verify
From repo root: