use snapbox::{assert_data_eq, cmd::Command, str};
use std::env::consts::EXE_SUFFIX;
const BINS: &[&str] = &["forge", "cast", "anvil", "chisel"];
const OPTIONAL_BINS: &[&str] = &["solar"];
mod foundry_bins;
mod installer_script;
mod self_update;
fn foundryup() -> Command {
Command::new(snapbox::cmd::cargo_bin!("foundryup")).env("NO_COLOR", "1")
}
fn write_fake_bin(path: &std::path::Path) {
let source = snapbox::cmd::cargo_bin!("foundryup");
#[cfg(unix)]
std::os::unix::fs::symlink(source, path).unwrap();
#[cfg(windows)]
std::fs::hard_link(source, path).unwrap();
#[cfg(not(any(unix, windows)))]
std::fs::copy(source, path).unwrap();
}
#[test]
fn help() {
foundryup().arg("--help").assert().success().stdout_eq(str![[r#"
The installer for Foundry.
Update or revert to a specific Foundry version with ease.
By default, the latest stable version is installed from built binaries.
Usage: foundryup[EXE] [OPTIONS]
Options:
-U, --update
Update foundryup to the latest version
-r, --repo <REPO>
Build and install from a remote GitHub repo (uses default branch if no other options)
-b, --branch <BRANCH>
Build and install a specific branch
-i, --install <VERSION>
Install a specific version from built binaries (e.g., stable, nightly, 0.3.0)
-l, --list
List installed versions
-u, --use <VERSION>
Use a specific installed version
-p, --path <PATH>
Build and install a local repository
-P, --pr <PR>
Build and install a specific Pull Request
-C, --commit <COMMIT>
Build and install a specific commit
-j, --jobs <JOBS>
Number of CPUs to use for building (default: all)
--cargo-profile <CARGO_PROFILE>
Cargo profile to use for building
[default: release]
--cargo-features <CARGO_FEATURES>
Cargo features to enable for building
-f, --force
Skip SHA verification (INSECURE)
--arch <ARCH>
Install a specific architecture (amd64, arm64)
--platform <PLATFORM>
Install a specific platform (win32, linux, darwin, alpine)
--completions <SHELL>
Generate shell completions
[possible values: bash, elvish, fish, powershell, zsh]
-V, --version
Print version
-h, --help
Print help (see a summary with '-h')
"#]]);
}
#[test]
fn version() {
foundryup().arg("--version").assert().success().stdout_eq(str![[r#"
foundryup [..]
"#]]);
}
#[test]
fn version_short() {
foundryup().arg("-V").assert().success().stdout_eq(str![[r#"
foundryup [..]
"#]]);
}
#[test]
fn version_short_alias() {
foundryup().arg("-v").assert().success().stdout_eq(str![[r#"
foundryup [..]
"#]]);
}
#[test]
fn completions_bash() {
let assert = foundryup().args(["--completions", "bash"]).assert().success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout);
assert_data_eq!(
stdout.as_ref(),
str![[r#"
...
_foundryup()[..]
...
"#]]
);
}
#[test]
fn conflicting_args() {
foundryup().args(["--pr", "123", "--branch", "main"]).assert().failure().stderr_eq(str![[r#"
error: the argument '--pr <PR>' cannot be used with '--branch <BRANCH>'
Usage: foundryup[EXE] --pr <PR>
For more information, try '--help'.
"#]]);
}
#[test]
fn update_conflicts_with_list_and_use() {
foundryup().args(["--list", "--update"]).assert().failure().stderr_eq(str![[r#"
error: the argument '--list' cannot be used with '--update'
Usage: foundryup[EXE] --list
For more information, try '--help'.
"#]]);
foundryup().args(["--use", "v1.0.0", "--update"]).assert().failure().stderr_eq(str![[r#"
error: the argument '--use <VERSION>' cannot be used with '--update'
Usage: foundryup[EXE] --use <VERSION>
For more information, try '--help'.
"#]]);
}
#[test]
fn use_empty_version_errors() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
foundryup()
.env("FOUNDRY_DIR", temp_dir.path().join(".foundry"))
.args(["--use", ""])
.assert()
.failure()
.stderr_eq(str![[r#"
...
[..]no version provided[..]
...
"#]]);
}
#[test]
fn empty_repo_falls_back_to_default() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
foundryup()
.env("FOUNDRY_DIR", temp_dir.path().join(".foundry"))
.args(["--repo", "", "--use", "nonexistent-version"])
.assert()
.failure()
.stderr_eq(str![[r#"
...
[..]version nonexistent-version not installed for foundry-rs/foundry[..]
...
"#]]);
}
#[test]
fn use_nonexistent_version() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
foundryup()
.env("FOUNDRY_DIR", temp_dir.path().join(".foundry"))
.args(["--use", "nonexistent-version"])
.assert()
.failure()
.stderr_eq(str![[r#"
...
[..]version nonexistent-version not installed[..]
...
"#]]);
}
#[test]
fn use_version_normalizes_bare_semver() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
let version_dir = foundry_dir.join("versions/foundry-rs/foundry/v1.5.0");
std::fs::create_dir_all(&version_dir).unwrap();
for bin in BINS {
let bin_path = version_dir.join(format!("{bin}{EXE_SUFFIX}"));
write_fake_bin(&bin_path);
}
foundryup()
.env("FOUNDRY_DIR", &foundry_dir)
.args(["--use", "1.5.0"])
.assert()
.success()
.stderr_eq(str![[r#"
...
[..]use - [..]
...
"#]]);
for &bin in BINS {
let name = format!("{bin}{EXE_SUFFIX}");
assert!(foundry_dir.join("bin").join(&name).exists(), "{name} was not activated");
}
}
#[test]
fn use_version_allows_missing_optional_bin() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
let version_dir = foundry_dir.join("versions/foundry-rs/foundry/v1.5.0");
let bin_dir = foundry_dir.join("bin");
std::fs::create_dir_all(&version_dir).unwrap();
std::fs::create_dir_all(&bin_dir).unwrap();
for bin in BINS {
write_fake_bin(&version_dir.join(format!("{bin}{EXE_SUFFIX}")));
}
for bin in OPTIONAL_BINS {
write_fake_bin(&bin_dir.join(format!("{bin}{EXE_SUFFIX}")));
}
foundryup().env("FOUNDRY_DIR", &foundry_dir).args(["--use", "1.5.0"]).assert().success();
for &bin in BINS {
let name = format!("{bin}{EXE_SUFFIX}");
assert!(bin_dir.join(&name).exists(), "{name} was not activated");
}
for &bin in OPTIONAL_BINS {
let name = format!("{bin}{EXE_SUFFIX}");
assert!(!bin_dir.join(&name).exists(), "{name} should have been cleared");
}
}
#[test]
fn use_version_activates_optional_bin_when_present() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
let version_dir = foundry_dir.join("versions/foundry-rs/foundry/v1.5.0");
std::fs::create_dir_all(&version_dir).unwrap();
for bin in BINS.iter().chain(OPTIONAL_BINS) {
write_fake_bin(&version_dir.join(format!("{bin}{EXE_SUFFIX}")));
}
foundryup().env("FOUNDRY_DIR", &foundry_dir).args(["--use", "1.5.0"]).assert().success();
for &bin in OPTIONAL_BINS {
let name = format!("{bin}{EXE_SUFFIX}");
assert!(foundry_dir.join("bin").join(&name).exists(), "{name} was not activated");
}
}
#[test]
fn use_version_digit_prefixed_name_is_literal() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
let version_dir = foundry_dir.join("versions/foundry-rs/foundry/123abc");
std::fs::create_dir_all(&version_dir).unwrap();
for bin in BINS {
let bin_path = version_dir.join(format!("{bin}{EXE_SUFFIX}"));
write_fake_bin(&bin_path);
}
foundryup()
.env("FOUNDRY_DIR", &foundry_dir)
.args(["--use", "123abc"])
.assert()
.success()
.stderr_eq(str![[r#"
...
[..]use - [..]
...
"#]]);
for &bin in BINS {
let name = format!("{bin}{EXE_SUFFIX}");
assert!(foundry_dir.join("bin").join(&name).exists(), "{name} was not activated");
}
}
#[test]
fn use_version_finds_custom_repo_without_repo_flag() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
let version_dir = foundry_dir.join("versions/someone/foundry/someone-branch-x");
std::fs::create_dir_all(&version_dir).unwrap();
for bin in BINS {
let bin_path = version_dir.join(format!("{bin}{EXE_SUFFIX}"));
write_fake_bin(&bin_path);
}
foundryup()
.env("FOUNDRY_DIR", &foundry_dir)
.args(["--use", "someone-branch-x"])
.assert()
.success()
.stderr_eq(str![[r#"
...
[..]use - [..]
...
"#]]);
for &bin in BINS {
let name = format!("{bin}{EXE_SUFFIX}");
assert!(foundry_dir.join("bin").join(&name).exists(), "{name} was not activated");
}
}
#[test]
fn use_version_ambiguous_across_repos_errors() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
for repo in ["alice/foundry", "bob/foundry"] {
let version_dir = foundry_dir.join(format!("versions/{repo}/shared-name"));
std::fs::create_dir_all(&version_dir).unwrap();
for bin in BINS {
write_fake_bin(&version_dir.join(format!("{bin}{EXE_SUFFIX}")));
}
}
foundryup()
.env("FOUNDRY_DIR", &foundry_dir)
.args(["--use", "shared-name"])
.assert()
.failure()
.stderr_eq(str![[r#"
...
[..]installed for multiple repos[..]
...
"#]]);
}
#[cfg(unix)]
#[test]
fn use_version_missing_bin_leaves_previous_active() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
let good_dir = foundry_dir.join("versions/foundry-rs/foundry/v1.0.0");
let broken_dir = foundry_dir.join("versions/foundry-rs/foundry/v2.0.0");
std::fs::create_dir_all(&good_dir).unwrap();
std::fs::create_dir_all(&broken_dir).unwrap();
for bin in BINS {
write_fake_bin(&good_dir.join(format!("{bin}{EXE_SUFFIX}")));
}
for bin in BINS.iter().filter(|b| **b != "chisel") {
write_fake_bin(&broken_dir.join(format!("{bin}{EXE_SUFFIX}")));
}
foundryup().env("FOUNDRY_DIR", &foundry_dir).args(["--use", "1.0.0"]).assert().success();
foundryup()
.env("FOUNDRY_DIR", &foundry_dir)
.args(["--use", "2.0.0"])
.assert()
.failure()
.stderr_eq(str![[r#"
...
[..]binary chisel not found[..]
...
"#]]);
let bin_dir = foundry_dir.join("bin");
for &bin in BINS {
let active = bin_dir.join(format!("{bin}{EXE_SUFFIX}"));
let target = std::fs::read_link(&active)
.unwrap_or_else(|e| panic!("{bin} is not an active symlink: {e}"));
assert_eq!(target, good_dir.join(format!("{bin}{EXE_SUFFIX}")), "{bin} was switched");
}
}
#[cfg(unix)]
#[test]
fn use_version_broken_bin_leaves_previous_active() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
let good_dir = foundry_dir.join("versions/foundry-rs/foundry/v1.0.0");
let broken_dir = foundry_dir.join("versions/foundry-rs/foundry/v2.0.0");
std::fs::create_dir_all(&good_dir).unwrap();
std::fs::create_dir_all(&broken_dir).unwrap();
for bin in BINS {
write_fake_bin(&good_dir.join(format!("{bin}{EXE_SUFFIX}")));
}
for bin in BINS.iter().filter(|b| **b != "chisel") {
write_fake_bin(&broken_dir.join(format!("{bin}{EXE_SUFFIX}")));
}
std::fs::write(broken_dir.join(format!("chisel{EXE_SUFFIX}")), b"not an executable").unwrap();
foundryup().env("FOUNDRY_DIR", &foundry_dir).args(["--use", "1.0.0"]).assert().success();
foundryup()
.env("FOUNDRY_DIR", &foundry_dir)
.args(["--use", "2.0.0"])
.assert()
.failure()
.stderr_eq(str![[r#"
...
[..]failed to run chisel[..]
...
"#]]);
let bin_dir = foundry_dir.join("bin");
for &bin in BINS {
let active = bin_dir.join(format!("{bin}{EXE_SUFFIX}"));
let target = std::fs::read_link(&active)
.unwrap_or_else(|e| panic!("{bin} is not an active symlink: {e}"));
assert_eq!(target, good_dir.join(format!("{bin}{EXE_SUFFIX}")), "{bin} was switched");
}
}
#[cfg(unix)]
#[test]
fn use_version_creates_symlink_on_unix() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
let version_dir = foundry_dir.join("versions/foundry-rs/foundry/v1.5.0");
std::fs::create_dir_all(&version_dir).unwrap();
for bin in BINS {
write_fake_bin(&version_dir.join(bin));
}
foundryup().env("FOUNDRY_DIR", &foundry_dir).args(["--use", "1.5.0"]).assert().success();
for &bin in BINS {
let dest = foundry_dir.join("bin").join(bin);
let meta = std::fs::symlink_metadata(&dest).unwrap();
assert!(meta.file_type().is_symlink(), "{bin} should be a symlink");
assert_eq!(std::fs::read_link(&dest).unwrap(), version_dir.join(bin));
}
}
#[test]
fn empty_foundryup_env_is_ignored() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
foundryup()
.env("FOUNDRY_DIR", temp_dir.path().join(".foundry"))
.env("FOUNDRYUP_VERSION", "")
.env("FOUNDRYUP_PR", "")
.arg("--list")
.assert()
.success();
}
#[test]
fn list_empty() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
foundryup()
.env("FOUNDRY_DIR", temp_dir.path().join(".foundry"))
.arg("--list")
.assert()
.success();
}
#[test]
fn list_writes_to_stdout() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
let version_dir = foundry_dir.join("versions/foundry-rs/foundry/v1.0.0");
std::fs::create_dir_all(&version_dir).unwrap();
for bin in BINS {
write_fake_bin(&version_dir.join(format!("{bin}{EXE_SUFFIX}")));
}
foundryup().env("FOUNDRY_DIR", &foundry_dir).arg("--list").assert().success().stdout_eq(str![
[r#"
foundryup: foundry-rs/foundry v1.0.0
...
"#]
]);
}
#[test]
fn list_includes_optional_bin_when_present() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
let version_dir = foundry_dir.join("versions/foundry-rs/foundry/v1.0.0");
std::fs::create_dir_all(&version_dir).unwrap();
for bin in BINS.iter().chain(OPTIONAL_BINS) {
write_fake_bin(&version_dir.join(format!("{bin}{EXE_SUFFIX}")));
}
let assert = foundryup().env("FOUNDRY_DIR", &foundry_dir).arg("--list").assert().success();
let stdout = String::from_utf8_lossy(&assert.get_output().stdout);
let bin_count = stdout.lines().filter(|line| line.starts_with("foundryup: - ")).count();
assert_eq!(bin_count, BINS.len() + OPTIONAL_BINS.len());
}
#[test]
fn list_fails_on_broken_install() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
let version_dir = foundry_dir.join("versions/foundry-rs/foundry/v1.0.0");
std::fs::create_dir_all(&version_dir).unwrap();
for bin in &BINS[..BINS.len() - 1] {
write_fake_bin(&version_dir.join(format!("{bin}{EXE_SUFFIX}")));
}
foundryup().env("FOUNDRY_DIR", &foundry_dir).arg("--list").assert().failure().stderr_eq(str![
[r#"
...
[..]is broken: failed to run [..]
...
"#]
]);
}
#[test]
fn migrate_legacy_versions() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
let versions_dir = foundry_dir.join("versions");
std::fs::create_dir_all(versions_dir.join("nightly")).unwrap();
std::fs::create_dir_all(versions_dir.join("stable")).unwrap();
for version in ["nightly", "stable"] {
for bin in BINS {
let bin_path = versions_dir.join(version).join(format!("{bin}{EXE_SUFFIX}"));
write_fake_bin(&bin_path);
}
}
assert!(versions_dir.join("nightly").exists());
assert!(versions_dir.join("stable").exists());
foundryup().env("FOUNDRY_DIR", &foundry_dir).arg("--list").assert().success().stderr_eq(str![
[r#"
...
foundryup: migrating legacy version [..]
...
foundryup: migrating legacy version [..]
...
"#]
]);
assert!(!versions_dir.join("nightly").exists());
assert!(!versions_dir.join("stable").exists());
assert!(versions_dir.join("foundry-rs/foundry/nightly").exists());
assert!(versions_dir.join("foundry-rs/foundry/stable").exists());
}
#[test]
fn migrate_legacy_versions_skips_existing_target() {
let temp_dir = tempfile::Builder::new().tempdir().unwrap();
let foundry_dir = temp_dir.path().join(".foundry");
let versions_dir = foundry_dir.join("versions");
let legacy_dir = versions_dir.join("nightly");
let version_dir = versions_dir.join("foundry-rs/foundry/nightly");
std::fs::create_dir_all(&legacy_dir).unwrap();
std::fs::create_dir_all(&version_dir).unwrap();
for bin in BINS {
write_fake_bin(&legacy_dir.join(format!("{bin}{EXE_SUFFIX}")));
write_fake_bin(&version_dir.join(format!("{bin}{EXE_SUFFIX}")));
}
foundryup()
.env("FOUNDRY_DIR", &foundry_dir)
.arg("--list")
.assert()
.success()
.stdout_eq(str![[r#"
foundryup: foundry-rs/foundry nightly
...
"#]])
.stderr_eq("");
assert!(legacy_dir.exists());
assert!(version_dir.exists());
}