mod common;
use std::fs;
use std::path::Path;
use std::process::Command;
fn create_mini_project(dir: &Path) {
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(
dir.join("Cargo.toml"),
r#"[package]
name = "mini"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "mini"
path = "src/main.rs"
"#,
)
.unwrap();
fs::write(
dir.join("src").join("main.rs"),
r#"fn main() {
let result = work();
println!("result: {result}");
}
fn work() -> u64 {
let mut sum = 0u64;
for i in 0..1000 {
sum += i;
}
sum
}
"#,
)
.unwrap();
}
#[test]
fn run_command_builds_executes_and_reports() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("mini");
create_mini_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let runs_dir = tmp.path().join("runs");
let output = Command::new(piano_bin)
.args(["profile", "--fn", "work", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.output()
.expect("failed to run piano profile");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"piano profile failed:\nstderr: {stderr}\nstdout: {stdout}"
);
assert!(
stdout.contains("result: 499500"),
"program output should appear, got: {stdout}"
);
assert!(
stderr.contains("built:"),
"should show built path on stderr, got: {stderr}"
);
assert!(
stdout.contains("work"),
"report should appear on stdout with 'work' function, got: {stdout}"
);
}
fn create_exit_one_project(dir: &Path) {
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(
dir.join("Cargo.toml"),
r#"[package]
name = "exit-one"
version = "0.1.0"
edition = "2024"
"#,
)
.unwrap();
fs::write(
dir.join("src").join("main.rs"),
r#"fn main() {
work();
std::process::exit(1);
}
fn work() -> u64 {
let mut sum = 0u64;
for i in 0..100 {
sum += i;
}
sum
}
"#,
)
.unwrap();
}
#[test]
fn run_command_warns_on_nonzero_exit_code() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("exit-one");
create_exit_one_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let runs_dir = tmp.path().join("runs");
let output = Command::new(piano_bin)
.args(["profile", "--fn", "work", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.output()
.expect("failed to run piano profile");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("exited with code"),
"should warn about non-zero exit code without --ignore-exit-code, got: {stderr}"
);
}
#[test]
fn run_command_ignore_exit_code_suppresses_warning() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("exit-one");
create_exit_one_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let runs_dir = tmp.path().join("runs");
let output = Command::new(piano_bin)
.args(["profile", "--fn", "work", "--ignore-exit-code", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.output()
.expect("failed to run piano profile with --ignore-exit-code");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("exited with code"),
"should NOT warn about exit code with --ignore-exit-code, got: {stderr}"
);
}
#[test]
fn profile_suppresses_no_runs_error_on_nonzero_exit() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("exit-one");
create_exit_one_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let runs_dir = tmp.path().join("runs");
fs::create_dir_all(&runs_dir).unwrap();
let output = Command::new(piano_bin)
.args(["profile", "--fn", "work", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.output()
.expect("failed to run piano profile");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("exited with code"),
"should warn about non-zero exit, got: {stderr}"
);
assert!(
!stderr.contains("no piano runs found"),
"should suppress NoRuns when program exited non-zero, got: {stderr}"
);
}
fn create_exit_early_project(dir: &Path) {
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(
dir.join("Cargo.toml"),
r#"[package]
name = "exit-early"
version = "0.1.0"
edition = "2024"
"#,
)
.unwrap();
fs::write(
dir.join("src").join("main.rs"),
r#"fn main() {
// Exit immediately -- work() is never called.
std::process::exit(1);
}
fn work() -> u64 {
let mut sum = 0u64;
for i in 0..100 {
sum += i;
}
sum
}
"#,
)
.unwrap();
}
#[test]
fn profile_ignore_exit_code_surfaces_no_data_written() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("exit-early");
create_exit_early_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let runs_dir = tmp.path().join("runs");
fs::create_dir_all(&runs_dir).unwrap();
let output = Command::new(piano_bin)
.args(["profile", "--fn", "work", "--ignore-exit-code", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.output()
.expect("failed to run piano profile with --ignore-exit-code");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"piano profile with --ignore-exit-code should succeed, got:\nstderr: {stderr}\nstdout: {stdout}"
);
assert!(
!stderr.contains("exited with code"),
"should NOT warn about exit code with --ignore-exit-code, got: {stderr}"
);
assert!(
!stdout.contains("work"),
"report should NOT show work (never called), got: {stdout}"
);
}
#[test]
fn profile_propagates_child_exit_code() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("exit-one");
create_exit_one_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let runs_dir = tmp.path().join("runs");
fs::create_dir_all(&runs_dir).unwrap();
let output = Command::new(piano_bin)
.args(["profile", "--fn", "work", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.output()
.expect("failed to run piano profile");
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
output.status.code(),
Some(1),
"piano profile should propagate child's exit code 1, got: {:?}\nstderr: {stderr}",
output.status.code()
);
}
#[test]
fn profile_ignore_exit_code_returns_success() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("exit-one");
create_exit_one_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let runs_dir = tmp.path().join("runs");
fs::create_dir_all(&runs_dir).unwrap();
let output = Command::new(piano_bin)
.args(["profile", "--fn", "work", "--ignore-exit-code", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.output()
.expect("failed to run piano profile with --ignore-exit-code");
assert!(
output.status.success(),
"piano profile with --ignore-exit-code should exit 0, got: {:?}",
output.status.code()
);
}
fn create_echo_args_project(dir: &Path) {
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(
dir.join("Cargo.toml"),
r#"[package]
name = "echo-args"
version = "0.1.0"
edition = "2024"
"#,
)
.unwrap();
fs::write(
dir.join("src").join("main.rs"),
r#"fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
println!("args: {}", args.join(" "));
}
"#,
)
.unwrap();
}
#[test]
fn run_command_passes_args_after_separator() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("echo-args");
create_echo_args_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let runs_dir = tmp.path().join("runs");
let output = Command::new(piano_bin)
.args(["profile", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.args(["--", "--input", "data.csv", "--verbose"])
.env("PIANO_RUNS_DIR", &runs_dir)
.output()
.expect("failed to run piano profile with args");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"piano profile with args failed:\nstderr: {stderr}\nstdout: {stdout}"
);
assert!(
stdout.contains("args: --input data.csv --verbose"),
"program should receive the passed args, got: {stdout}"
);
}
#[test]
fn run_executes_last_built_binary() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("mini");
create_mini_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let build_output = Command::new(piano_bin)
.args(["build", "--fn", "work", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.output()
.expect("failed to run piano build");
assert!(
build_output.status.success(),
"piano build failed: {}",
String::from_utf8_lossy(&build_output.stderr)
);
let run_output = Command::new(piano_bin)
.arg("run")
.current_dir(&project_dir)
.output()
.expect("failed to run piano run");
let stdout = String::from_utf8_lossy(&run_output.stdout);
let stderr = String::from_utf8_lossy(&run_output.stderr);
assert!(
run_output.status.success(),
"piano run failed:\nstderr: {stderr}\nstdout: {stdout}"
);
assert!(
stdout.contains("result: 499500"),
"program output should appear, got stdout: {stdout}"
);
assert!(
stderr.contains("running:"),
"should show which binary is running, got stderr: {stderr}"
);
}
#[test]
fn run_passes_args_via_separator() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("echo-args");
create_echo_args_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let build_output = Command::new(piano_bin)
.args(["build", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.output()
.expect("failed to run piano build");
assert!(build_output.status.success());
let run_output = Command::new(piano_bin)
.args(["run", "--", "--input", "data.csv", "--verbose"])
.current_dir(&project_dir)
.output()
.expect("failed to run piano run with args");
let stdout = String::from_utf8_lossy(&run_output.stdout);
assert!(
run_output.status.success(),
"piano run with args failed: {}",
String::from_utf8_lossy(&run_output.stderr)
);
assert!(
stdout.contains("args: --input data.csv --verbose"),
"program should receive the passed args, got: {stdout}"
);
}
#[test]
fn process_exit_preserves_profiling_data() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("exit-one");
create_exit_one_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let runs_dir = tmp.path().join("runs");
let output = Command::new(piano_bin)
.args(["profile", "--fn", "work", "--ignore-exit-code", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.output()
.expect("failed to run piano profile");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"piano profile with --ignore-exit-code should exit 0:\nstderr: {stderr}\nstdout: {stdout}"
);
assert!(
runs_dir.exists(),
"runs directory should exist: {runs_dir:?}"
);
let data_path = common::largest_ndjson_file(&runs_dir);
let data = fs::read_to_string(&data_path)
.unwrap_or_else(|e| panic!("should read data file {data_path:?}: {e}"));
assert!(
data.contains("work"),
"profiling data should contain 'work' function, got: {data}"
);
assert!(
stdout.contains("work"),
"report should show 'work' function, got: {stdout}"
);
}
fn create_panic_project(dir: &Path) {
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(
dir.join("Cargo.toml"),
r#"[package]
name = "panic-test"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "panic-test"
path = "src/main.rs"
"#,
)
.unwrap();
fs::write(
dir.join("src").join("main.rs"),
r#"fn main() {
let result = work();
println!("result: {result}");
panic!("boom");
}
fn work() -> u64 {
let mut sum = 0u64;
for i in 0..1000 {
sum += i;
}
sum
}
"#,
)
.unwrap();
}
#[test]
fn profile_captures_data_on_panic() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("panic-test");
create_panic_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let runs_dir = tmp.path().join("runs");
let output = Command::new(piano_bin)
.args(["profile", "--fn", "work", "--ignore-exit-code", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.output()
.expect("failed to run piano profile");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stderr.contains("panicked"),
"child process should have panicked, but stderr shows no panic:\n{stderr}"
);
assert!(
output.status.success(),
"piano profile with --ignore-exit-code should exit 0 even on panic:\nstderr: {stderr}\nstdout: {stdout}"
);
assert!(
runs_dir.exists(),
"runs directory should exist: {runs_dir:?}"
);
let data_path = common::largest_ndjson_file(&runs_dir);
let data = fs::read_to_string(&data_path)
.unwrap_or_else(|e| panic!("should read data file {data_path:?}: {e}"));
assert!(
data.contains("work"),
"profiling data should contain the instrumented function 'work', got: {data}"
);
assert!(
stdout.contains("work"),
"report should appear on stdout with 'work' function, got: {stdout}"
);
}
#[test]
fn run_errors_when_no_binary_exists() {
let tmp = tempfile::tempdir().unwrap();
let piano_bin = env!("CARGO_BIN_EXE_piano");
let output = Command::new(piano_bin)
.arg("run")
.current_dir(tmp.path())
.output()
.expect("failed to run piano run");
assert!(
!output.status.success(),
"piano run should fail when no binary exists"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("piano build"),
"error should mention piano build, got: {stderr}"
);
}
fn create_sleeping_project(dir: &Path) {
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(
dir.join("Cargo.toml"),
r#"[package]
name = "sleeper"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "sleeper"
path = "src/main.rs"
"#,
)
.unwrap();
fs::write(
dir.join("src").join("main.rs"),
r#"fn main() {
let result = work();
println!("result: {result}");
std::thread::sleep(std::time::Duration::from_secs(60));
}
fn work() -> u64 {
let mut sum = 0u64;
for i in 0..1000 {
sum += i;
}
sum
}
"#,
)
.unwrap();
}
#[cfg(unix)]
#[test]
fn duration_stop_exits_zero_with_no_warning() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("sleeper");
create_sleeping_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let runs_dir = tmp.path().join("runs");
let output = Command::new(piano_bin)
.args(["profile", "--fn", "work", "--duration", "5", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.output()
.expect("failed to run piano profile with --duration");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"piano profile --duration should exit 0, got: {:?}\nstderr: {stderr}\nstdout: {stdout}",
output.status.code()
);
assert!(
!stderr.contains("warning:"),
"should NOT print a warning for --duration stop, got: {stderr}"
);
assert!(
stdout.contains("work"),
"report should contain 'work' function, got: {stdout}"
);
}
#[cfg(unix)]
#[test]
fn sigint_terminates_child_and_produces_data() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("sleeper");
create_sleeping_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let runs_dir = tmp.path().join("runs");
let child = Command::new(piano_bin)
.args(["profile", "--fn", "work", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to spawn piano profile");
std::thread::sleep(std::time::Duration::from_secs(10));
unsafe {
libc::kill(child.id() as libc::pid_t, libc::SIGINT);
}
let output = child
.wait_with_output()
.expect("failed to wait for piano profile");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"piano profile after SIGINT should exit 0, got: {:?}\nstderr: {stderr}\nstdout: {stdout}",
output.status.code()
);
assert!(
!stderr.contains("warning:"),
"should NOT print a warning after SIGINT, got: {stderr}"
);
assert!(
stdout.contains("work"),
"report should contain 'work' function, got: {stdout}"
);
}
#[test]
fn duration_zero_rejected() {
let piano_bin = env!("CARGO_BIN_EXE_piano");
let output = Command::new(piano_bin)
.args(["profile", "--duration", "0"])
.output()
.expect("failed to run piano");
assert!(
!output.status.success(),
"piano profile --duration 0 should fail"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("duration cannot be zero"),
"should mention 'duration cannot be zero', got: {stderr}"
);
}
#[test]
fn duration_negative_rejected() {
let piano_bin = env!("CARGO_BIN_EXE_piano");
let output = Command::new(piano_bin)
.args(["profile", "--duration=-1"])
.output()
.expect("failed to run piano");
assert!(
!output.status.success(),
"piano profile --duration -1 should fail"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("duration cannot be negative"),
"should mention 'duration cannot be negative', got: {stderr}"
);
}
#[cfg(unix)]
#[test]
fn duration_fractional_accepted() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("sleeper");
create_sleeping_project(&project_dir);
common::prepopulate_deps(&project_dir, common::mini_seed());
let piano_bin = env!("CARGO_BIN_EXE_piano");
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let runtime_path = manifest_dir.join("piano-runtime");
let runs_dir = tmp.path().join("runs");
let output = Command::new(piano_bin)
.args(["profile", "--fn", "work", "--duration", "2.5", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.output()
.expect("failed to run piano profile with --duration 2.5");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"piano profile --duration 2.5 should exit 0, got: {:?}\nstderr: {stderr}\nstdout: {stdout}",
output.status.code()
);
assert!(
stderr.contains("will stop after 2.5s"),
"should show 'will stop after 2.5s', got: {stderr}"
);
}