stet — "let it stand." A proofreading term meaning the original text is correct and should not be changed. In the same way, stet identifies the tests that can stand as-is, and reruns only the ones affected by your changes.
stet is an open-source implementation of the selective test execution system described in Stripe's blog post Fast CI for a 50M-Line Ruby Monorepo. It traces which files each test opens, builds a dependency graph, and uses it to select just the tests impacted by your code changes. It works with Ruby/Minitest, Python/Pytest, and Bun on macOS and Linux.
Research preview.
stetis functional and tested but is not yet battle-tested in production. APIs, config format, and on-disk storage may change without notice. Feedback and contributions are welcome.
-
Record —
stetruns your test suite with a shared library injected viaDYLD_INSERT_LIBRARIES(macOS) orLD_PRELOAD(Linux) that intercepts file-open syscalls. This produces a map of which source files each test depends on. -
Select — When files change,
stetdiffs the repo inventory against the recorded dependency graph and identifies which tests are affected. -
Run —
stetexecutes only the affected tests, re-traces their dependencies, and updates the dependency graph for next time. No full re-record needed.
Using the GitHub CLI:
# macOS (Apple Silicon)
gh release download --repo qltysh/stet --pattern 'stet-aarch64-apple-darwin.tar.gz'
tar xzf stet-aarch64-apple-darwin.tar.gz
# Linux (x86_64)
gh release download --repo qltysh/stet --pattern 'stet-x86_64-unknown-linux-gnu.tar.gz'
tar xzf stet-x86_64-unknown-linux-gnu.tar.gzThen add the extracted directory to your PATH. The archive contains both the stet binary and the libstet_interceptor library.
Alternatively, build from source with the Rust toolchain:
git clone https://github.com/qltysh/stet.git
cd stet
bash scripts/setup.shRun stet init from your project root to auto-detect your test runner and generate a config:
stet initThis scans your repo for dependency files (Gemfile, pyproject.toml, package.json/bun.lockb) and test directories, then writes a commented stet.toml with the right settings. Add .stet/ to your .gitignore.
Then record a baseline and start running:
# Record the baseline (runs your full test suite once with tracing)
stet record
# Make a code change, then run just the affected tests
stet runstet init accepts --runner minitest|pytest|bun_test to skip detection, --force to overwrite an existing config, and --config <PATH> for a custom output location.
If you prefer to create stet.toml by hand:
Ruby/Minitest:
test_runner = "minitest"
test_globs = ["test/**/*_test.rb"]
test_args = ["-Itest"]
inventory_excludes = [".git/**", ".stet/**", "tmp/**"]Python/Pytest:
test_runner = "pytest"
test_globs = ["tests/test_*.py", "tests/**/*_test.py"]
test_args = ["-m", "pytest"]
inventory_excludes = [".git/**", ".stet/**", "__pycache__/**", ".pytest_cache/**"]Bun:
test_runner = "bun_test"
test_globs = ["test/**/*.test.ts"]
test_args = ["test"]
inventory_excludes = [".git/**", ".stet/**", "node_modules/**"]state_dir defaults to .stet. runtime is inferred from test_runner (ruby, python3, or bun) and resolved from PATH. Both can be overridden explicitly.
stet looks for stet.toml in the current directory and auto-discovers the interceptor library next to its own binary. Use --config and --interceptor to override.
Auto-detects the test runner from your repo contents and generates a commented stet.toml. Supports --runner to skip detection, --force to overwrite, and --config for a custom output path.
Runs every test with tracing and publishes a new dependency baseline under .stet/bases/. Use this for initial setup or to force a full re-record.
Reads the latest baseline, diffs current file hashes against it, and outputs the list of affected test units. Doesn't run anything.
Selects affected tests, runs them with tracing, and updates the baseline. If no baseline exists, bootstraps by running the full suite. This is the steady-state command for day-to-day development.
Tests that should always run regardless of what changed:
mandatory_test_globs = ["test/health/**/*_test.rb"]A script that runs before tests to generate shared artifacts (e.g., schema files). Changes to files opened by the root preload will select all tests:
root_args = ["-Itest", "test/root_preload.rb"]Run additional commands when specific files change:
[[tool_routes]]
name = "yaml-lint"
changed_file_globs = ["config/**/*.yml"]
command = ["{runtime}", "script/changed_file_yaml_lint.rb", "{changed_files...}"]max_workers = 8 # defaults to available CPU coresTry the interactive demo:
bash scripts/demo-fixture.sh --fixture ruby
bash scripts/demo-fixture.sh --fixture python
bash scripts/demo-fixture.sh --fixture bunRun the test suite:
bash scripts/setup.sh
cargo test --workspaceRun clippy and format checks:
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --checkBenchmark:
bash scripts/benchmark-fixture.sh --fixture ruby
bash scripts/benchmark-fixture.sh --fixture python
bash scripts/benchmark-fixture.sh --fixture bunLinux validation (requires Docker):
bash scripts/validate-linux-docker.shMIT