🥇 ouro 🥇
A golden test runner for language hackers. Embed test expectations directly in your source files as comments — no separate fixture files, no test harness boilerplate.
test tests/golden/simple.myc ... ok
test tests/golden/errors.myc ... ok
test tests/golden/multiline.myc ... ok
test result: ok. 3 passed; 0 failed
How it works
ouro runs a binary (your compiler, interpreter, or language tool) against each test file and compares the output to expectations written in the file's own comments.
// args: --optimize
// out: 42
// :out
let x = 42
console.log
Directives live in comment lines starting with a configurable prefix (// by default). Everything else is source code passed to your binary.
Binary invocation
For each test file ouro calls:
<binary> [args...] <test-file-path>
The test file path is always the last argument. args come from args: directives in the file. If there are none, ouro calls <binary> <test-file-path> with no extra flags.
Directives
| Directive | Form | Meaning |
|---|---|---|
out: <text> |
inline | Entire stdout must equal <text> |
out: / :out |
block | Multi-line stdout expectation |
err: <text> |
inline | Entire stderr must equal <text> |
err: / :err |
block | Multi-line stderr expectation |
args: <flags> |
inline | Append shell-split flags to args (can repeat) |
args: / :args |
block | Multi-line args, one arg per line |
exit: <n> |
inline | Expected exit code (default: 0) |
Omitting out: or err: means that stream is not checked at all.
Comparison
Trailing newlines are trimmed from both sides before comparing. Everything else is an exact match — no whitespace normalization, no regex.
Inline shorthand
# args: --run
# out: hello world
# exit: 0
Multi-line block
// out:
// ; optimized output
// mov rax, 42
// ret
// :out
Block content can contain anything — including }, //, or other tokens from your language — without ambiguity.
Setup
1. Add to Cargo.toml
[]
= "0.1"
2. Create ouro.toml in your project root
= "target/debug/myc" # path to your binary
= "tests/**/*.myc" # glob of test files
= "// " # comment prefix (default: "// ")
3. Write a test
// tests/golden.rs
Or use the builder directly:
4. Run
cargo test golden
CLI
Install the ouro binary with:
cargo install ouro --features binary
ouro [OPTIONS]
ouro llm-context
--binary <PATH> Binary to test
--files <GLOB> Test file glob [default: tests/**/*]
--prefix <STR> Comment prefix [default: "// "]
--update Overwrite expected output with actual
--jobs <N> Parallel workers [default: num CPUs]
--config <PATH> Path to ouro.toml [default: search upward from CWD]
Exit 0 if all tests pass, 1 if any fail.
ouro llm-context prints a compact plain-text spec of directives, invocation contract, comparison rules, and the library API — suitable for pasting into an LLM context window.
Updating expectations
When your output intentionally changes, regenerate all expected values in one step:
ouro --update
This rewrites the directive lines in each test file with the actual output from your binary. Review the diff with git diff, then commit.
Changing the comment prefix
For languages with a different comment syntax, set prefix in ouro.toml or via --prefix:
# Python / Ruby / shell
= "# "
Haskell
= "-- "
; Assembly / .ini
= "; "
Parallelism
Tests run in parallel by default using Rayon. Control the thread count:
# ouro.toml
= 4
ouro --jobs 4
Disable parallelism entirely by depending on ouro without the parallel feature:
= { = "0.1", = false }
Cargo features
| Feature | Default | Description |
|---|---|---|
parallel |
yes | Parallel test execution via Rayon |
binary |
no | Build the ouro CLI binary (implies parallel) |
Development
Prerequisites
- Rust 1.70+ (stable)
Build
cargo build
cargo build --features binary # includes the CLI
Test
cargo test
The test suite includes:
- Unit tests for the parser state machine (
src/parser.rs) - Unit tests for the output rewriter (
src/runner.rs) - Integration tests that run the full suite against a small fake compiler (
tests/integration.rs)
Project layout
src/
lib.rs public API: Suite builder, run(), run_from_cwd()
config.rs ouro.toml parsing, upward config search
patterns.rs PatternSet trait + DefaultPatterns
parser.rs line scanner / state machine → TestCase
runner.rs spawn binary, capture output, compare, --update rewriter
diff.rs colored unified diff output
main.rs CLI entry point (binary feature)
k
tests/
integration.rs end-to-end integration tests
fixtures/myc minimal fake compiler used by integration tests
golden/ golden test files for the integration suite
Adding a new directive
- Add a method to
PatternSetinsrc/patterns.rs - Implement it in
DefaultPatterns - Handle the new state/transition in
src/parser.rs - Add a unit test in the
parser::testsmodule
Releasing
Releases are managed by release-plz. No separate release branch is needed.
How it works
- When a PR is merged to
main, release-plz opens a release PR that bumps the version inCargo.tomland updatesCHANGELOG.md. - When that release PR is merged, release-plz:
- Creates a GitHub release with the generated changelog
- Publishes the crate to crates.io
- The
release.ymlworkflow triggers on the published GitHub release and builds cross-platform binaries (linux-x86_64,macos-x86_64,macos-aarch64,windows-x86_64), attaching them to the release.
Required secrets
Add these in Settings → Secrets and variables → Actions:
| Secret | Where to get it |
|---|---|
CARGO_REGISTRY_TOKEN |
crates.io → New token (scope: publish-new, publish-update) |
GITHUB_TOKEN is provided automatically by GitHub Actions.
Prior art
License
MIT