mod common;
use common::{init, OptimizationLevel, FIXTURES};
use lazy_static::lazy_static;
use std::collections::HashMap;
use std::process::Stdio;
use std::sync::{Arc, Once};
use std::time::Duration;
use tokio::process::Command;
use tokio::sync::RwLock;
use tokio::time::timeout;
lazy_static! {
static ref GLOBAL_TEST_MANAGER: Arc<RwLock<HashMap<OptimizationLevel, GlobalTestProcess>>> =
Arc::new(RwLock::new(HashMap::new()));
}
struct GlobalTestProcess {
child: tokio::process::Child,
pid: u32,
optimization_level: OptimizationLevel,
}
impl GlobalTestProcess {
async fn start_with_opt(opt_level: OptimizationLevel) -> anyhow::Result<Self> {
let binary_path = FIXTURES.get_test_binary_with_opt("sample_program", opt_level)?;
println!(
"🚀 Starting global sample_program ({}): {}",
opt_level.description(),
binary_path.display()
);
let child = Command::new(binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = child
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
println!(
"✓ Started global sample_program ({}) with PID: {}",
opt_level.description(),
pid
);
Ok(Self {
child,
pid,
optimization_level: opt_level,
})
}
fn get_pid(&self) -> u32 {
self.pid
}
async fn terminate(mut self) -> anyhow::Result<()> {
println!(
"🛑 Terminating global sample_program ({}, PID: {})",
self.optimization_level.description(),
self.pid
);
let _ = self.child.kill().await.is_ok();
match timeout(Duration::from_secs(2), self.child.wait()).await {
Ok(_) => {
println!(
"✓ Global sample_program ({}) terminated gracefully",
self.optimization_level.description()
);
}
Err(_) => {
let _ = std::process::Command::new("kill")
.arg("-KILL")
.arg(self.pid.to_string())
.status()
.is_ok();
println!(
"⚠️ Force killed global sample_program ({})",
self.optimization_level.description()
);
}
}
Ok(())
}
}
async fn get_global_test_pid_with_opt(opt_level: OptimizationLevel) -> anyhow::Result<u32> {
let manager = GLOBAL_TEST_MANAGER.clone();
{
let read_guard = manager.read().await;
if let Some(process) = read_guard.get(&opt_level) {
let status = std::process::Command::new("kill")
.arg("-0")
.arg(process.pid.to_string())
.status();
if status.is_ok_and(|s| s.success()) {
return Ok(process.pid);
}
}
}
let mut write_guard = manager.write().await;
if let Some(process) = write_guard.get(&opt_level) {
let status = std::process::Command::new("kill")
.arg("-0")
.arg(process.pid.to_string())
.status();
if status.is_ok_and(|s| s.success()) {
return Ok(process.pid);
}
}
let old_proc = write_guard.remove(&opt_level);
drop(write_guard);
if let Some(old) = old_proc {
let _ = old.terminate().await;
}
let new_process = GlobalTestProcess::start_with_opt(opt_level).await?;
let pid = new_process.get_pid();
let mut write_guard = manager.write().await;
write_guard.insert(opt_level, new_process);
Ok(pid)
}
pub async fn cleanup_global_test_process() -> anyhow::Result<()> {
let manager = GLOBAL_TEST_MANAGER.clone();
let mut write_guard = manager.write().await;
let processes: Vec<GlobalTestProcess> = write_guard.drain().map(|(_, p)| p).collect();
drop(write_guard);
for proc in processes.into_iter() {
let _ = proc.terminate().await;
}
Ok(())
}
#[tokio::test]
async fn test_void_pointer_addition_prints_address() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let opt_level = OptimizationLevel::Debug;
let _ = get_global_test_pid_with_opt(opt_level).await?;
let script_content = r#"
trace sink_void {
print p + 1;
}
"#;
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_opt(script_content, 4, opt_level).await?;
assert_eq!(
exit_code, 0,
"unexpected error: stderr={stderr}\nstdout={stdout}"
);
let mut saw_addr = false;
for line in stdout.lines() {
let t = line.trim();
if (t.starts_with("(p+1) = ") && t.contains("0x"))
|| (t.starts_with("0x") && t.contains("(void*)"))
{
saw_addr = true;
break;
}
}
assert!(
saw_addr,
"expected (p+1) to print an address.\nSTDOUT: {stdout}\nSTDERR: {stderr}"
);
Ok(())
}
#[tokio::test]
async fn test_struct_pointer_addition_scales_by_type_size() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let opt_level = OptimizationLevel::Debug;
let _ = get_global_test_pid_with_opt(opt_level).await?;
let script_content = r#"
trace print_record {
print record + 1;
print record + 2;
}
"#;
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_opt(script_content, 5, opt_level).await?;
if exit_code != 0 && stderr.contains("BPF_PROG_LOAD") {
return Ok(());
}
assert_eq!(
exit_code, 0,
"unexpected error: stderr={stderr}\nstdout={stdout}"
);
let mut addr1: Option<u64> = None;
let mut addr2: Option<u64> = None;
for line in stdout.lines() {
let t = line.trim();
if t.starts_with("(record+1) = ") || t.starts_with("(record + 1) = ") {
if let Some(ix) = t.rfind("0x") {
let mut j = ix + 2;
let bytes = t.as_bytes();
while j < t.len() && bytes[j].is_ascii_hexdigit() {
j += 1;
}
if j > ix + 2 {
if let Ok(v) = u64::from_str_radix(&t[ix + 2..j], 16) {
addr1 = Some(v);
}
}
}
}
if t.starts_with("(record+2) = ") || t.starts_with("(record + 2) = ") {
if let Some(ix) = t.rfind("0x") {
let mut j = ix + 2;
let bytes = t.as_bytes();
while j < t.len() && bytes[j].is_ascii_hexdigit() {
j += 1;
}
if j > ix + 2 {
if let Ok(v) = u64::from_str_radix(&t[ix + 2..j], 16) {
addr2 = Some(v);
}
}
}
}
}
if let (Some(a1), Some(a2)) = (addr1, addr2) {
let delta = a2.wrapping_sub(a1);
assert_eq!(
delta, 48,
"expected address delta sizeof(DataRecord)=48 bytes (got {delta}).\nSTDOUT: {stdout}\nSTDERR: {stderr}"
);
} else {
}
Ok(())
}
#[tokio::test]
async fn test_special_pid_in_if_condition() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let opt_level = OptimizationLevel::Debug;
let test_pid = get_global_test_pid_with_opt(opt_level).await?;
let script_content = format!(
"trace sample_program.c:16 {{\n if $pid == {test_pid} {{ print \"PID_OK\"; }} else {{ print \"PID_BAD\"; }}\n}}\n"
);
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_opt(&script_content, 3, opt_level).await?;
assert_eq!(exit_code, 0, "stderr={stderr}");
assert!(
stdout.contains("PID_OK"),
"Expected PID_OK in output. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_special_tid_and_timestamp_print() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let opt_level = OptimizationLevel::Debug;
let _ = get_global_test_pid_with_opt(opt_level).await?;
let script_content = r#"
trace sample_program.c:16 {
print "TST:{} {}", $tid, $timestamp;
}
"#;
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_opt(script_content, 3, opt_level).await?;
assert_eq!(exit_code, 0, "stderr={stderr}");
assert!(
stdout.contains("TST:"),
"Expected TST: with tid/timestamp in output. STDOUT: {stdout}"
);
Ok(())
}
static GLOBAL_CLEANUP_REGISTERED: Once = Once::new();
fn ensure_global_cleanup_registered() {
GLOBAL_CLEANUP_REGISTERED.call_once(|| {
extern "C" fn cleanup_on_exit() {
println!("🧹 Global test cleanup: All tests finished, cleaning up...");
let _pkill_result = std::process::Command::new("pkill")
.args(["-f", "sample_program"]) .status()
.is_ok();
let fixtures_path =
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
let sample_program_dir = fixtures_path.join("sample_program");
println!("🧹 Running make clean in sample_program directory...");
let clean_result = std::process::Command::new("make")
.arg("clean")
.current_dir(sample_program_dir)
.output();
match clean_result {
Ok(output) => {
if output.status.success() {
println!("✓ Successfully cleaned sample_program build files");
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
println!("⚠️ Make clean failed: {stderr}");
}
}
Err(e) => {
println!("⚠️ Failed to run make clean: {e}");
}
}
println!("🧹 Global cleanup completed");
}
unsafe {
libc::atexit(cleanup_on_exit);
}
println!("✓ Global cleanup handler registered");
});
}
async fn run_ghostscope_with_script_opt(
script_content: &str,
timeout_secs: u64,
opt_level: OptimizationLevel,
) -> anyhow::Result<(i32, String, String)> {
let test_pid = get_global_test_pid_with_opt(opt_level).await?;
println!(
"🔍 Running ghostscope with {} binary (PID: {})",
opt_level.description(),
test_pid
);
common::runner::GhostscopeRunner::new()
.with_script(script_content)
.with_pid(test_pid)
.timeout_secs(timeout_secs)
.enable_sysmon_shared_lib(false)
.run()
.await
}
async fn run_ghostscope_with_script(
script_content: &str,
timeout_secs: u64,
) -> anyhow::Result<(i32, String, String)> {
run_ghostscope_with_script_opt(script_content, timeout_secs, OptimizationLevel::Debug).await
}
#[tokio::test]
async fn test_logical_or_short_circuit_chain() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
// Exercise chained OR; final should be true
print (0 || 0 || 1);
// Exercise chained OR; final should be false
print (0 || 0 || 0);
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 5).await?;
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
let saw_true = stdout.contains("true");
let saw_false = stdout.contains("false");
assert!(
saw_true,
"Expected at least one true result. STDOUT: {stdout}"
);
assert!(
saw_false,
"Expected at least one false result. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_memcmp_rejects_script_pointer_variable_e2e() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
let p = "A";
if memcmp(p, hex("41"), 1) { print "OK"; } else { print "NO"; }
}
"#;
let (exit_code, _stdout, stderr) = run_ghostscope_with_script(script_content, 2).await?;
assert!(
exit_code != 0,
"expected non-zero exit due to compile error; stderr={stderr}"
);
let has_banner = stderr.contains("No uprobe configurations created")
|| stderr.contains("Script compilation failed");
let has_failed_targets = stderr.contains("Failed targets:");
let has_reason = stderr.contains("expression is not a pointer/address");
let has_tip = stderr.contains("Tip: fix the reported compile-time errors above");
assert!(
has_banner && has_failed_targets && has_reason && has_tip,
"Expected failed-targets details with pointer/address reason and tip. stderr={stderr}"
);
Ok(())
}
#[tokio::test]
async fn test_pointer_ordered_comparison_is_rejected_e2e() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace process_data {
if message > 0 { print "BAD"; }
}
"#;
let (exit_code, _stdout, stderr) = run_ghostscope_with_script(script_content, 2).await?;
assert!(
exit_code != 0,
"expected non-zero exit due to compile error; stderr={stderr}"
);
let has_banner = stderr.contains("No uprobe configurations created")
|| stderr.contains("Script compilation failed");
let has_reason =
stderr.contains("Pointer ordered comparison ('<', '<=', '>', '>=') is not supported");
assert!(
has_banner && has_reason,
"Expected pointer ordered comparison rejection with banner. stderr={stderr}"
);
Ok(())
}
#[tokio::test]
async fn test_pointer_addition_print_reads_element_at_offset() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let opt_level = OptimizationLevel::Debug;
let _ = get_global_test_pid_with_opt(opt_level).await?;
let script_content = r#"
trace log_activity {
print activity + 1;
}
"#;
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_opt(script_content, 4, opt_level).await?;
assert_eq!(
exit_code, 0,
"unexpected error: stderr={stderr}\nstdout={stdout}"
);
let mut matched = false;
for line in stdout.lines() {
let t = line.trim();
let is_name = t.starts_with("(activity+1) = ") || t.starts_with("activity + 1 = ");
if is_name && (t.ends_with("97") || t.ends_with("'a'")) {
matched = true;
break;
}
}
assert!(
matched,
"expected activity + 1 to print 'a' (97).\nSTDOUT: {stdout}\nSTDERR: {stderr}"
);
Ok(())
}
#[tokio::test]
async fn test_pointer_addition_scales_on_int_array() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let opt_level = OptimizationLevel::Debug;
let _ = get_global_test_pid_with_opt(opt_level).await?;
let script_content = r#"
trace sample_program.c:42 {
print numbers + 1;
print numbers + 2;
}
"#;
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_opt(script_content, 5, opt_level).await?;
assert_eq!(
exit_code, 0,
"unexpected error: stderr={stderr}\nstdout={stdout}"
);
let mut saw_20 = false;
let mut saw_30 = false;
let mut addr1: Option<u64> = None;
let mut addr2: Option<u64> = None;
for line in stdout.lines() {
let t = line.trim();
if t.starts_with("(numbers+1) = ") {
if t.ends_with("20") {
saw_20 = true;
}
if let Some(ix) = t.rfind("0x") {
let mut j = ix + 2;
let bytes = t.as_bytes();
while j < t.len() && bytes[j].is_ascii_hexdigit() {
j += 1;
}
if j > ix + 2 {
if let Ok(v) = u64::from_str_radix(&t[ix + 2..j], 16) {
addr1 = Some(v);
}
}
}
}
if t.starts_with("(numbers+2) = ") {
if t.ends_with("30") {
saw_30 = true;
}
if let Some(ix) = t.rfind("0x") {
let mut j = ix + 2;
let bytes = t.as_bytes();
while j < t.len() && bytes[j].is_ascii_hexdigit() {
j += 1;
}
if j > ix + 2 {
if let Ok(v) = u64::from_str_radix(&t[ix + 2..j], 16) {
addr2 = Some(v);
}
}
}
}
}
if !(saw_20 && saw_30) {
if let (Some(a1), Some(a2)) = (addr1, addr2) {
assert_eq!(
a2.wrapping_sub(a1),
4,
"expected address delta 4 bytes.\nSTDOUT: {stdout}\nSTDERR: {stderr}"
);
} else {
panic!("expected (numbers+1)=20 and (numbers+2)=30, or address delta=4.\nSTDOUT: {stdout}\nSTDERR: {stderr}");
}
}
Ok(())
}
#[tokio::test]
async fn test_string_variable_copy_allowed_e2e() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
let s = "A";
let p = s;
print p;
}
"#;
let (exit_code, _stdout, stderr) = run_ghostscope_with_script(script_content, 5).await?;
assert_eq!(exit_code, 0, "unexpected error: stderr={stderr}");
Ok(())
}
#[tokio::test]
async fn test_assignment_is_rejected_with_friendly_message_e2e() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
let a = G_STATE.lib;
a = G_STATE;
if memcmp(a, hex("00"), 1) { print "A"; }
else if memcmp(gm, hex("48"), 1) { print "B"; }
else { print "C"; }
}
"#;
let (exit_code, _stdout, stderr) = run_ghostscope_with_script(script_content, 2).await?;
assert!(
exit_code != 0,
"expected compile-time error; stderr={stderr}"
);
assert!(
stderr.contains("Assignment is not supported: variables are immutable"),
"stderr should contain friendly assignment error. stderr={stderr}"
);
Ok(())
}
#[tokio::test]
async fn test_logical_mixed_precedence() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
print "MIX:{}|{}", (1 || 0 && 0), (0 || 1 && 0);
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 5).await?;
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
let expected = "MIX:true|false";
assert!(
stdout.contains(expected),
"Expected \"{expected}\". STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_logical_and_short_circuit_chain() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
// true && true && false => false
print (1 && 1 && 0);
// true && true && true => true
print (1 && 1 && 1);
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 5).await?;
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
let saw_true = stdout.contains("true");
let saw_false = stdout.contains("false");
assert!(
saw_true,
"Expected at least one true result. STDOUT: {stdout}"
);
assert!(
saw_false,
"Expected at least one false result. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_syntax_error() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
print "missing semicolon" // Missing semicolon - should cause parse error
invalid_token_here
}
"#;
println!("=== Syntax Error Test ===");
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 5).await?;
println!("Exit code: {exit_code}");
println!("STDOUT: {stdout}");
println!("STDERR: {stderr}");
println!("=========================");
assert_ne!(exit_code, 0, "Invalid syntax should cause non-zero exit");
assert!(
stderr.contains("Parse error") || stderr.contains("not running"),
"Should contain parse error: {stderr}"
);
if stderr.contains("Parse error") {
println!("✓ Syntax error correctly detected and rejected");
} else {
println!(
"○ Ghostscope exited because target process ended before parsing (stderr: {})",
stderr.trim()
);
}
Ok(())
}
#[tokio::test]
async fn test_format_mismatch() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
print "format {} {} but only one arg", a; // Format/argument count mismatch
}
"#;
println!("=== Format Mismatch Test ===");
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 5).await?;
println!("Exit code: {exit_code}");
println!("STDOUT: {stdout}");
println!("STDERR: {stderr}");
println!("============================");
assert_ne!(exit_code, 0, "Format mismatch should cause non-zero exit");
if stderr.contains("Parse error")
|| stderr.contains("Type error")
|| stderr.contains("format")
|| stderr.contains("placeholders")
{
println!("✓ Format mismatch correctly detected");
} else {
println!("⚠️ Expected format validation error, got: {stderr}");
}
Ok(())
}
#[tokio::test]
async fn test_nonexistent_function() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace nonexistent_function_12345 {
print "This function does not exist in sample_program";
}
"#;
println!("=== Nonexistent Function Test ===");
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 5).await?;
println!("Exit code: {exit_code}");
println!("STDOUT: {stdout}");
println!("STDERR: {stderr}");
println!("=================================");
assert_ne!(
exit_code, 0,
"Nonexistent function should cause non-zero exit"
);
assert!(
!stderr.contains("Parse error"),
"Script syntax should be valid: {stderr}"
);
if stderr.contains("No uprobe configurations created") {
println!("✓ Correctly detected that target function doesn't exist");
} else {
println!("⚠️ Expected 'No uprobe configurations' error, got: {stderr}");
}
Ok(())
}
#[tokio::test]
async fn test_function_level_tracing() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
print "CALC: a={} b={}", a, b;
}
"#;
let optimization_levels = [OptimizationLevel::Debug, OptimizationLevel::O2];
for opt_level in &optimization_levels {
println!(
"=== Function Level Tracing Test ({}) ===",
opt_level.description()
);
if *opt_level != OptimizationLevel::Debug {
println!(
"⏭️ Skipping {} run (TODO: handle inlined symbols without full debug info)",
opt_level.description()
);
continue;
}
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_opt(script_content, 3, *opt_level).await?;
println!("Exit code: {exit_code}");
println!("STDOUT: {stdout}");
println!("STDERR: {stderr}");
println!("===============================================");
assert_eq!(
exit_code,
0,
"Ghostscope should succeed for {} (stderr: {})",
opt_level.description(),
stderr
);
println!("✓ Ghostscope attached and ran successfully");
let mut math_validations = 0;
let mut function_calls_found = 0;
let mut validation_errors = Vec::new();
for line in stdout.lines() {
if line.contains("CALC: ") {
function_calls_found += 1;
if let Some((a, b)) = parse_calc_line_simple(line) {
if a == b - 5 {
println!("✓ Math validation passed: a={} == b-5={}", a, b - 5);
math_validations += 1;
} else {
let error_msg =
format!("Math validation failed: a={a} != b-5={} (b={b})", b - 5);
println!("❌ {error_msg}");
validation_errors.push(error_msg);
}
} else {
println!("⚠️ Failed to parse line: {line}");
}
}
}
if function_calls_found == 0 {
panic!("❌ No function calls captured - test failed. Expected at least one calculate_something call. This indicates either:\n 1. sample_program is not running\n 2. Function is not being called\n 3. Ghostscope failed to attach properly");
} else if !validation_errors.is_empty() {
panic!("❌ Function calls captured but math validation failed:\n Found {} function calls, {} validation errors:\n {}",
function_calls_found, validation_errors.len(), validation_errors.join("\n "));
} else if math_validations > 0 {
println!("✓ Validated {math_validations} calculate_something calls");
}
println!("===============================================");
}
Ok(())
}
#[tokio::test]
async fn test_multiple_trace_targets() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
print "FUNC: a={} b={}", a, b;
}
trace sample_program.c:16 {
print "LINE16: a={} b={} result={}", a, b, result;
}
"#;
let optimization_levels = [OptimizationLevel::Debug, OptimizationLevel::O2];
for opt_level in &optimization_levels {
println!(
"=== Multiple Trace Targets Test ({}) ===",
opt_level.description()
);
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_opt(script_content, 3, *opt_level).await?;
println!("Exit code: {exit_code}");
println!("STDOUT: {stdout}");
println!("STDERR: {stderr}");
println!("=====================================");
assert!(
!stderr.contains("Parse error"),
"Multi-target script should have valid syntax: {stderr}"
);
assert_eq!(
exit_code,
0,
"Ghostscope should succeed for {} (stderr: {})",
opt_level.description(),
stderr
);
println!("✓ Multiple trace targets attached and ran successfully");
let has_func = stdout.contains("FUNC:");
let has_line16 = stdout.contains("LINE16:");
assert!(
has_func,
"Expected function-level trace output for {} but none was captured. STDOUT: {}",
opt_level.description(),
stdout
);
assert!(
has_line16,
"Expected line-level trace output for {} but none was captured. STDOUT: {}",
opt_level.description(),
stdout
);
println!("Trace capture status: FUNC={has_func}, LINE16={has_line16}");
let mut func_validations = 0;
let mut line_validations = 0;
let mut validation_errors = Vec::new();
let func_has_placeholder_zero = stdout.lines().any(|line| line.contains("FUNC: a=0 b=0"));
let func_has_optimized_marker = stdout
.lines()
.any(|line| line.contains("FUNC:") && line.to_lowercase().contains("optimiz"));
let line_has_optimized_marker = stdout
.lines()
.any(|line| line.contains("LINE16:") && line.to_lowercase().contains("optimiz"));
for line in stdout.lines() {
if line.contains("FUNC: ") {
if let Some((a, b)) = parse_calc_line_simple(line) {
if *opt_level == OptimizationLevel::Debug || (a != 0 || b != 0) {
if a == b - 5 {
println!(
"✓ Function-level math validation passed: a={} == b-5={}",
a,
b - 5
);
func_validations += 1;
} else {
let error_msg = format!(
"Function-level validation failed: a={} != b-5={}",
a,
b - 5
);
println!("❌ {error_msg}");
validation_errors.push(error_msg);
}
}
}
}
}
for line in stdout.lines() {
if line.contains("LINE16: ") {
if let Some((a, b, result)) = parse_line16_trace(line) {
let expected = a * b + 42;
if result == expected {
println!("✓ Line-level math validation passed: {a} * {b} + 42 = {result}");
line_validations += 1;
} else {
let error_msg = format!(
"Line-level validation failed: {a} * {b} + 42 = {expected} but got {result}"
);
println!("❌ {error_msg}");
validation_errors.push(error_msg);
}
}
}
}
if *opt_level == OptimizationLevel::Debug {
if func_validations == 0 {
panic!(
"❌ Expected function-level traces for {} but none validated successfully. STDOUT: {}",
opt_level.description(),
stdout
);
}
if line_validations == 0 {
panic!(
"❌ Expected line-level traces for {} but none validated successfully. STDOUT: {}",
opt_level.description(),
stdout
);
}
} else {
assert!(
!func_has_placeholder_zero,
"Should not emit placeholder optimized-out values in optimized builds. STDOUT: {stdout}"
);
if func_validations == 0 && !func_has_optimized_marker {
panic!(
"❌ Expected function-level traces to be either numerically valid or marked as optimized-out. STDOUT: {stdout}"
);
}
if line_validations == 0 && !line_has_optimized_marker {
panic!(
"❌ Expected line-level traces to be either numerically valid or marked as optimized-out. STDOUT: {stdout}"
);
}
}
if !validation_errors.is_empty() {
panic!(
"❌ Traces captured but validation failed:\n Function validations: {}, Line validations: {}\n Errors: {}",
func_validations,
line_validations,
validation_errors.join("\n ")
);
}
println!(
"✓ Multiple trace targets validated successfully: {func_validations} function traces, {line_validations} line traces"
);
println!("=====================================");
}
Ok(())
}
fn parse_calc_line_simple(line: &str) -> Option<(i32, i32)> {
let line = line
.trim_start_matches("CALC: ")
.trim_start_matches("FUNC: ");
let mut a = None;
let mut b = None;
for part in line.split_whitespace() {
if let Some(value_str) = part.strip_prefix("a=") {
a = value_str.parse().ok();
} else if let Some(value_str) = part.strip_prefix("b=") {
b = value_str.parse().ok();
}
}
match (a, b) {
(Some(a_val), Some(b_val)) => Some((a_val, b_val)),
_ => {
println!("⚠️ Failed to parse a and b from line: {line}");
None
}
}
}
#[tokio::test]
async fn test_line_level_tracing() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace sample_program.c:16 {
print "LINE16: a={} b={} result={}", a, b, result;
}
"#;
println!("=== Line Level Tracing Test ===");
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 3).await?;
println!("Exit code: {exit_code}");
println!("STDOUT: {stdout}");
println!("STDERR: {stderr}");
println!("===============================================");
assert_eq!(exit_code, 0, "Ghostscope should succeed (stderr: {stderr})");
if exit_code == 0 {
println!("✓ Ghostscope attached and ran successfully");
let mut math_validations = 0;
let mut function_calls_found = 0;
let mut validation_errors = Vec::new();
for line in stdout.lines() {
if line.contains("LINE16: ") {
function_calls_found += 1;
if let Some((a, b, result)) = parse_line16_trace(line) {
let expected = a * b + 42;
if result == expected {
println!("✓ Math validation passed: {a} * {b} + 42 = {result}");
math_validations += 1;
} else {
let error_msg = format!(
"Math validation failed: {a} * {b} + 42 = {expected} but got {result}"
);
println!("❌ {error_msg}");
validation_errors.push(error_msg);
}
} else {
println!("⚠️ Failed to parse line: {line}");
}
}
}
if function_calls_found == 0 {
panic!("❌ No line traces captured - test failed. Expected at least one line:16 execution trace. This indicates either:\n 1. sample_program is not running\n 2. Line 16 is not being executed\n 3. Line-level tracing failed to attach");
} else if !validation_errors.is_empty() {
panic!("❌ Line traces captured but math validation failed:\n Found {} line executions, {} validation errors:\n {}",
function_calls_found, validation_errors.len(), validation_errors.join("\n "));
} else if math_validations > 0 {
println!("✓ Validated {math_validations} line:16 executions with correct math");
}
}
Ok(())
}
fn parse_line16_trace(line: &str) -> Option<(i32, i32, i32)> {
let line = line.trim_start_matches("LINE16: ");
let mut a = None;
let mut b = None;
let mut result = None;
for part in line.split_whitespace() {
if let Some(value_str) = part.strip_prefix("a=") {
a = value_str.parse().ok();
} else if let Some(value_str) = part.strip_prefix("b=") {
b = value_str.parse().ok();
} else if let Some(value_str) = part.strip_prefix("result=") {
result = value_str.parse().ok();
}
}
match (a, b, result) {
(Some(a_val), Some(b_val), Some(result_val)) => Some((a_val, b_val, result_val)),
_ => {
println!("⚠️ Failed to parse line16 trace: {line}");
None
}
}
}
#[tokio::test]
async fn test_print_variables_directly() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
print a;
print b;
}
"#;
println!("=== Print Variables Directly Test ===");
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 3).await?;
println!("Exit code: {exit_code}");
println!("STDOUT: {stdout}");
println!("STDERR: {stderr}");
println!("======================================");
assert!(
!stderr.contains("Parse error"),
"Print variables script should have valid syntax: {stderr}"
);
assert_eq!(exit_code, 0, "Ghostscope should succeed (stderr: {stderr})");
if exit_code == 0 {
println!("✓ Print variables script attached successfully");
let mut variable_prints = 0;
for line in stdout.lines() {
if line.trim().parse::<i32>().is_ok() {
variable_prints += 1;
println!("✓ Found variable print: {}", line.trim());
}
}
if variable_prints > 0 {
println!("✓ Successfully captured {variable_prints} variable prints");
} else {
println!("⚠️ No direct variable prints captured");
}
}
Ok(())
}
#[tokio::test]
async fn test_custom_variables() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
let sum = a + b;
let diff = a - b;
let product = a * b;
print "CUSTOM: sum={} diff={} product={}", sum, diff, product;
}
"#;
println!("=== Custom Variables Test ===");
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 3).await?;
println!("Exit code: {exit_code}");
println!("STDOUT: {stdout}");
println!("STDERR: {stderr}");
println!("==============================");
assert!(
!stderr.contains("Parse error"),
"Custom variables script should have valid syntax: {stderr}"
);
assert_eq!(exit_code, 0, "Ghostscope should succeed (stderr: {stderr})");
if exit_code == 0 {
println!("✓ Custom variables script attached successfully");
let mut custom_var_outputs = 0;
let mut math_validations = 0;
for line in stdout.lines() {
if line.contains("CUSTOM: ") {
custom_var_outputs += 1;
if let Some((a, b, sum, diff, product)) = parse_custom_variables_line(line) {
let expected_sum = a + b;
let expected_diff = a - b;
let expected_product = a * b;
if sum == expected_sum && diff == expected_diff && product == expected_product {
math_validations += 1;
println!("✓ Custom variables validated: sum={a}+{b}={sum}, diff={a}-{b}={diff}, product={a}*{b}={product}");
} else {
println!("❌ Custom variables validation failed:");
println!(
" Expected: sum={expected_sum}, diff={expected_diff}, product={expected_product}"
);
println!(" Got: sum={sum}, diff={diff}, product={product}");
}
}
}
}
if custom_var_outputs > 0 && math_validations > 0 {
println!("✓ Successfully validated {math_validations} custom variable calculations");
} else if custom_var_outputs > 0 {
println!("⚠️ Custom variable outputs captured but validation failed");
} else {
println!("⚠️ No custom variable outputs captured");
}
}
Ok(())
}
fn parse_custom_variables_line(line: &str) -> Option<(i32, i32, i32, i32, i32)> {
let line = line.trim_start_matches("CUSTOM: ");
let mut sum = None;
let mut diff = None;
let mut product = None;
for part in line.split_whitespace() {
if let Some(value_str) = part.strip_prefix("sum=") {
sum = value_str.parse().ok();
} else if let Some(value_str) = part.strip_prefix("diff=") {
diff = value_str.parse().ok();
} else if let Some(value_str) = part.strip_prefix("product=") {
product = value_str.parse().ok();
}
}
match (sum, diff, product) {
(Some(sum_val), Some(diff_val), Some(product_val)) => {
let a = (sum_val + diff_val) / 2;
let b = (sum_val - diff_val) / 2;
Some((a, b, sum_val, diff_val, product_val))
}
_ => {
println!("⚠️ Failed to parse custom variables line: {line}");
None
}
}
}
struct TestProgramInstance {
child: tokio::process::Child,
pid: u32,
}
impl TestProgramInstance {
async fn terminate(mut self) -> anyhow::Result<()> {
println!("🛑 Terminating sample_program (PID: {})", self.pid);
let _ = self.child.kill().await.is_ok();
match timeout(Duration::from_secs(2), self.child.wait()).await {
Ok(_) => println!("✓ Test_program terminated gracefully"),
Err(_) => {
let _ = std::process::Command::new("kill")
.arg("-KILL")
.arg(self.pid.to_string())
.output();
println!("⚠️ Force killed sample_program");
}
}
Ok(())
}
}
async fn start_independent_sample_program() -> anyhow::Result<TestProgramInstance> {
let binary_path = FIXTURES.get_test_binary("sample_program")?;
let child = Command::new(binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = child
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
Ok(TestProgramInstance { child, pid })
}
async fn run_ghostscope_with_specific_pid(
script_content: &str,
target_pid: u32,
timeout_secs: u64,
) -> anyhow::Result<(i32, String, String)> {
common::runner::GhostscopeRunner::new()
.with_script(script_content)
.with_pid(target_pid)
.timeout_secs(timeout_secs)
.enable_sysmon_shared_lib(false)
.run()
.await
}
#[tokio::test]
async fn test_invalid_pid_handling() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
print "Should never see this: a={} b={}", a, b;
}
"#;
println!("=== Invalid PID Handling Test ===");
let fake_pid = 999999;
let (exit_code, stdout, stderr) =
run_ghostscope_with_specific_pid(script_content, fake_pid, 3).await?;
println!("Exit code: {exit_code}");
println!("STDOUT: {stdout}");
println!("STDERR: {stderr}");
println!("=====================================");
assert_ne!(exit_code, 0, "Invalid PID should cause non-zero exit");
assert!(
stderr.contains("No such process")
|| stderr.contains("Failed to attach")
|| stderr.contains("Invalid PID")
|| stderr.contains("Permission denied")
|| stderr.contains("Operation not permitted")
|| stderr.contains("is not running"),
"Should contain appropriate error message: {stderr}"
);
println!("✓ Invalid PID correctly rejected");
Ok(())
}
#[tokio::test]
async fn test_string_comparison_char_ptr() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace log_activity {
if (activity == "main_loop") {
print "CSTR_EQ";
}
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 5).await?;
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
stdout.contains("CSTR_EQ"),
"Expected to see CSTR_EQ when activity == 'main_loop'. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_string_comparison_char_array() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace process_record {
if (record.name == "test_record") {
print "ARR_EQ";
}
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 6).await?;
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
stdout.contains("ARR_EQ"),
"Expected to see ARR_EQ when record.name == \"test_record\". STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_builtins_strncmp_starts_with_activity() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace log_activity {
print "SN:{}", strncmp(activity, "main", 4);
print "SW:{}", starts_with(activity, "main");
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 6).await?;
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
stdout.lines().any(|l| l.contains("SN:true")),
"Expected SN:true for strncmp(activity, \"main\", 4). STDOUT: {stdout}"
);
assert!(
stdout.lines().any(|l| l.contains("SW:true")),
"Expected SW:true for starts_with(activity, \"main\"). STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_builtin_strncmp_on_struct_pointer_mismatch() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace process_record {
print "REC_SN:{}", strncmp(record, "HTTP", 4);
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 6).await?;
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
stdout.lines().any(|l| l.contains("REC_SN:false")),
"Expected REC_SN:false for non-string pointer compare. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_bool_literals_in_expressions() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace log_activity {
// positive cases
print "B1:{}", starts_with(activity, "main") == true;
print "B4:{}", true == starts_with(activity, "main");
// unary not
print "BN1:{}", !starts_with(activity, "main");
// negative (non-match literal)
print "B6:{}", starts_with(activity, "zzz") == false;
}
trace process_record {
// positive cases
print "B2:{}", strncmp(record, "HTTP", 4) == false;
print "B3:{}", false == strncmp(record, "HTTP", 4);
// unary not
print "BN2:{}", !strncmp(record, "HTTP", 4);
// negative case (should be false)
print "B5:{}", strncmp(record, "HTTP", 4) == true;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script(script_content, 6).await?;
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
stdout.lines().any(|l| l.contains("B1:true")),
"Expected B1:true. STDOUT: {stdout}"
);
assert!(
stdout.lines().any(|l| l.contains("B2:true")),
"Expected B2:true. STDOUT: {stdout}"
);
assert!(
stdout.lines().any(|l| l.contains("B3:true")),
"Expected B3:true. STDOUT: {stdout}"
);
assert!(
stdout.lines().any(|l| l.contains("B4:true")),
"Expected B4:true. STDOUT: {stdout}"
);
assert!(
stdout.lines().any(|l| l.contains("B6:true")),
"Expected B6:true. STDOUT: {stdout}"
);
assert!(
stdout.lines().any(|l| l.contains("B5:false")),
"Expected B5:false. STDOUT: {stdout}"
);
assert!(
stdout.lines().any(|l| l.contains("BN1:false")),
"Expected BN1:false. STDOUT: {stdout}"
);
assert!(
stdout.lines().any(|l| l.contains("BN2:true")),
"Expected BN2:true. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_correct_pid_filtering() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
print "FILTERED: a={} b={}", a, b;
}
"#;
println!("=== Correct PID Filtering Test ===");
let sample_program_1 = start_independent_sample_program().await?;
let sample_program_2 = start_independent_sample_program().await?;
println!(
"Started sample_program_1 with PID: {}",
sample_program_1.pid
);
println!(
"Started sample_program_2 with PID: {}",
sample_program_2.pid
);
let (exit_code, stdout, stderr) =
run_ghostscope_with_specific_pid(script_content, sample_program_1.pid, 3).await?;
println!("Exit code: {exit_code}");
println!("STDOUT: {stdout}");
println!("STDERR: {stderr}");
println!("=====================================");
sample_program_1.terminate().await?;
sample_program_2.terminate().await?;
if exit_code == 0 {
let filtered_outputs = stdout
.lines()
.filter(|line| line.contains("FILTERED:"))
.count();
if filtered_outputs > 0 {
println!("✓ Successfully captured {filtered_outputs} function calls from target PID");
println!("✓ PID filtering working correctly");
} else {
println!("⚠️ No function calls captured, but PID filtering test completed");
}
} else {
println!("⚠️ Unexpected exit code: {exit_code}. STDERR: {stderr}");
}
Ok(())
}
#[tokio::test]
async fn test_pid_specificity_with_multiple_processes() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
let script_content = r#"
trace calculate_something {
print "TARGET_ONLY: traced a={} b={}", a, b;
}
"#;
println!("=== PID Specificity with Multiple Processes Test ===");
let programs = vec![
start_independent_sample_program().await?,
start_independent_sample_program().await?,
start_independent_sample_program().await?,
];
for (i, program) in programs.iter().enumerate() {
println!("Started sample_program_{} with PID: {}", i + 1, program.pid);
}
let target_pid = programs[1].pid;
let (exit_code, stdout, stderr) =
run_ghostscope_with_specific_pid(script_content, target_pid, 4).await?;
println!("Target PID: {target_pid}");
println!("Exit code: {exit_code}");
println!("STDOUT: {stdout}");
println!("STDERR: {stderr}");
println!("==================================================");
for program in programs {
program.terminate().await?;
}
if exit_code == 0 {
let traced_outputs = stdout
.lines()
.filter(|line| line.contains("TARGET_ONLY:"))
.count();
if traced_outputs > 0 {
println!("✓ Successfully captured {traced_outputs} function calls");
println!("✓ PID specificity verified - only target process traced");
} else {
println!("⚠️ No function calls captured during test window");
}
} else {
println!("⚠️ Unexpected exit code: {exit_code}. STDERR: {stderr}");
}
Ok(())
}
#[tokio::test]
#[serial_test::serial]
async fn test_stripped_binary_with_debuglink() -> anyhow::Result<()> {
init();
ensure_global_cleanup_registered();
common::ensure_test_program_compiled_with_opt(OptimizationLevel::Stripped)?;
let script_content = r#"
trace add_numbers {
print "STRIPPED_BINARY: add_numbers called with a={} b={}", a, b;
}
"#;
println!("=== Stripped Binary with .gnu_debuglink Test ===");
let binary_path =
FIXTURES.get_test_binary_with_opt("sample_program", OptimizationLevel::Stripped)?;
println!("Binary path: {}", binary_path.display());
let debug_file = binary_path.with_file_name("sample_program_stripped.debug");
assert!(
debug_file.exists(),
"Debug file should exist: {}",
debug_file.display()
);
println!("Debug file found: {}", debug_file.display());
let output = std::process::Command::new("readelf")
.args(["-S", binary_path.to_str().unwrap()])
.output()?;
let sections_output = String::from_utf8_lossy(&output.stdout);
if sections_output.contains(".debug_info") {
println!("⚠️ Warning: Binary still contains .debug_info section");
} else {
println!("✓ Binary is stripped (no .debug_info section)");
}
if sections_output.contains(".gnu_debuglink") {
println!("✓ Binary has .gnu_debuglink section");
} else {
println!("⚠️ Warning: Binary missing .gnu_debuglink section");
}
let mut child = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = child.id().expect("Failed to get PID");
println!("Started stripped binary with PID: {pid}");
tokio::time::sleep(Duration::from_millis(100)).await;
let (exit_code, stdout, stderr) =
run_ghostscope_with_specific_pid(script_content, pid, 3).await?;
println!("Exit code: {exit_code}");
println!("STDOUT: {stdout}");
println!("STDERR: {stderr}");
println!("===============================================");
let _ = child.kill().await.is_ok();
if exit_code == 0 {
let traced_outputs = stdout
.lines()
.filter(|line| line.contains("STRIPPED_BINARY:"))
.count();
if traced_outputs > 0 {
println!("✓ Successfully traced {traced_outputs} function calls from stripped binary");
println!("✓ .gnu_debuglink mechanism working correctly");
println!("✓ Uprobe offset calculation correct for stripped binary");
} else {
println!("⚠️ No function calls captured, but debuglink loading succeeded");
}
if stderr.contains("Looking for debug file")
|| stderr.contains("Loading DWARF from separate debug file")
{
println!("✓ Confirmed: Debug info loaded from .gnu_debuglink");
}
} else {
if stderr.contains("No debug information found") {
println!("✗ Failed: Could not load debug information from .gnu_debuglink");
anyhow::bail!("Debug information not found - .gnu_debuglink not working");
} else {
println!("⚠️ Unexpected exit code: {exit_code}. STDERR: {stderr}");
}
}
Ok(())
}