mod common;
use std::fs;
use std::path::Path;
fn create_exit_project(dir: &Path) {
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(
dir.join("Cargo.toml"),
r#"[package]
name = "exit-test"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "exit-test"
path = "src/main.rs"
"#,
)
.unwrap();
fs::write(
dir.join("src").join("main.rs"),
r#"fn main() {
let _ = work();
std::process::exit(1);
}
fn work() -> u64 {
let mut sum = 0u64;
for i in 0..1000 {
sum += i;
}
sum
}
"#,
)
.unwrap();
}
#[test]
fn process_exit_produces_valid_profiling_data() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("exit-test");
create_exit_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 = std::process::Command::new(piano_bin)
.args(["build", "--fn", "work", "--fn", "main", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.output()
.expect("piano build failed");
assert!(
build_output.status.success(),
"piano build failed:\n{}",
String::from_utf8_lossy(&build_output.stderr)
);
let binary_path = String::from_utf8_lossy(&build_output.stdout)
.trim()
.to_string();
assert!(
Path::new(&binary_path).exists(),
"binary not found: {binary_path}"
);
let runs_dir = tmp.path().join("runs");
fs::create_dir_all(&runs_dir).unwrap();
let run_output = std::process::Command::new(&binary_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.output()
.expect("failed to run instrumented binary");
let stderr = String::from_utf8_lossy(&run_output.stderr);
assert!(
!stderr.contains("cannot access a Thread Local Storage"),
"TLS panic detected in stderr:\n{stderr}"
);
assert!(
!stderr.contains("panic"),
"unexpected panic in stderr:\n{stderr}"
);
let ndjson_files: Vec<_> = fs::read_dir(&runs_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "ndjson"))
.collect();
assert!(
!ndjson_files.is_empty(),
"no NDJSON files written (runtime crashed without writing data)"
);
let ndjson_path = common::largest_ndjson_file(&runs_dir);
let content = fs::read_to_string(&ndjson_path).unwrap();
let stats = common::aggregate_ndjson(&content);
assert!(
stats.contains_key("work"),
"profiling data should contain 'work' function, got: {stats:?}"
);
assert!(
stats["work"].calls >= 1,
"work() should have been called at least once"
);
assert!(
!stats.contains_key("main"),
"main should NOT appear in output (lifecycle boundary, excluded from name table)"
);
}
#[test]
fn process_exit_streaming_no_tls_panic() {
let tmp = tempfile::tempdir().unwrap();
let project_dir = tmp.path().join("exit-stream-test");
create_exit_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 = std::process::Command::new(piano_bin)
.args(["build", "--fn", "work", "--fn", "main", "--project"])
.arg(&project_dir)
.arg("--runtime-path")
.arg(&runtime_path)
.output()
.expect("piano build failed");
assert!(
build_output.status.success(),
"piano build failed:\n{}",
String::from_utf8_lossy(&build_output.stderr)
);
let binary_path = String::from_utf8_lossy(&build_output.stdout)
.trim()
.to_string();
let runs_dir = tmp.path().join("runs");
fs::create_dir_all(&runs_dir).unwrap();
let run_output = std::process::Command::new(&binary_path)
.env("PIANO_RUNS_DIR", &runs_dir)
.env("PIANO_STREAM_FRAMES", "1")
.output()
.expect("failed to run instrumented binary");
let stderr = String::from_utf8_lossy(&run_output.stderr);
assert!(
!stderr.contains("cannot access a Thread Local Storage"),
"TLS panic detected in streaming atexit path:\n{stderr}"
);
assert!(
!stderr.contains("panic"),
"unexpected panic in stderr:\n{stderr}"
);
let ndjson_files: Vec<_> = fs::read_dir(&runs_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|ext| ext == "ndjson"))
.collect();
assert!(
!ndjson_files.is_empty(),
"no NDJSON files written in streaming mode (runtime crashed without writing data)"
);
let ndjson_path = common::largest_ndjson_file(&runs_dir);
let content = fs::read_to_string(&ndjson_path).unwrap();
let stats = common::aggregate_ndjson(&content);
assert!(
stats.contains_key("work"),
"streaming profiling data should contain 'work' function, got: {stats:?}"
);
}