mod common;
use common::{init, FIXTURES};
use regex::Regex;
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
}
async fn run_ghostscope_with_script_for_pid_with_log(
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
}
#[tokio::test]
async fn test_memcmp_hex_helper_on_globals() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
if memcmp(gm, hex("48656c6c6f2c20"), 7) { print "HEX_OK"; }
if memcmp(lm, hex("4c49425f"), 4) { print "HEX_LM"; }
}
"#;
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("HEX_OK"),
"Expected HEX_OK. STDOUT: {stdout}"
);
assert!(
stdout.contains("HEX_LM"),
"Expected HEX_LM. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_if_memcmp_failure_emits_exprerror_and_suppress_else() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
if memcmp(G_STATE.lib, hex("00"), 1) { print "THEN"; } else { print "ELSE"; }
print "AFTER";
}
"#;
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}");
assert!(
stdout.contains("ExprError"),
"Expected ExprError warning. STDOUT: {stdout}"
);
assert!(stdout.contains("AFTER"), "Expected AFTER. STDOUT: {stdout}");
assert!(
!stdout.contains("THEN"),
"THEN should be suppressed. STDOUT: {stdout}"
);
assert!(stdout.contains("ELSE"), "Expected ELSE. STDOUT: {stdout}");
Ok(())
}
#[tokio::test]
async fn test_struct_arithmetic_is_rejected_with_friendly_error() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let prog = tokio::process::Command::new(&binary_path)
.current_dir(bin_dir)
.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 tick_once {
print G_STATE + 1;
}
"#;
let (_exit_code, stdout_buf, stderr_buf) =
run_ghostscope_with_script_for_pid_with_log(script, 3, pid).await?;
let has_banner = stderr_buf.contains("Script compilation failed")
|| stderr_buf.contains("No uprobe configurations created");
let has_friendly = stderr_buf
.contains("Unsupported arithmetic/ordered comparison involving struct/union/array")
|| stderr_buf.contains("Pointer arithmetic requires a pointer or array expression");
assert!(
has_banner && has_friendly,
"Expected friendly struct arithmetic rejection.\nSTDERR: {stderr_buf}\nSTDOUT: {stdout_buf}"
);
Ok(())
}
#[tokio::test]
async fn test_unknown_member_on_global_reports_members() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print G_STATE.no_such_member;
}
"#;
let (_exit_code, _stdout, stderr) =
run_ghostscope_with_script_for_pid_with_log(script, 3, pid).await?;
let _ = prog.kill().await.is_ok();
let has_msg = stderr.contains("Unknown member 'no_such_member' in struct")
|| stderr.contains("Unknown member 'no_such_member' in union");
assert!(
has_msg,
"Expected unknown-member friendly message. STDERR: {stderr}"
);
Ok(())
}
#[tokio::test]
async fn test_else_if_continues_after_error() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
if memcmp(G_STATE.lib, 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_for_pid(script, 4, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout} ");
assert!(
stdout.contains("ExprError"),
"Expected ExprError warning. STDOUT: {stdout}"
);
let mut saw_b_token = false;
let mut saw_a_token = false;
let mut saw_c_token = false;
for line in stdout.lines() {
let t = line.trim();
if t == "B" {
saw_b_token = true;
}
if t == "A" {
saw_a_token = true;
}
if t == "C" {
saw_c_token = true;
}
}
assert!(saw_b_token, "Expected B from else-if. STDOUT: {stdout}");
assert!(!saw_a_token, "A should not be printed. STDOUT: {stdout}");
assert!(
!saw_c_token,
"C should be suppressed due to else-if true. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_script_signed_ints_regression() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
let a = -1;
let b = -2;
let c = -3;
print a;
print b;
print c;
print "FMT:{}|{}|{}", a, b, c;
}
"#;
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("a = -1"),
"Expected a = -1. STDOUT: {stdout}"
);
assert!(
stdout.contains("b = -2"),
"Expected b = -2. STDOUT: {stdout}"
);
assert!(
stdout.contains("c = -3"),
"Expected c = -3. STDOUT: {stdout}"
);
assert!(
stdout.contains("FMT:-1|-2|-3"),
"Expected formatted signed values. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_trace_by_address_via_dwarf_line_lookup() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 test binary: {}", e))?;
let addrs = analyzer.lookup_addresses_by_source_line("globals_program.c", 32);
anyhow::ensure!(
!addrs.is_empty(),
"No DWARF addresses found for globals_program.c:32"
);
let pc = addrs[0].address;
let script = format!("trace 0x{pc:x} {{\n print \"ADDR_OK\";\n}}\n");
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.lines().any(|l| l.contains("ADDR_OK")),
"Expected ADDR_OK in output. STDOUT: {stdout}"
);
Ok(())
}
async fn run_ghostscope_with_script_for_target(
script_content: &str,
timeout_secs: u64,
target_path: &std::path::Path,
) -> anyhow::Result<(i32, String, String)> {
common::runner::GhostscopeRunner::new()
.with_script(script_content)
.with_target(target_path)
.timeout_secs(timeout_secs)
.run()
.await
}
#[tokio::test]
async fn test_special_vars_pid_tid_timestamp_globals() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {{\n print \"PID={} TID={} TS={}\", $pid, $tid, $timestamp;\n if $pid == {} {{ print \"PID_EQ\"; }}\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_EQ"),
"Expected PID_EQ. STDOUT: {stdout}"
);
assert!(
stdout.contains("PID=") || stdout.contains("PID:"),
"Expected PID print. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_trace_address_with_target_shared_library() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 lib_path = bin_dir.join("libgvars.so");
anyhow::ensure!(
lib_path.exists(),
"libgvars.so not found at {}",
lib_path.display()
);
let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&lib_path)
.await
.map_err(|e| anyhow::anyhow!("Failed to load DWARF for lib: {}", e))?;
let addrs = analyzer.lookup_function_addresses("lib_tick");
anyhow::ensure!(
!addrs.is_empty(),
"No addresses for lib_tick in libgvars.so"
);
let pc = addrs[0].address;
let script = format!("trace 0x{pc:x} {{\n print \"LIB_ADDR_OK\";\n}}\n");
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_for_target(&script, 2, &lib_path).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(
exit_code, 0,
"stderr={stderr} stdout={stdout} script={script}",
);
assert!(
stdout.lines().any(|l| l.contains("LIB_ADDR_OK")),
"Expected LIB_ADDR_OK in output. STDOUT: {stdout}, script {script}"
);
Ok(())
}
#[tokio::test]
async fn test_trace_module_qualified_address_in_pid_mode() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 lib_path = bin_dir.join("libgvars.so");
anyhow::ensure!(
lib_path.exists(),
"libgvars.so not found at {}",
lib_path.display()
);
let analyzer = ghostscope_dwarf::DwarfAnalyzer::from_exec_path(&lib_path)
.await
.map_err(|e| anyhow::anyhow!("Failed to load DWARF for lib: {}", e))?;
let addrs = analyzer.lookup_function_addresses("lib_tick");
anyhow::ensure!(
!addrs.is_empty(),
"No addresses for lib_tick in libgvars.so"
);
let pc = addrs[0].address;
let script = format!("trace libgvars.so:0x{pc:x} {{\n print \"LIB_MQUAL_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}, script={script}",
);
assert!(
stdout.lines().any(|l| l.contains("LIB_MQUAL_OK")),
"Expected LIB_MQUAL_OK in output. STDOUT: {stdout}, script: {script}"
);
Ok(())
}
#[tokio::test]
async fn test_address_of_with_hint_regression() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print &LIB_STATE;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 1, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(
stdout.contains("0x"),
"Expected hex address. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_unary_minus_nested() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
let d = -(-1);
print d;
print "X:{}", d;
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 1, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
assert!(stdout.contains("d = 1"), "Expected d = 1. STDOUT: {stdout}");
assert!(
stdout.contains("X:1"),
"Expected formatted 1. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_string_comparison_globals_char_ptr_and_array() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
if (gm == "Hello, Global!") { print "GM_OK"; }
if (lm == "LIB_MESSAGE") { print "LM_OK"; }
if (s.name == "RUNNING") { print "GNAME_RUN"; }
}
"#;
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("GM_OK"),
"Expected GM_OK for g_message. STDOUT: {stdout}"
);
assert!(
stdout.contains("LM_OK"),
"Expected LM_OK for lib_message. STDOUT: {stdout}"
);
assert!(
stdout.contains("GNAME_RUN"),
"Expected GNAME_RUN for G_STATE.name. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_print_format_current_global_member_leaf() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print "GY:{}", G_STATE.inner.y;
}
"#;
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}");
let re = Regex::new(r"GY:([0-9]+(?:\.[0-9]+)?)").unwrap();
let mut vals: Vec<f64> = Vec::new();
for line in stdout.lines() {
if let Some(c) = re.captures(line) {
assert!(
!line.contains("Inner {"),
"Expected scalar for G_STATE.inner.y, got struct: {line}"
);
vals.push(c[1].parse().unwrap_or(0.0));
}
}
assert!(vals.len() >= 2, "Insufficient GY events. STDOUT: {stdout}");
let d = ((vals[1] - vals[0]) * 100.0).round() as i64;
assert_eq!(
d, 50,
"G_STATE.inner.y should +0.5 per tick. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_print_format_global_autoderef_pointer_member() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print "X: {}", G_STATE.lib.inner.x;
}
"#;
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 num_re = Regex::new(r"X:\s*(-?\d+)").unwrap();
let err_re = Regex::new(r"X:\s*<error: null pointer dereference> \(int\*\)").unwrap();
let mut has_num = false;
let mut has_err = false;
for line in stdout.lines() {
if num_re.is_match(line) {
has_num = true;
}
if err_re.is_match(line) {
has_err = true;
}
}
assert!(has_num, "Expected numeric X line. STDOUT: {stdout}");
assert!(has_err, "Expected NullDeref X line. STDOUT: {stdout}");
Ok(())
}
#[tokio::test]
async fn test_cross_type_comparisons_globals() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
let th = 6;
print "SI_GT5:{} PIN0:{} SI_GT_TH:{}",
s_internal > 5,
p_lib_internal == 0,
s_internal > th;
}
"#;
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 re = Regex::new(r"SI_GT5:(true|false) PIN0:(true|false) SI_GT_TH:(true|false)").unwrap();
let mut saw_line = false;
let mut saw_pin0_flag = false;
for line in stdout.lines() {
if let Some(c) = re.captures(line) {
saw_line = true;
if &c[2] == "true" || &c[2] == "false" {
saw_pin0_flag = true;
}
}
}
assert!(
saw_line,
"Expected at least one comparison line. STDOUT: {stdout}"
);
assert!(saw_pin0_flag, "Expected PIN0 present. STDOUT: {stdout}");
Ok(())
}
#[tokio::test]
async fn test_if_else_if_and_bare_expr_globals() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
// bare expression print
print s_internal > 5;
if s_internal > 5 {
print "wtf";
} else if p_lib_internal == 0 {
// else-if prints an expression result when lib ptr is null
print p_lib_internal == 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_expr_line = stdout
.lines()
.any(|l| l.contains("(s_internal>5) = true") || l.contains("(s_internal>5) = false"));
assert!(
has_expr_line,
"Expected bare expression output for s_internal>5. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_if_else_if_logical_ops_globals() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
// Stable conditions to exercise both operators; first branch always true
if 1 == 1 && s_bss_counter >= 0 { print "AND"; }
else if 1 == 0 || p_lib_internal == 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}");
let has_and = stdout.lines().any(|l| l.contains("AND"));
assert!(has_and, "Expected AND branch output. STDOUT: {stdout}");
Ok(())
}
#[tokio::test]
async fn test_address_of_and_comparisons_globals() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print &G_STATE; // pointer to global struct
print (&G_STATE != 0); // expression with address-of
if &G_STATE != 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}");
assert!(
stdout.contains("0x"),
"Expected hex pointer for &G_STATE. STDOUT: {stdout}"
);
let has_expr = stdout
.lines()
.any(|l| l.contains("(&G_STATE!=0) = true") || l.contains("(&G_STATE!=0) = false"));
assert!(
has_expr,
"Expected (&G_STATE!=0) bare expr. STDOUT: {stdout}"
);
assert!(
stdout.contains("ADDR"),
"Expected then-branch ADDR line. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_string_equality_globals() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print "GM_EQ:{}", g_message == "Hello, Global!";
}
"#;
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("GM_EQ:true") || stdout.contains("GM_EQ:false"));
Ok(())
}
#[tokio::test]
async fn test_chain_tail_array_constant_index_increments() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print "A0:{}", G_STATE.lib.array[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}");
let re_num = Regex::new(r"^\s*A0:(-?\d+)").unwrap();
let re_err = Regex::new(r"^\s*A0:<error: null pointer dereference>").unwrap();
let mut vals: Vec<i64> = Vec::new();
let mut a0_lines = 0usize;
for line in stdout.lines() {
if line.trim_start().starts_with("A0:") {
a0_lines += 1;
}
if let Some(c) = re_num.captures(line) {
vals.push(c[1].parse::<i64>().unwrap_or(0));
} else if re_err.is_match(line) {
}
}
assert!(a0_lines >= 2, "Insufficient A0 events. STDOUT: {stdout}");
assert!(
!vals.is_empty(),
"Expected at least one numeric A0 sample. STDOUT: {stdout}"
);
if vals.len() >= 2 {
assert!(
vals[1] >= vals[0],
"A0 should not decrease. STDOUT: {stdout}"
);
}
Ok(())
}
#[tokio::test]
async fn test_builtins_strncmp_starts_with_globals() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print "SN1:{}", strncmp(gm, "Hello", 5);
print "SW1:{}", starts_with(gm, "Hello");
print "SN2:{}", strncmp(lm, "LIB_", 4);
}
"#;
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.lines().any(|l| l.contains("SN1:true")),
"Expected SN1:true. STDOUT: {stdout}"
);
assert!(
stdout.lines().any(|l| l.contains("SW1:true")),
"Expected SW1:true. STDOUT: {stdout}"
);
assert!(
stdout.lines().any(|l| l.contains("SN2:true")),
"Expected SN2:true. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_builtin_strncmp_generic_ptr_and_null() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print "SL:{}", strncmp(s.lib, "LIB", 3);
}
"#;
let (exit_code, stdout, stderr) = run_ghostscope_with_script_for_pid(script, 5, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
let saw_true = stdout.lines().any(|l| l.contains("SL:true"));
let saw_false = stdout.lines().any(|l| l.contains("SL:false"));
assert!(saw_true, "Expected SL:true at least once. STDOUT: {stdout}");
assert!(
saw_false,
"Expected SL:false at least once. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_rodata_char_element() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
// variable-print (name = value)
print g_message[0];
print lib_message[0];
// format-print (pure value in placeholder)
print "G0:{}", g_message[0];
print "L0:{}", lib_message[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}");
let _re_char_only = r"\s*='(?:.|\x[0-9a-fA-F]{2})'";
let _re_num_and_char = r"\s*=\s*\d+\s*\('(?:.|\x[0-9a-fA-F]{2})'\)";
let _re_num_only = r"\s*=\s*\d+";
let re_g1 = Regex::new(r"^\s*g_message\[0\]\s*='[^']'").unwrap();
let re_g2 = Regex::new(r"^\s*g_message\[0\]\s*=\s*\d+\s*\('[^']'\)").unwrap();
let re_g3 = Regex::new(r"^\s*g_message\[0\]\s*=\s*\d+").unwrap();
let re_l1 = Regex::new(r"^\s*lib_message\[0\]\s*='[^']'").unwrap();
let re_l2 = Regex::new(r"^\s*lib_message\[0\]\s*=\s*\d+\s*\('[^']'\)").unwrap();
let re_l3 = Regex::new(r"^\s*lib_message\[0\]\s*=\s*\d+").unwrap();
let has_g = stdout
.lines()
.any(|l| re_g1.is_match(l) || re_g2.is_match(l) || re_g3.is_match(l));
let has_l = stdout
.lines()
.any(|l| re_l1.is_match(l) || re_l2.is_match(l) || re_l3.is_match(l));
let re_fmt_val = r"(?:'[^']'|\d+)";
let re_fmt_g = Regex::new(&format!(r"^\s*G0:{re_fmt_val}")).unwrap();
let re_fmt_l = Regex::new(&format!(r"^\s*L0:{re_fmt_val}")).unwrap();
let has_fmt_g = stdout.lines().any(|l| re_fmt_g.is_match(l));
let has_fmt_l = stdout.lines().any(|l| re_fmt_l.is_match(l));
assert!(
has_g && has_l && has_fmt_g && has_fmt_l,
"Expected variable and formatted char outputs. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_format_specifiers_memory_and_pointer() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
// Hex dump first 4 bytes of g_message
print "HX={:x.4}", g_message;
// ASCII dump first 5 bytes of s.name
print "AS={:s.5}", s.name;
// Dynamic star length 4 on lm (lib_message)
print "DS={:s.*}", 4, lm;
// Pointer formatting for &G_STATE
print "P={:p}", &G_STATE;
}
"#;
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_hex4 = Regex::new(r"HX=([0-9a-fA-F]{2}(\s+[0-9a-fA-F]{2}){3})").unwrap();
let has_as = stdout.lines().any(|l| {
l.contains("AS=INIT")
|| l.contains("AS=RUNNI")
|| l.contains("AS=LIB")
|| l.contains("AS=HELLO")
});
let has_ds = stdout
.lines()
.any(|l| l.contains("DS=LIB_") || l.contains("DS=Hell"));
let has_ptr = stdout.lines().any(|l| l.contains("P=0x"));
let has_hex = stdout.lines().any(|l| re_hex4.is_match(l));
assert!(has_hex, "Expected hex dump HX=. STDOUT: {stdout}");
assert!(has_as, "Expected ASCII dump AS=. STDOUT: {stdout}");
assert!(has_ds, "Expected dynamic star ASCII DS=. STDOUT: {stdout}");
assert!(has_ptr, "Expected pointer P=0x.... STDOUT: {stdout}");
Ok(())
}
#[tokio::test]
async fn test_large_pattern_dump_and_checks() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print "LPX16={:x.16}", lib_pattern;
print "LPD10={:x.*}", 10, lib_pattern;
print "B100={} B255={}", lib_pattern[100], lib_pattern[255];
}
"#;
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_first16 = Regex::new(r"LPX16=00(\s+01)(\s+02)(\s+03)(\s+04)(\s+05)(\s+06)(\s+07)(\s+08)(\s+09)(\s+0a)(\s+0b)(\s+0c)(\s+0d)(\s+0e)(\s+0f)").unwrap();
let has_first16 = stdout.lines().any(|l| re_first16.is_match(l));
let has_dyn10 = stdout
.lines()
.any(|l| l.contains("LPD10=00 01 02 03 04 05 06 07 08 09"));
let has_b100 = stdout.lines().any(|l| l.contains("B100=100"));
let has_b255 = stdout.lines().any(|l| l.contains("B255=255"));
assert!(
has_first16,
"Expected first 16 bytes 00..0f. STDOUT: {stdout}"
);
assert!(
has_dyn10,
"Expected dynamic 10 bytes 00..09. STDOUT: {stdout}"
);
assert!(has_b100, "Expected B100=100. STDOUT: {stdout}");
assert!(has_b255, "Expected B255=255. STDOUT: {stdout}");
Ok(())
}
#[tokio::test]
async fn test_format_capture_len_zero_and_exceed_cap() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
let z = 0;
let big = 128;
print "Z0={:x.z$}", lib_pattern; // expect empty
print "XC={:x.big$}", lib_pattern; // expect 64 bytes due to cap
}
"#;
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_z0_empty = Regex::new(r"^\s*Z0=\s*$").unwrap();
let has_z0_empty = stdout.lines().any(|l| re_z0_empty.is_match(l));
let re_64_hex = Regex::new(r"XC=([0-9a-fA-F]{2}(\s+[0-9a-fA-F]{2}){63})").unwrap();
let has_trunc_64 = stdout.lines().any(|l| re_64_hex.is_match(l));
assert!(has_z0_empty, "Expected Z0= (empty). STDOUT: {stdout}");
assert!(
has_trunc_64,
"Expected XC= to contain exactly 64 bytes due to cap. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_format_negative_len_clamped_to_zero() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
let neg = -5;
print "ZN1={:x.neg$}", lib_pattern; // capture negative -> empty
print "ZN2={:x.*}", -10, lib_pattern; // star negative -> empty
}
"#;
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_empty1 = Regex::new(r"^\s*ZN1=\s*$").unwrap();
let re_empty2 = Regex::new(r"^\s*ZN2=\s*$").unwrap();
let has_zn1 = stdout.lines().any(|l| re_empty1.is_match(l));
let has_zn2 = stdout.lines().any(|l| re_empty2.is_match(l));
assert!(has_zn1, "Expected ZN1= (empty). STDOUT: {stdout}");
assert!(has_zn2, "Expected ZN2= (empty). STDOUT: {stdout}");
Ok(())
}
#[tokio::test]
async fn test_format_specifiers_capture_len() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
let n = 4;
// Capture length from script variable for ASCII and HEX
print "CL={:s.n$}", lm;
print "CH={:x.n$}", g_message;
}
"#;
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_hex4 = Regex::new(r"CH=([0-9a-fA-F]{2}(\s+[0-9a-fA-F]{2}){3})").unwrap();
let has_hex = stdout.lines().any(|l| re_hex4.is_match(l));
let has_cl = stdout
.lines()
.any(|l| l.contains("CL=LIB_") || l.contains("CL=Hell"));
assert!(has_hex, "Expected hex dump CH=. STDOUT: {stdout}");
assert!(has_cl, "Expected capture-len ASCII CL=. STDOUT: {stdout}");
Ok(())
}
#[tokio::test]
async fn test_alias_to_complex_dwarf_expr_index_and_address_of() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
let a = G_STATE.array; // alias to DWARF array member
print "APTR={:p}", &a; // address-of alias
print "A0={}", a[0]; // index on alias
print "A1={}", a[1];
}
"#;
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("APTR=0x"),
"Expected APTR pointer line. STDOUT: {stdout}"
);
let has_a0 = stdout
.lines()
.any(|l| l.trim_start().starts_with("A0=") || l.trim_start().starts_with("A0:"));
let has_a1 = stdout
.lines()
.any(|l| l.trim_start().starts_with("A1=") || l.trim_start().starts_with("A1:"));
assert!(
has_a0,
"Expected A0 line from alias index. STDOUT: {stdout}"
);
assert!(
has_a1,
"Expected A1 line from alias index. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_alias_to_aggregate_and_chain_and_string_prefix() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
let a = s; // alias to pointer-to-aggregate (GlobalState*)
print "AX:{}", a.inner.x; // member chain from alias
// probe string prefix on nested char[N]
print "RUN?{}", starts_with(a.name, "RUN");
print "INI?{}", starts_with(a.name, "INI");
// also alias a nested aggregate and read numeric
let b = a.inner;
print "BX:{}", b.x;
}
"#;
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 re_ax = Regex::new(r"^\s*AX:(-?\d+)").unwrap();
let re_bx = Regex::new(r"^\s*BX:(-?\d+)").unwrap();
let has_ax = stdout.lines().any(|l| re_ax.is_match(l));
let has_bx = stdout.lines().any(|l| re_bx.is_match(l));
assert!(
has_ax,
"Expected AX numeric via alias chain. STDOUT: {stdout}"
);
assert!(
has_bx,
"Expected BX numeric via nested alias. STDOUT: {stdout}"
);
let saw_run_true = stdout.lines().any(|l| l.contains("RUN?true"));
let saw_ini_true = stdout.lines().any(|l| l.contains("INI?true"));
assert!(
saw_run_true || saw_ini_true,
"Expected one of RUN?true/INI?true. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_alias_rodata_string_builtins() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
let sg = gm; // alias to g_message (const char*)
let sl = lm; // alias to lib_message (const char*)
print "GST:{}", starts_with(sg, "Hell");
print "GSN:{}", strncmp(sg, "Hello", 5);
print "LST:{}", starts_with(sl, "LIB_");
print "LSN:{}", strncmp(sl, "LIB_", 4);
}
"#;
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}");
for tag in ["GST:true", "GSN:true", "LST:true", "LSN:true"] {
assert!(
stdout.contains(tag),
"Expected {tag} at least once. STDOUT: {stdout}"
);
}
Ok(())
}
#[tokio::test]
async fn test_alias_rodata_string_builtins_perf() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
let sg = gm; // alias to g_message (const char*)
let sl = lm; // alias to lib_message (const char*)
print "GST:{}", starts_with(sg, "Hell");
print "GSN:{}", strncmp(sg, "Hello", 5);
print "LST:{}", starts_with(sl, "LIB_");
print "LSN:{}", strncmp(sl, "LIB_", 4);
}
"#;
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}");
for tag in ["GST:true", "GSN:true", "LST:true", "LSN:true"] {
assert!(
stdout.contains(tag),
"Expected {tag} at least once. STDOUT: {stdout}"
);
}
Ok(())
}
#[tokio::test]
async fn test_alias_address_of_cross_module_uses_correct_hint() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print "MX:{}", G_STATE.counter; // touch main exe symbol first
let a = LIB_STATE; // alias to library global (cross-module)
print "PA={:p}", &a; // &alias must equal &LIB_STATE
print "PL={:p}", &LIB_STATE;
}
"#;
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 re_pa = Regex::new(r"PA=0x([0-9a-fA-F]+)").unwrap();
let re_pl = Regex::new(r"PL=0x([0-9a-fA-F]+)").unwrap();
let mut last_pa: Option<u64> = None;
let mut last_pl: Option<u64> = None;
for line in stdout.lines() {
if let Some(c) = re_pa.captures(line) {
if let Ok(v) = u64::from_str_radix(&c[1], 16) {
last_pa = Some(v);
}
}
if let Some(c) = re_pl.captures(line) {
if let Ok(v) = u64::from_str_radix(&c[1], 16) {
last_pl = Some(v);
}
}
}
let pa = last_pa.ok_or_else(|| anyhow::anyhow!("missing PA"))?;
let pl = last_pl.ok_or_else(|| anyhow::anyhow!("missing PL"))?;
assert_eq!(pa, pl, "&alias should equal &LIB_STATE (cross-module hint)");
Ok(())
}
#[tokio::test]
async fn test_top_level_array_member_struct_field() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print "SX:{}", g_slots[1].x;
}
"#;
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 re = Regex::new(r"SX:(-?\d+)").unwrap();
let has = stdout.lines().any(|l| re.is_match(l));
assert!(
has,
"Expected struct field numeric via g_slots[1].x. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_tick_once_entry_strings_and_structs() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:26 {
print s.name; // char[32] -> string
print ls.name; // from shared library
print s; // struct GlobalState pretty print
print *ls; // deref pointer to struct
}
"#;
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}");
let has_s_name = stdout.contains("\"INIT\"") || stdout.contains("\"RUNNING\"");
assert!(has_s_name, "Expected s.name string. STDOUT: {stdout}");
assert!(
stdout.contains("\"LIB\""),
"Expected ls.name == \"LIB\". STDOUT: {stdout}"
);
let has_struct = stdout.contains("GlobalState {") || stdout.contains("*ls = GlobalState {");
assert!(
has_struct,
"Expected pretty struct output. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_tick_once_formatted_counters() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 tick_once {
print "G:{} L:{}", s.counter, ls.counter;
}
"#;
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("G:") && stdout.contains("L:"),
"Expected formatted counters. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_tick_once_pointer_values() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:26 {
print gm; // const char* to executable rodata
print lm; // const char* to library rodata
}
"#;
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("0x"),
"Expected hexadecimal pointer output. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_two_events_evolution_and_statics() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:26 {
print s.counter;
print ls.counter;
print *p_s_internal;
print *p_s_bss;
print *p_lib_internal;
}
"#;
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 re_s = Regex::new(r"s\.counter\s*=\s*(\d+)").unwrap();
let re_ls = Regex::new(r"ls\.counter\s*=\s*(\d+)").unwrap();
let re_si = Regex::new(r"\*p_s_internal\s*=\s*(\d+)").unwrap();
let re_sb = Regex::new(r"\*p_s_bss\s*=\s*(\d+)").unwrap();
let re_li = Regex::new(r"\*p_lib_internal\s*=\s*(\d+)").unwrap();
let re_li_err =
Regex::new(r"\*p_lib_internal\s*=\s*<error: null pointer dereference>").unwrap();
let mut s_vals = Vec::new();
let mut ls_vals = Vec::new();
let mut si_vals = Vec::new();
let mut sb_vals = Vec::new();
let mut li_vals = Vec::new();
let mut li_errs = 0usize;
for line in stdout.lines() {
if let Some(c) = re_s.captures(line) {
s_vals.push(c[1].parse::<i64>().unwrap_or(0));
}
if let Some(c) = re_ls.captures(line) {
ls_vals.push(c[1].parse::<i64>().unwrap_or(0));
}
if let Some(c) = re_si.captures(line) {
si_vals.push(c[1].parse::<i64>().unwrap_or(0));
}
if let Some(c) = re_sb.captures(line) {
sb_vals.push(c[1].parse::<i64>().unwrap_or(0));
}
if let Some(c) = re_li.captures(line) {
li_vals.push(c[1].parse::<i64>().unwrap_or(0));
} else if re_li_err.is_match(line) {
li_errs += 1;
}
}
assert!(
s_vals.len() >= 2 && ls_vals.len() >= 2 && si_vals.len() >= 2 && sb_vals.len() >= 2,
"Insufficient events for delta checks (exe-side). STDOUT: {stdout}"
);
assert!(s_vals[1] >= s_vals[0], "s.counter should be non-decreasing");
assert_eq!(ls_vals[1] - ls_vals[0], 2, "ls.counter should +2 per tick");
assert_eq!(si_vals[1] - si_vals[0], 2, "s_internal should +2 per tick");
assert_eq!(
sb_vals[1] - sb_vals[0],
3,
"s_bss_counter should +3 per tick"
);
if li_vals.len() >= 2 {
assert_eq!(
li_vals[1] - li_vals[0],
5,
"lib_internal_counter should +5 per tick"
);
} else {
assert!(
(li_vals.len() == 1 && li_errs >= 1) || (li_vals.is_empty() && li_errs >= 2),
"Expected lib_internal to be numeric twice, or NULL twice, or mixed once over two events. STDOUT: {stdout}"
);
}
Ok(())
}
#[tokio::test]
async fn test_direct_globals_current_module() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print G_STATE;
print s_internal;
print s_bss_counter;
// Also verify print format with direct globals
print "FMT:{}|{}", s_internal, s_bss_counter;
}
"#;
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("G_STATE") && stdout.contains("GlobalState"),
"Expected G_STATE struct print. STDOUT: {stdout}"
);
let re_si = Regex::new(r"s_internal\s*=\s*(-?\d+)").unwrap();
let re_sb = Regex::new(r"s_bss_counter\s*=\s*(-?\d+)").unwrap();
let mut si_vals = Vec::new();
let mut sb_vals = Vec::new();
for line in stdout.lines() {
if let Some(c) = re_si.captures(line) {
si_vals.push(c[1].parse::<i64>().unwrap_or(0));
}
if let Some(c) = re_sb.captures(line) {
sb_vals.push(c[1].parse::<i64>().unwrap_or(0));
}
}
assert!(
si_vals.len() >= 2 && sb_vals.len() >= 2,
"Insufficient events. STDOUT: {stdout}"
);
assert_eq!(si_vals[1] - si_vals[0], 2, "s_internal should +2 per tick");
assert_eq!(
sb_vals[1] - sb_vals[0],
3,
"s_bss_counter should +3 per tick"
);
let re_fmt = Regex::new(r"FMT:(-?\d+)\|(-?\d+)").unwrap();
let mut f_a = Vec::new();
let mut f_b = Vec::new();
for line in stdout.lines() {
if let Some(c) = re_fmt.captures(line) {
f_a.push(c[1].parse::<i64>().unwrap_or(0));
f_b.push(c[2].parse::<i64>().unwrap_or(0));
}
}
assert!(
f_a.len() >= 2 && f_b.len() >= 2,
"Insufficient FMT events. STDOUT: {stdout}"
);
assert_eq!(f_a[1] - f_a[0], 2, "FMT s_internal delta +2");
assert_eq!(f_b[1] - f_b[0], 3, "FMT s_bss_counter delta +3");
Ok(())
}
#[tokio::test]
async fn test_direct_global_cross_module() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print LIB_STATE;
// Also emit formatted cross-module counter to ensure format-path works for globals
print "LIBCNT:{}", LIB_STATE.counter;
}
"#;
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("LIB_STATE"),
"Expected LIB_STATE in output. STDOUT: {stdout}"
);
assert!(
stdout.contains("GlobalState {") || (stdout.contains("{") && stdout.contains("name:")),
"Expected pretty struct print for LIB_STATE. STDOUT: {stdout}"
);
let re = Regex::new(r"LIBCNT:(-?\d+)").unwrap();
let mut vals = Vec::new();
for line in stdout.lines() {
if let Some(c) = re.captures(line) {
vals.push(c[1].parse::<i64>().unwrap_or(0));
}
}
assert!(
vals.len() >= 2,
"Insufficient LIBCNT events. STDOUT: {stdout}"
);
assert_eq!(vals[1] - vals[0], 2, "LIB_STATE.counter should +2 per tick");
Ok(())
}
#[tokio::test]
async fn test_rodata_direct_strings() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print g_message; // executable .rodata (char[...])
print lib_message; // library .rodata (char[...])
// Also check formatted path for strings
print "FMT:{}|{}", g_message, lib_message;
}
"#;
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}");
let got_g_message = stdout
.lines()
.any(|l| l.contains("g_message = \"") && l.contains("\""));
let got_lib_message = stdout
.lines()
.any(|l| l.contains("lib_message = \"") && l.contains("\""));
assert!(
got_g_message && got_lib_message,
"Expected direct string prints for rodata. STDOUT: {stdout}"
);
let fmt_has_strings = stdout
.lines()
.any(|l| l.contains("FMT:") && l.matches('"').count() >= 2);
assert!(
fmt_has_strings,
"Expected formatted strings line. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_bss_first_byte_evolves() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap().to_path_buf();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print *gb; // g_bss_buffer[0]
print *lb; // lib_bss[0]
// Also formatted first bytes from both buffers
print "BF:{}|{}", *gb, *lb;
}
"#;
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 re_gb = Regex::new(r"\*gb\s*=\s*(-?\d+)").unwrap();
let re_lb = Regex::new(r"\*lb\s*=\s*(-?\d+)").unwrap();
let mut gb_vals = Vec::new();
let mut lb_vals = Vec::new();
for line in stdout.lines() {
if let Some(c) = re_gb.captures(line) {
gb_vals.push(c[1].parse::<i64>().unwrap_or(0));
}
if let Some(c) = re_lb.captures(line) {
lb_vals.push(c[1].parse::<i64>().unwrap_or(0));
}
}
assert!(
gb_vals.len() >= 2 && lb_vals.len() >= 2,
"Insufficient events. STDOUT: {stdout}"
);
assert!(
gb_vals[1] >= gb_vals[0],
"gb[0] should not decrease. STDOUT: {stdout}"
);
assert!(
lb_vals[1] >= lb_vals[0],
"lb[0] should not decrease. STDOUT: {stdout}"
);
let re = Regex::new(r"BF:(-?\d+)\|(-?\d+)").unwrap();
let mut fa = Vec::new();
let mut fb = Vec::new();
for line in stdout.lines() {
if let Some(c) = re.captures(line) {
fa.push(c[1].parse::<i64>().unwrap_or(0));
fb.push(c[2].parse::<i64>().unwrap_or(0));
}
}
assert!(
fa.len() >= 2 && fb.len() >= 2,
"Insufficient BF events. STDOUT: {stdout}"
);
assert!(
fa[1] >= fa[0] && fb[1] >= fb[0],
"BF values should not decrease. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_print_variable_global_member_direct() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap().to_path_buf();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print G_STATE.counter;
print LIB_STATE.counter;
}
"#;
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 re_g = Regex::new(r"G_STATE\.counter\s*=\s*(-?\d+)").unwrap();
let re_l = Regex::new(r"LIB_STATE\.counter\s*=\s*(-?\d+)").unwrap();
let mut gv = Vec::new();
let mut lv = Vec::new();
for line in stdout.lines() {
if let Some(c) = re_g.captures(line) {
gv.push(c[1].parse::<i64>().unwrap_or(0));
}
if let Some(c) = re_l.captures(line) {
lv.push(c[1].parse::<i64>().unwrap_or(0));
}
}
assert!(
gv.len() >= 2 && lv.len() >= 2,
"Insufficient events. STDOUT: {stdout}"
);
assert!(gv[1] >= gv[0], "G_STATE.counter should be non-decreasing");
assert_eq!(lv[1] - lv[0], 2, "LIB_STATE.counter should +2 per tick");
Ok(())
}
#[tokio::test]
async fn test_print_format_global_member_direct() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap().to_path_buf();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print "LIBCNT:{}", LIB_STATE.counter;
}
"#;
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 re = Regex::new(r"LIBCNT:(-?\d+)").unwrap();
let mut vals = Vec::new();
for line in stdout.lines() {
if let Some(c) = re.captures(line) {
vals.push(c[1].parse::<i64>().unwrap_or(0));
}
}
assert!(
vals.len() >= 2,
"Insufficient LIBCNT events. STDOUT: {stdout}"
);
assert_eq!(vals[1] - vals[0], 2, "LIB_STATE.counter should +2 per tick");
Ok(())
}
#[tokio::test]
async fn test_print_format_global_member_leaf() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap().to_path_buf();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print "LIBY:{}", LIB_STATE.inner.y;
}
"#;
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 re = Regex::new(r"LIBY:([0-9]+(?:\.[0-9]+)?)").unwrap();
let mut vals: Vec<f64> = Vec::new();
for line in stdout.lines() {
if let Some(c) = re.captures(line) {
let v: f64 = c[1].parse().unwrap_or(0.0);
assert!(
!line.contains("Inner {"),
"Leaf member should print scalar, got struct: {line}"
);
vals.push(v);
}
}
assert!(
vals.len() >= 2,
"Insufficient LIBY events. STDOUT: {stdout}"
);
let d = ((vals[1] - vals[0]) * 100.0).round() as i64;
assert_eq!(
d, 125,
"LIB_STATE.inner.y should +1.25 per tick. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_print_variable_global_member_leaf() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap().to_path_buf();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print LIB_STATE.inner.y;
}
"#;
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 re = Regex::new(r"LIB_STATE\.inner\.y\s*=\s*(-?[0-9]+(?:\.[0-9]+)?)").unwrap();
let mut vals: Vec<f64> = Vec::new();
for line in stdout.lines() {
if let Some(c) = re.captures(line) {
let v: f64 = c[1].parse().unwrap_or(0.0);
vals.push(v);
}
}
assert!(
vals.len() >= 2,
"Insufficient LIB_STATE.inner.y events. STDOUT: {stdout}"
);
let d = ((vals[1] - vals[0]) * 100.0).round() as i64;
assert_eq!(d, 125, "inner.y should +1.25 per tick. STDOUT: {stdout}");
Ok(())
}
#[tokio::test]
async fn test_print_format_current_global_member_leaf_perf() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap().to_path_buf();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
print "GY:{}", G_STATE.inner.y;
}
"#;
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_for_pid_perf(script, 2, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
let re = Regex::new(r"GY:([0-9]+(?:\.[0-9]+)?)").unwrap();
let mut vals: Vec<f64> = Vec::new();
for line in stdout.lines() {
if let Some(c) = re.captures(line) {
assert!(
!line.contains("Inner {"),
"Expected scalar for G_STATE.inner.y, got struct: {line}"
);
vals.push(c[1].parse().unwrap_or(0.0));
}
}
assert!(vals.len() >= 2, "Insufficient GY events. STDOUT: {stdout}");
let d = ((vals[1] - vals[0]) * 100.0).round() as i64;
assert_eq!(
d, 50,
"G_STATE.inner.y should +0.5 per tick. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_tick_once_entry_strings_and_structs_perf() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap().to_path_buf();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:26 {
print s.name; // char[32] -> string
print ls.name; // from shared library
print s; // struct GlobalState pretty print
print *ls; // deref pointer to struct
}
"#;
let (exit_code, stdout, stderr) =
run_ghostscope_with_script_for_pid_perf(script, 2, pid).await?;
let _ = prog.kill().await.is_ok();
assert_eq!(exit_code, 0, "stderr={stderr} stdout={stdout}");
let has_s_name = stdout.contains("\"INIT\"") || stdout.contains("\"RUNNING\"");
assert!(has_s_name, "Expected s.name string. STDOUT: {stdout}");
assert!(
stdout.contains("\"LIB\""),
"Expected ls.name == \"LIB\". STDOUT: {stdout}"
);
let has_struct = stdout.contains("GlobalState {") || stdout.contains("*ls = GlobalState {");
assert!(
has_struct,
"Expected pretty struct output. STDOUT: {stdout}"
);
Ok(())
}
#[tokio::test]
async fn test_memcmp_numeric_pointer_literal_and_hex_len() -> anyhow::Result<()> {
init();
let binary_path = FIXTURES.get_test_binary("globals_program")?;
let bin_dir = binary_path.parent().unwrap().to_path_buf();
let mut prog = Command::new(&binary_path)
.current_dir(bin_dir)
.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 globals_program.c:32 {
// hex static length should work when comparing equal pointers
if memcmp(&lib_pattern[0], &lib_pattern[0], 0x10) { print "LENHEX"; }
}
"#;
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("LENHEX"),
"Expected LENHEX. STDOUT: {stdout}"
);
Ok(())
}