mod common;
use common::{init, OptimizationLevel, FIXTURES};
use std::process::Stdio;
use std::time::Duration;
use tokio::process::Command;
async fn run_ghostscope_with_script_for_pid(
script_content: &str,
timeout_secs: u64,
pid: u32,
) -> anyhow::Result<(i32, String, String)> {
common::runner::GhostscopeRunner::new()
.with_script(script_content)
.with_pid(pid)
.timeout_secs(timeout_secs)
.enable_sysmon_shared_lib(false)
.run()
.await
}
async fn run_ghostscope_with_script_for_pid_perf(
script_content: &str,
timeout_secs: u64,
pid: u32,
) -> anyhow::Result<(i32, String, String)> {
common::runner::GhostscopeRunner::new()
.with_script(script_content)
.with_pid(pid)
.timeout_secs(timeout_secs)
.force_perf_event_array(true)
.enable_sysmon_shared_lib(false)
.run()
.await
}
#[tokio::test]
async fn test_memcmp_int_array_decay_to_pointer() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace update_complex {
if memcmp(c.arr, c.arr, 16) { print "ARR_EQ"; }
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 2, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
stdout.contains("ARR_EQ"),
"Expected ARR_EQ. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_entry_prints() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:7 {
print &*&*c; // pointer address of c (struct Complex*)
print c.friend_ref; // pointer value or NULL
print c.name; // char[16] -> string
print *c.friend_ref; // dereferenced struct (or null-deref error)
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
assert_eq!(
exit_code, 0,
"ghostscope should run successfully (stderr={stderr}, stdout={stdout})"
);
let has_any_ptr = stdout.contains("0x") && stdout.contains("(Complex*)");
assert!(
has_any_ptr,
"Expected pointer print with type suffix. STDOUT: {stdout}"
);
let has_name = stdout.contains("\"Alice\"") || stdout.contains("\"Bob\"");
assert!(has_name, "Expected c.name string. STDOUT: {stdout}");
let has_deref_struct = stdout.contains("*c.friend_ref")
&& (stdout.contains("Complex {") || stdout.contains("<error: null pointer dereference>"));
assert!(
has_deref_struct,
"Expected deref output (struct or null-deref). STDOUT: {stdout}"
);
let _ = prog.kill().await.is_ok();
Ok(())
}
#[tokio::test]
async fn test_memcmp_struct_name_equal_and_diff() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace update_complex {
if memcmp(&c.name[0], &c.name[0], 5) { print "CNAME_EQ"; }
if !memcmp(&c.name[0], &c.name[1], 5) { print "CNAME_NE"; }
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 4, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
stdout.contains("CNAME_EQ"),
"Expected CNAME_EQ. STDOUT: {stdout}"
);
assert!(
stdout.contains("CNAME_NE"),
"Expected CNAME_NE. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_memcmp_dynamic_and_zero_negative_on_name() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace update_complex {
// len=0 -> true
if memcmp(&c.name[0], &c.name[1], 0) { print "Z0"; }
// dynamic len from script var
let n = 8;
if memcmp(&c.name[0], &c.name[0], n) { print "DYN_OK"; }
// negative clamps to 0 -> true
let k = -3;
if memcmp(&c.name[0], &c.name[1], k) { print "NEG_OK"; }
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 4, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(stdout.contains("Z0"), "Expected Z0. STDOUT: {stdout}");
assert!(
stdout.contains("DYN_OK"),
"Expected DYN_OK. STDOUT: {stdout}"
);
assert!(
stdout.contains("NEG_OK"),
"Expected NEG_OK. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_string_comparison_struct_char_array() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace update_complex {
if (c.name == "Alice") { print "CNAME_A"; }
if (c.name == "Bob") { print "CNAME_B"; }
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 4, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
let saw_a = stdout.contains("CNAME_A");
let saw_b = stdout.contains("CNAME_B");
assert!(
saw_a || saw_b,
"Expected to see at least Alice or Bob. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_local_array_constant_index_format() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:25 {
print "ARR:{}|BRR:{}", a.arr[1], b.arr[0];
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
use regex::Regex;
let re_arr = Regex::new(r"ARR:(-?\d+)").unwrap();
let re_brr = Regex::new(r"BRR:(-?\d+)").unwrap();
let has_arr = stdout.lines().any(|l| re_arr.is_match(l));
let has_brr = stdout.lines().any(|l| re_brr.is_match(l));
assert!(
has_arr,
"Expected formatted ARR value from a.arr[1]. STDOUT: {stdout}"
);
assert!(
has_brr,
"Expected formatted BRR value from b.arr[0]. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_local_chain_tail_array_index_format() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:25 {
print "CF:{}|AF:{}", b.friend_ref.arr[1], a.arr[2];
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
use regex::Regex;
let re_cf = Regex::new(r"CF:(-?\d+)").unwrap();
let re_af = Regex::new(r"AF:(-?\d+)").unwrap();
let has_cf = stdout.lines().any(|l| re_cf.is_match(l));
let has_af = stdout.lines().any(|l| re_af.is_match(l));
assert!(
has_cf,
"Expected CF value from b.friend_ref.arr[1]. STDOUT: {stdout}"
);
assert!(has_af, "Expected AF value from a.arr[2]. STDOUT: {stdout}");
Ok(())
}
#[tokio::test]
async fn test_local_array_constant_index_access() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:25 {
print "AR:{}", a.arr[1];
print "BR:{}", b.arr[0];
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
use regex::Regex;
let re_ar = Regex::new(r"AR:(-?\d+)").unwrap();
let re_br = Regex::new(r"BR:(-?\d+)").unwrap();
let has_ar = stdout.lines().any(|l| re_ar.is_match(l));
let has_br = stdout.lines().any(|l| re_br.is_match(l));
assert!(
has_ar,
"Expected at least one numeric a.arr[1] sample. STDOUT: {stdout}"
);
assert!(
has_br,
"Expected at least one numeric b.arr[0] sample. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_cross_type_comparisons_local() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:25 {
let t = 100;
print "GT:{} EQ:{} PZ:{} LT:{}",
a.age > 26,
a.status == 0,
a.friend_ref == 0,
a.age < t;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 4, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
use regex::Regex;
let re =
Regex::new(r"GT:(true|false) EQ:(true|false) PZ:(true|false) LT:(true|false)").unwrap();
let mut saw_line = false;
let mut saw_pz_true = false;
for line in stdout.lines() {
if let Some(c) = re.captures(line) {
saw_line = true;
if &c[3] == "true" {
saw_pz_true = true; }
}
}
assert!(
saw_line,
"Expected at least one comparison line. STDOUT: {stdout}"
);
assert!(
saw_pz_true,
"Expected PZ:1 for pointer==0. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_special_vars_pid_tid_timestamp_complex() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = format!(
"trace complex_types_program.c:25 {{\n print \"PID={} TID={} TS={}\", $pid, $tid, $timestamp;\n if $pid == {} {{ print \"PID_OK\"; }}\n}}\n",
"{}", "{}", "{}", pid
);
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(&script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
stdout.contains("PID_OK"),
"Expected PID_OK. STDOUT: {stdout}"
);
assert!(
stdout.contains("PID=") || stdout.contains("PID:"),
"Expected PID field in output. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_if_else_if_and_bare_expr_local() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:25 {
// bare expression print should render name = value
print a.status == 0;
if a.status == 0 {
print "wtf";
} else if a.status == 1 {
print a.age == 0;
}
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 4, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
let has_status_line = stdout
.lines()
.any(|l| l.contains("(a.status==0) = true") || l.contains("(a.status==0) = false"));
assert!(
has_status_line,
"Expected bare expression output for a.status==0. STDOUT: {stdout}"
);
let has_then = stdout.lines().any(|l| l.contains("wtf"));
let has_elseif_expr = stdout
.lines()
.any(|l| l.contains("(a.age==0) = true") || l.contains("(a.age==0) = false"));
assert!(
has_then || has_elseif_expr,
"Expected either then-branch 'wtf' or else-if expr output. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_if_else_if_logical_ops_local() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:25 {
// Truthiness check for script ints
let x = 2; let y = 1; let z = 0;
print "AND:{} OR:{}", x && y, x || z;
// DWARF-backed locals with logical ops
if a.age > 26 && a.status == 0 { print "AND"; }
else if a.age < 100 || a.friend_ref == 0 { print "OR"; }
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 4, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
use regex::Regex;
let re = Regex::new(r"AND:(true|false) OR:(true|false)").unwrap();
let mut saw_fmt = false;
for line in stdout.lines() {
if re.is_match(line) {
saw_fmt = true;
break;
}
}
assert!(saw_fmt, "Expected logical fmt line. STDOUT: {stdout}");
Ok(())
}
#[tokio::test]
async fn test_or_short_circuit_avoids_null_deref() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace update_complex {
print (1 || *c.friend_ref);
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
stdout.contains("true"),
"Expected true result. STDOUT: {stdout}"
);
assert!(
!stdout.contains("<error: null pointer dereference>"),
"Short-circuit should avoid null-deref RHS. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_and_short_circuit_avoids_null_deref() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace update_complex {
print (0 && *c.friend_ref);
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
stdout.contains("false"),
"Expected false result. STDOUT: {stdout}"
);
assert!(
!stdout.contains("<error: null pointer dereference>"),
"Short-circuit should avoid null-deref RHS. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_address_of_and_comparisons_local() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:25 {
// top-level &expr should print as pointer with hex and type suffix
print &a;
// address-of in expression should print name=value
print (&a != 0);
if &a != 0 {
print "ADDR";
}
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 4, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
let has_hex_ptr = stdout.contains("0x");
assert!(has_hex_ptr, "Expected hex pointer for &a. STDOUT: {stdout}");
let has_expr_bool = stdout
.lines()
.any(|l| l.contains("(&a!=0) = true") || l.contains("(&a!=0) = false"));
assert!(
has_expr_bool,
"Expected bare expr output for (&a!=0). STDOUT: {stdout}"
);
let has_then = stdout.lines().any(|l| l.contains("ADDR"));
assert!(has_then, "Expected then-branch ADDR line. STDOUT: {stdout}");
Ok(())
}
#[tokio::test]
async fn test_string_equality_local() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:25 {
print "SE:{}", a.name == "Alice";
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(stdout.contains("SE:true") || stdout.contains("SE:false"));
Ok(())
}
#[tokio::test]
async fn test_entry_pointer_values() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:7 {
print &*&*c;
print c.friend_ref;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
assert_eq!(
exit_code, 0,
"ghostscope should run successfully (stderr={stderr}, stdout={stdout})"
);
assert!(
stdout.contains("0x") && stdout.contains("(Complex*)"),
"Expected pointer formatting with type suffix. STDOUT: {stdout}"
);
let _ = prog.kill().await.is_ok();
Ok(())
}
#[tokio::test]
async fn test_entry_name_string_and_deref_struct_fields() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:7 {
print c.name;
print *c.friend_ref;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
let has_name = stdout.contains("\"Alice\"") || stdout.contains("\"Bob\"");
assert!(has_name, "Expected c.name string. STDOUT: {stdout}");
let mut found_struct = false;
for line in stdout.lines() {
if line.contains("*c.friend_ref = Complex {") {
let has_status = line.contains("status:") && line.contains("Status::");
let has_data = line.contains("data: union Data {");
let has_arr = line.contains("arr: [");
let has_active = line.contains("active:");
let has_flags = line.contains("flags:");
if has_status && has_data && has_arr && has_active && has_flags {
found_struct = true;
break;
}
}
}
assert!(
found_struct,
"Expected at least one full struct deref with fields. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_entry_friend_ref_null_and_non_null_cases() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:7 {
print c.friend_ref;
print *c.friend_ref;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
let saw_null_ptr = stdout.contains("c.friend_ref = NULL (struct Complex*)");
let saw_non_null_ptr = stdout.contains("c.friend_ref = 0x");
let saw_struct_deref = stdout.contains("*c.friend_ref = Complex {");
let saw_null_deref_err = stdout.contains("*c.friend_ref = <error: null pointer dereference>");
assert!(
saw_null_ptr || saw_non_null_ptr,
"Expected at least one friend_ref pointer print. STDOUT: {stdout}"
);
assert!(
saw_struct_deref || saw_null_deref_err,
"Expected deref to produce either struct or null-deref error. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_trace_by_address_nopie_complex_types() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary_complex_nopie()?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&binary_path)
.await
.map_err(|e| anyhow::anyhow!("Failed to load DWARF for Non-PIE test binary: {}", e))?;
let addrs = analyzer.lookup_addresses_by_source_line("complex_types_program.c", 8);
anyhow::ensure!(
!addrs.is_empty(),
"No DWARF addresses found for complex_types_program.c:8"
);
let pc = addrs[0].address;
let script = format!("trace 0x{pc:x} {{\n print \"NP_ADDR_OK\";\n}}\n");
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(&script, 2, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
stdout.lines().any(|l| l.contains("NP_ADDR_OK")),
"Expected NP_ADDR_OK in output. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_complex_types_formatting() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script_content = r#"
trace complex_types_program.c:25 {
print a; // struct
print a.name; // char[N] as string
print "User: {} Age: {} {}", a.name, a.age, a.status;
}
"#;
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_for_pid(script_content, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(
exit_code, 0,
"ghostscope should run successfully. stderr={stderr} stdout={stdout}"
);
let has_struct =
stdout.contains("Complex {") && stdout.contains("name:") && stdout.contains("age:");
assert!(
has_struct,
"Expected struct output with fields. STDOUT: {stdout}"
);
let has_name_str = stdout.contains("\"Alice\"") || stdout.contains("\"Bob\"");
assert!(
has_name_str,
"Expected name string output. STDOUT: {stdout}"
);
let has_arr_field = stdout.contains("arr:");
assert!(
has_arr_field,
"Expected struct output contains arr field. STDOUT: {stdout}"
);
let has_formatted = stdout.contains("User:") && stdout.contains("Age:");
assert!(
has_formatted,
"Expected formatted print output. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_pointer_auto_deref_member_access() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace update_complex {
print c.name;
print "U:{} A:{}", c.name, c.age;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(
exit_code, 0,
"ghostscope should run successfully. stderr={stderr} stdout={stdout}"
);
let has_name = stdout.contains("\"Alice\"") || stdout.contains("\"Bob\"");
assert!(
has_name,
"Expected dereferenced name (\"Alice\" or \"Bob\"). STDOUT: {stdout}"
);
let has_formatted = stdout.contains("U:") && stdout.contains("A:");
assert!(
has_formatted,
"Expected formatted pointer-deref output. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_pointer_auto_deref_source_line_entry() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:6 {
print c.name;
print "U:{} A:{}", c.name, c.age;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(
exit_code, 0,
"ghostscope should run successfully. stderr={stderr} stdout={stdout}"
);
let has_name = stdout.contains("\"Alice\"") || stdout.contains("\"Bob\"");
assert!(
has_name,
"Expected dereferenced name at entry (\"Alice\" or \"Bob\"). STDOUT: {stdout}"
);
let has_formatted = stdout.contains("U:") && stdout.contains("A:");
assert!(
has_formatted,
"Expected formatted pointer-deref output at entry. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_complex_types_formatting_nopie() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary_complex_nopie()?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script_content = r#"
trace complex_types_program.c:25 {
print a; // struct
print a.name;
print "User: {} Age: {} {}", a.name, a.age, a.status;
}
"#;
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_for_pid(script_content, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(
exit_code, 0,
"ghostscope should run successfully. stderr={stderr} stdout={stdout}"
);
let has_struct =
stdout.contains("Complex {") && stdout.contains("name:") && stdout.contains("age:");
assert!(
has_struct,
"Expected struct output with fields. STDOUT: {stdout}"
);
let has_name_str = stdout.contains("\"Alice\"") || stdout.contains("\"Bob\"");
assert!(
has_name_str,
"Expected name string output. STDOUT: {stdout}"
);
let has_arr_field = stdout.contains("arr:");
assert!(
has_arr_field,
"Expected struct output contains arr field. STDOUT: {stdout}"
);
let has_formatted = stdout.contains("User:") && stdout.contains("Age:");
assert!(
has_formatted,
"Expected formatted print output. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_pointer_auto_deref_member_access_nopie() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary_complex_nopie()?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace update_complex {
print c.name;
print "U:{} A:{}", c.name, c.age;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(
exit_code, 0,
"ghostscope should run successfully. stderr={stderr} stdout={stdout}"
);
let has_name = stdout.contains("\"Alice\"") || stdout.contains("\"Bob\"");
assert!(
has_name,
"Expected dereferenced name (\"Alice\" or \"Bob\"). STDOUT: {stdout}"
);
let has_formatted = stdout.contains("U:") && stdout.contains("A:");
assert!(
has_formatted,
"Expected formatted pointer-deref output. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_bitfields_correctness() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script_fn = r#"
trace complex_types_program.c:25 {
print "I={}", i;
print a.active;
print a.flags;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script_fn, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(
exit_code, 0,
"ghostscope should run successfully. stderr={stderr} stdout={stdout}"
);
use regex::Regex;
let re_i = Regex::new(r"I=([0-9]+)").unwrap();
let re_active = Regex::new(r"(?i)(?:\b|\.)active\s*=\s*([0-9]+)").unwrap();
let re_flags = Regex::new(r"(?i)(?:\b|\.)flags\s*=\s*([0-9]+)").unwrap();
let mut found_i: Option<u64> = None;
let mut found_active: Option<u64> = None;
let mut found_flags: Option<u64> = None;
for line in stdout.lines() {
if found_i.is_none() {
if let Some(caps) = re_i.captures(line) {
if let Ok(val) = caps[1].parse::<u64>() {
found_i = Some(val);
}
}
}
if found_active.is_none() {
if let Some(caps) = re_active.captures(line) {
if let Ok(val) = caps[1].parse::<u64>() {
found_active = Some(val);
}
}
}
if found_flags.is_none() {
if let Some(caps) = re_flags.captures(line) {
if let Ok(val) = caps[1].parse::<u64>() {
found_flags = Some(val);
}
}
}
if found_i.is_some() && found_active.is_some() && found_flags.is_some() {
break;
}
}
let i_val = found_i.ok_or_else(|| anyhow::anyhow!("Missing I=... line. STDOUT: {stdout}"))?;
let active_val =
found_active.ok_or_else(|| anyhow::anyhow!("Missing active line. STDOUT: {stdout}"))?;
let flags_val =
found_flags.ok_or_else(|| anyhow::anyhow!("Missing flags line. STDOUT: {stdout}"))?;
assert!(
active_val <= 1,
"active should be 0 or 1, got {active_val}. STDOUT: {stdout}"
);
assert!(
flags_val <= 7,
"flags should be 0..7, got {flags_val}. STDOUT: {stdout}"
);
assert_eq!(
active_val,
i_val & 1,
"active must equal i&1 (i={i_val}, active={active_val})"
);
assert_eq!(
flags_val,
i_val & 7,
"flags must equal i&7 (i={i_val}, flags={flags_val})"
);
Ok(())
}
#[tokio::test]
async fn test_entry_prints_perf() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:7 {
print &*&*c; // pointer address of c (struct Complex*)
print c.friend_ref; // pointer value or NULL
print c.name; // char[16] -> string
print *c.friend_ref; // dereferenced struct (or null-deref error)
}
"#;
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_for_pid_perf(script, 3, pid).await?;
assert_eq!(
exit_code, 0,
"ghostscope should run successfully (stderr={stderr}, stdout={stdout})"
);
let has_any_ptr = stdout.contains("0x") && stdout.contains("(Complex*)");
assert!(
has_any_ptr,
"Expected pointer print with type suffix. STDOUT: {stdout}"
);
let has_name = stdout.contains("\"Alice\"") || stdout.contains("\"Bob\"");
assert!(has_name, "Expected c.name string. STDOUT: {stdout}");
let has_deref_struct = stdout.contains("*c.friend_ref")
&& (stdout.contains("Complex {") || stdout.contains("<error: null pointer dereference>"));
assert!(
has_deref_struct,
"Expected deref output (struct or null-deref). STDOUT: {stdout}"
);
let _ = prog.kill().await.is_ok();
Ok(())
}
#[tokio::test]
async fn test_local_array_constant_index_format_perf() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script = r#"
trace complex_types_program.c:25 {
print "ARR:{}|BRR:{}", a.arr[1], b.arr[0];
}
"#;
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_for_pid_perf(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
use regex::Regex;
let re_arr = Regex::new(r"ARR:(-?\d+)").unwrap();
let re_brr = Regex::new(r"BRR:(-?\d+)").unwrap();
let has_arr = stdout.lines().any(|l| re_arr.is_match(l));
let has_brr = stdout.lines().any(|l| re_brr.is_match(l));
assert!(
has_arr,
"Expected formatted ARR value from a.arr[1]. STDOUT: {stdout}"
);
assert!(
has_brr,
"Expected formatted BRR value from b.arr[0]. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_complex_types_formatting_perf() -> anyhow::Result<()> {
init();
let binary_path =
FIXTURES.get_test_binary_with_opt("complex_types_program", OptimizationLevel::Debug)?;
let mut prog = Command::new(&binary_path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
let pid = prog
.id()
.ok_or_else(|| anyhow::anyhow!("Failed to get PID"))?;
tokio::time::sleep(Duration::from_millis(500)).await;
let script_content = r#"
trace complex_types_program.c:25 {
print a; // struct
print a.name; // char[N] as string
print "User: {} Age: {} {}", a.name, a.age, a.status;
}
"#;
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_for_pid_perf(script_content, 3, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(
exit_code, 0,
"ghostscope should run successfully. stderr={stderr} stdout={stdout}"
);
let has_struct =
stdout.contains("Complex {") && stdout.contains("name:") && stdout.contains("age:");
assert!(
has_struct,
"Expected struct output with fields. STDOUT: {stdout}"
);
let has_name_str = stdout.contains("\"Alice\"") || stdout.contains("\"Bob\"");
assert!(
has_name_str,
"Expected name string output. STDOUT: {stdout}"
);
let has_arr_field = stdout.contains("arr:");
assert!(
has_arr_field,
"Expected struct output contains arr field. STDOUT: {stdout}"
);
let has_formatted = stdout.contains("User:") && stdout.contains("Age:");
assert!(
has_formatted,
"Expected formatted print output. STDOUT: {stdout}"
);
Ok(())
}