starship/starship
Testing
Starship's test suite is a cargo test-driven set of unit tests, almost all colocated with the code they exercise. The key abstraction is ModuleRenderer.
Running tests
# Whole workspace, all features, including ignored tests
cargo test --all-features --locked --workspace -- --include-ignored
# Just the smoke run (skips ignored)
cargo test --all-features --locked --workspace
# A single module's tests
cargo test --all-features rust::tests
# A single test by exact name
cargo test --all-features test_correct_version_formatCI runs cargo llvm-cov to collect coverage and uploads lcov.info to Codecov. You can replicate locally:
cargo install cargo-llvm-cov
cargo llvm-cov --all-features --locked --workspace -- --include-ignoredWhere tests live
Each module has a #[cfg(test)] mod tests { ... } block at the bottom of its src/modules/<name>.rs file. Top-level tests for the engine sit in src/print.rs, src/config.rs, etc., next to the code they cover.
Two notable exceptions:
src/test/mod.rs— the test harness (ModuleRenderer,default_context, fixture loading). Not a test file itself.src/test/fixtures/— bundledgit-repo.bundleandhg-repo.bundlefiles that are extracted intotempdirs for VCS tests.
ModuleRenderer
The harness everyone uses. From src/test/mod.rs:
let actual = ModuleRenderer::new("rust")
.path(&tempdir.path()) // working dir
.config(toml::toml!{ // override TOML config
[rust]
format = "[$version]($style)"
})
.env("RUST_VERSION", "1.85.0") // mock env vars
.cmd("rustc --version", Some(CommandOutput { // mock external commands
stdout: "rustc 1.85.0 (xyz 2024-12-13)".into(),
stderr: String::new(),
}))
.collect();
assert_eq!(actual, Some("1.85.0".to_string()));Available builders:
path/logical_path— set the cwd / logical cwd.config(toml::Table)— provide a config TOML for this run.env(key, value)— inject an env var.cmd(literal_command, Option<CommandOutput>)— mock a command. The literal command must exactly match what the module would call (including positional args).Nonemeans "this command failed / doesn't exist".claude_code_data(ClaudeCodeData)— for Claude statusline tests.keymap(...)/status_code(...)/pipestatus(...)/cmd_duration(...)— setPropertiesfields.shell(Shell::Bash)— set the active shell.collect()— run the module and returnOption<String>.
Test patterns
Detection check
#[test]
fn folder_without_rust_files() -> std::io::Result<()> {
let dir = tempfile::tempdir()?;
let actual = ModuleRenderer::new("rust").path(dir.path()).collect();
assert_eq!(actual, None);
dir.close()
}Mocking a binary
#[test]
fn rust_with_pinned_toolchain() -> std::io::Result<()> {
let dir = tempfile::tempdir()?;
File::create(dir.path().join("Cargo.toml"))?.sync_all()?;
File::create(dir.path().join("rust-toolchain.toml"))?
.write_all(br#"[toolchain]\nchannel = "1.85""#)?;
let actual = ModuleRenderer::new("rust")
.path(dir.path())
.cmd("rustup which rustc",
Some(CommandOutput { stdout: "/x/bin/rustc".into(), stderr: "".into() }))
.cmd("rustc -vV",
Some(CommandOutput { stdout: "rustc 1.85.0\n".into(), stderr: "".into() }))
.collect();
let expected = Some(format!("{} ", Color::Red.bold().paint("via 🦀 v1.85.0")));
assert_eq!(actual, expected);
dir.close()
}Git fixture tests
use crate::test::fixture_repo;
#[test]
fn git_branch_on_real_repo() -> std::io::Result<()> {
let repo_dir = fixture_repo(FixtureProvider::Git)?;
let actual = ModuleRenderer::new("git_branch").path(repo_dir.path()).collect();
// assert ...
repo_dir.close()
}fixture_repo extracts the bundled git/hg fixture into a tempdir so each test gets a fresh repo. Always .close() the returned dir.
Tests that need real binaries
If your test absolutely needs a binary on PATH (hg, fossil, real git), mark it #[ignore]:
#[test]
#[ignore]
fn hg_branch_real_binary() {
// ...
}CI runs ignored tests via --include-ignored after installing the dependency in .github/workflows/workflow.yml. Locally, run cargo test -- --include-ignored to include them.
Mocked commands list
crate::utils::mock_cmd in src/utils/mod.rs has a built-in match table for "well-known" command outputs (e.g., rustc --version, node --version). When a test doesn't override a command via .cmd(...), the harness falls back to this table. If you add a new context.exec_cmd(...) call in a module, you must also add a matching mock in mock_cmd — CONTRIBUTING.md calls this out explicitly.
File-system isolation
Tests should never touch the user's home directory or any path under /. The Context struct (in cfg(test) mode) carries a tempfile::TempDir as root_dir, and the context_path helper rewrites absolute paths into paths under that root:
let p = context_path(context, "/run/test/x"); // -> $TEMPDIR/run/test/x in testsIf a test creates files inside the tempdir, always call .sync_all() after creation — Windows is fussy about file visibility otherwise.
Reproducibility
Tests should be deterministic. Avoid:
- depending on the host's git config (set the environment vars instead),
- relying on the user having any particular binary on PATH,
- assuming the home directory is or isn't a git repo,
- timing-dependent assertions (use mocks for
cmd_duration).
Even seemingly innocuous assumptions ("nobody will have their home directory be a git repo") have caused regressions in the past — the comment in CONTRIBUTING.md is there for a reason.
See also
- Patterns and conventions — the module shape that tests assert on.
- Debugging — how to reproduce real-shell behavior locally.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.