Open-Source Wikis

/

Starship

/

How to contribute

/

Testing

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_format

CI 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-ignored

Where 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/ — bundled git-repo.bundle and hg-repo.bundle files that are extracted into tempdirs 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). None means "this command failed / doesn't exist".
  • claude_code_data(ClaudeCodeData) — for Claude statusline tests.
  • keymap(...) / status_code(...) / pipestatus(...) / cmd_duration(...) — set Properties fields.
  • shell(Shell::Bash) — set the active shell.
  • collect() — run the module and return Option<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_cmdCONTRIBUTING.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 tests

If 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

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Testing – Starship wiki | Factory