use std::{
path::Path,
process::{Command, Output},
time::{Duration, Instant},
};
fn cargo_elfpak(project: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_cargo-elfpak"))
.current_dir(project)
.args(args)
.output()
.expect("cargo-elfpak runs")
}
fn stdout(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
fn write_binary_project(project: &Path, name: &str, body: &str) {
std::fs::create_dir_all(project.join("src")).unwrap();
std::fs::write(
project.join("Cargo.toml"),
format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n"),
)
.unwrap();
std::fs::write(project.join("src/main.rs"), body).unwrap();
}
fn dry_run_args(rootfs: &Path) -> [&str; 5] {
[
"bundle",
"--output",
rootfs.to_str().unwrap(),
"--dry-run",
"--no-config",
]
}
fn host_target() -> String {
let output = Command::new("rustc").arg("-vV").output().unwrap();
assert!(output.status.success(), "{}", stderr(&output));
stdout(&output)
.lines()
.find_map(|line| line.strip_prefix("host: "))
.expect("rustc reports its host target")
.to_owned()
}
#[test]
fn builds_missing_and_stale_binaries_and_reuses_fresh_ones() {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("project");
let rootfs = tmp.path().join("rootfs");
write_binary_project(&project, "freshness-fixture", "fn main() {}\n");
let args = dry_run_args(&rootfs);
let first = cargo_elfpak(&project, &args);
assert!(first.status.success(), "{}", stderr(&first));
assert!(
stdout(&first).contains("built Cargo binary:"),
"{}",
stdout(&first)
);
assert!(stdout(&first).contains("rootfs:"), "{}", stdout(&first));
assert!(!rootfs.exists(), "dry-run must not materialize output");
let executable_modified = std::fs::metadata(
project
.join("target/debug")
.join(format!("freshness-fixture{}", std::env::consts::EXE_SUFFIX)),
)
.unwrap()
.modified()
.unwrap();
let second = cargo_elfpak(&project, &args);
assert!(second.status.success(), "{}", stderr(&second));
assert!(
stdout(&second).contains("fresh Cargo binary:"),
"{}",
stdout(&second)
);
let deadline = Instant::now() + Duration::from_secs(5);
loop {
std::fs::write(
project.join("src/main.rs"),
"fn main() { println!(\"changed\"); }\n",
)
.unwrap();
let source_modified = std::fs::metadata(project.join("src/main.rs"))
.unwrap()
.modified()
.unwrap();
if source_modified > executable_modified {
break;
}
assert!(
Instant::now() < deadline,
"source {source_modified:?} must become newer than executable {executable_modified:?}"
);
std::thread::sleep(Duration::from_millis(100));
}
let third = cargo_elfpak(&project, &args);
assert!(third.status.success(), "{}", stderr(&third));
assert!(
stdout(&third).contains("built Cargo binary:"),
"stdout:\n{}\nstderr:\n{}",
stdout(&third),
stderr(&third)
);
}
#[test]
fn package_selector_disambiguates_a_virtual_workspace() {
let tmp = tempfile::tempdir().unwrap();
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
std::fs::write(
workspace.join("Cargo.toml"),
"[workspace]\nresolver = \"3\"\nmembers = [\"api\", \"worker\"]\n",
)
.unwrap();
write_binary_project(&workspace.join("api"), "api", "fn main() {}\n");
write_binary_project(&workspace.join("worker"), "worker", "fn main() {}\n");
let rootfs = tmp.path().join("rootfs");
let ambiguous = cargo_elfpak(&workspace, &dry_run_args(&rootfs));
assert!(!ambiguous.status.success());
let error = stderr(&ambiguous);
assert!(error.contains("--package"), "{error}");
assert!(error.contains("api, worker"), "{error}");
let selected = cargo_elfpak(
&workspace,
&[
"elfpak",
"bundle",
"-p",
"worker",
"--output",
rootfs.to_str().unwrap(),
"--dry-run",
"--no-config",
],
);
assert!(selected.status.success(), "{}", stderr(&selected));
let output = stdout(&selected);
assert!(output.contains("worker"), "{output}");
assert!(output.contains("rootfs:"), "{output}");
}
#[test]
fn bin_selector_is_required_when_no_default_can_be_inferred() {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("tools");
std::fs::create_dir_all(project.join("src/bin")).unwrap();
std::fs::write(
project.join("Cargo.toml"),
"[package]\nname = \"tools\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
std::fs::write(project.join("src/bin/first.rs"), "fn main() {}\n").unwrap();
std::fs::write(project.join("src/bin/second.rs"), "fn main() {}\n").unwrap();
let rootfs = tmp.path().join("rootfs");
let ambiguous = cargo_elfpak(&project, &dry_run_args(&rootfs));
assert!(!ambiguous.status.success());
let error = stderr(&ambiguous);
assert!(error.contains("--bin"), "{error}");
assert!(error.contains("first, second"), "{error}");
let selected = cargo_elfpak(
&project,
&[
"bundle",
"--bin",
"second",
"--output",
rootfs.to_str().unwrap(),
"--dry-run",
"--no-config",
],
);
assert!(selected.status.success(), "{}", stderr(&selected));
assert!(
stdout(&selected).contains("second"),
"{}",
stdout(&selected)
);
}
#[test]
fn cargo_build_options_are_forwarded_to_metadata_and_build() {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("project");
let rootfs = tmp.path().join("rootfs");
let target_dir = tmp.path().join("custom-target");
let target = host_target();
write_binary_project(&project, "options-fixture", "fn main() {}\n");
let manifest = project.join("Cargo.toml");
std::fs::write(
&manifest,
"[package]\nname = \"options-fixture\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[features]\nspecial = []\n",
)
.unwrap();
let lock = Command::new("cargo")
.args(["generate-lockfile", "--offline"])
.current_dir(&project)
.output()
.unwrap();
assert!(lock.status.success(), "{}", stderr(&lock));
let output = cargo_elfpak(
tmp.path(),
&[
"bundle",
"--manifest-path",
manifest.to_str().unwrap(),
"--profile",
"dev",
"--target-dir",
target_dir.to_str().unwrap(),
"--target",
&target,
"--features",
"special",
"--no-default-features",
"--locked",
"--offline",
"--frozen",
"--output",
rootfs.to_str().unwrap(),
"--dry-run",
"--no-config",
],
);
assert!(output.status.success(), "{}", stderr(&output));
let text = stdout(&output);
assert!(
text.contains(&format!("custom-target/{target}/debug/options-fixture")),
"{text}"
);
}
#[test]
fn release_profile_is_forwarded() {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("project");
let rootfs = tmp.path().join("rootfs");
let target_dir = tmp.path().join("custom-target");
write_binary_project(&project, "release-fixture", "fn main() {}\n");
let output = cargo_elfpak(
&project,
&[
"bundle",
"--release",
"--target-dir",
target_dir.to_str().unwrap(),
"--output",
rootfs.to_str().unwrap(),
"--dry-run",
"--no-config",
],
);
assert!(output.status.success(), "{}", stderr(&output));
assert!(
stdout(&output).contains("custom-target/release/release-fixture"),
"{}",
stdout(&output)
);
}
#[test]
fn release_and_profile_conflict() {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("project");
write_binary_project(&project, "conflict-fixture", "fn main() {}\n");
let output = cargo_elfpak(
&project,
&["bundle", "--release", "--profile", "dev", "--dry-run"],
);
assert!(!output.status.success());
let error = stderr(&output);
assert!(error.contains("cannot be used with"), "{error}");
}
#[test]
fn positional_binary_is_rejected_in_favor_of_bin_selection() {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("project");
let rootfs = tmp.path().join("rootfs");
write_binary_project(&project, "selected-fixture", "fn main() {}\n");
let output = cargo_elfpak(
&project,
&[
"bundle",
"/ignored/binary",
"--output",
rootfs.to_str().unwrap(),
"--dry-run",
"--no-config",
],
);
assert!(!output.status.success());
let error = stderr(&output);
assert!(error.contains("unexpected argument"), "{error}");
}
#[test]
fn quiet_suppresses_cargo_warnings_and_bundle_output() {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("project");
let rootfs = tmp.path().join("rootfs");
write_binary_project(&project, "quiet-fixture", "fn main() { let unused = 1; }\n");
let output = cargo_elfpak(
&project,
&[
"bundle",
"--quiet",
"--output",
rootfs.to_str().unwrap(),
"--dry-run",
"--no-config",
],
);
assert!(output.status.success(), "{}", stderr(&output));
assert!(stdout(&output).is_empty(), "{}", stdout(&output));
assert!(stderr(&output).is_empty(), "{}", stderr(&output));
}
#[test]
fn quiet_still_reports_cargo_errors() {
let tmp = tempfile::tempdir().unwrap();
let project = tmp.path().join("project");
let rootfs = tmp.path().join("rootfs");
write_binary_project(&project, "error-fixture", "fn main( {\n");
let output = cargo_elfpak(
&project,
&[
"bundle",
"--quiet",
"--output",
rootfs.to_str().unwrap(),
"--dry-run",
"--no-config",
],
);
assert!(!output.status.success());
assert!(stdout(&output).is_empty(), "{}", stdout(&output));
let error = stderr(&output);
assert!(error.contains("src/main.rs"), "{error}");
assert!(error.contains("Cargo build failed"), "{error}");
}