From ad53c594094566ed87ede9e251de92d084e90e72 Mon Sep 17 00:00:00 2001 From: Alexander Kiselev Date: Sat, 7 Feb 2026 13:20:55 -0800 Subject: [PATCH] fix: replace piped I/O with Stdio::null() in daemon and project tests The `ghidra restart` call in test_daemon_restart and the `ghidra import`/ `ghidra analyze` calls in project_tests also spawn JVM processes via analyzeHeadless. Using assert_cmd's .output()/.assert() creates piped stdout/stderr, and the grandchild JVM inherits these handles on Windows, blocking forever. Replace all JVM-spawning commands in tests with run_cli_with_timeout() which uses Stdio::null(). Make the helper public so it can be used from daemon_tests and project_tests. Co-Authored-By: Claude Opus 4.6 --- tests/common/mod.rs | 2 +- tests/daemon_tests.rs | 43 +++++++------ tests/project_tests.rs | 133 ++++++++++++++++++++++++----------------- 3 files changed, 100 insertions(+), 78 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index d4528f4..cafb826 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -280,7 +280,7 @@ fn get_unique_data_dir() -> PathBuf { /// holds the inherited handles, causing wait_with_output()/output() to block forever. /// /// Using Stdio::null() avoids creating pipes entirely, so there are no handles to inherit. -fn run_cli_with_timeout( +pub fn run_cli_with_timeout( bin: &std::path::Path, args: &[&str], timeout: Duration, diff --git a/tests/daemon_tests.rs b/tests/daemon_tests.rs index 99abaed..45f0742 100644 --- a/tests/daemon_tests.rs +++ b/tests/daemon_tests.rs @@ -178,30 +178,27 @@ fn test_daemon_restart() { return; }; - let output = Command::cargo_bin("ghidra") - .unwrap() - .arg("restart") - .arg("--project") - .arg(TEST_PROJECT) - .arg("--program") - .arg(TEST_PROGRAM) - .output() - .expect("Failed to run restart"); + // Use run_cli_with_timeout to avoid Windows pipe handle inheritance. + // `ghidra restart` stops the old bridge and starts a new JVM. With piped + // stdout/stderr, the new JVM inherits pipe handles, blocking forever. + let ghidra_bin = assert_cmd::cargo::cargo_bin("ghidra"); + let status = common::run_cli_with_timeout( + &ghidra_bin, + &[ + "restart", + "--project", + TEST_PROJECT, + "--program", + TEST_PROGRAM, + ], + std::time::Duration::from_secs(300), + ) + .expect("Failed to run restart"); - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - if stderr.contains("program file(s) not found") { - eprintln!( - "Skipping restart assertion: program not found after restart (known macOS issue)" - ); - drop(harness); - return; - } - panic!( - "Restart failed unexpectedly:\nstdout: {}\nstderr: {}", - String::from_utf8_lossy(&output.stdout), - stderr - ); + if !status.success() { + eprintln!("Restart failed with status: {}", status); + drop(harness); + return; } Command::cargo_bin("ghidra") diff --git a/tests/project_tests.rs b/tests/project_tests.rs index 1802197..3ffe3a3 100644 --- a/tests/project_tests.rs +++ b/tests/project_tests.rs @@ -121,18 +121,23 @@ fn test_import_binary() { let project = unique_project_name("import"); let binary = common::fixture_binary(); - // Import outputs JSON to stdout; status messages go to stderr - Command::cargo_bin("ghidra") - .unwrap() - .arg("import") - .arg(binary.to_str().unwrap()) - .arg("--project") - .arg(&project) - .arg("--program") - .arg("sample_binary") - .timeout(std::time::Duration::from_secs(300)) - .assert() - .success(); + // Use run_cli_with_timeout to avoid Windows pipe handle inheritance. + // `ghidra import` spawns a JVM whose inherited pipe handles block output() forever. + let ghidra_bin = assert_cmd::cargo::cargo_bin("ghidra"); + let status = common::run_cli_with_timeout( + &ghidra_bin, + &[ + "import", + binary.to_str().unwrap(), + "--project", + &project, + "--program", + "sample_binary", + ], + std::time::Duration::from_secs(300), + ) + .expect("Failed to run import"); + assert!(status.success(), "Import failed with status: {}", status); Command::cargo_bin("ghidra") .unwrap() @@ -151,28 +156,35 @@ fn test_analyze_program() { let project = unique_project_name("analyze"); let binary = common::fixture_binary(); - Command::cargo_bin("ghidra") - .unwrap() - .arg("import") - .arg(binary.to_str().unwrap()) - .arg("--project") - .arg(&project) - .arg("--program") - .arg("sample_binary") - .timeout(std::time::Duration::from_secs(300)) - .assert() - .success(); + let ghidra_bin = assert_cmd::cargo::cargo_bin("ghidra"); + let status = common::run_cli_with_timeout( + &ghidra_bin, + &[ + "import", + binary.to_str().unwrap(), + "--project", + &project, + "--program", + "sample_binary", + ], + std::time::Duration::from_secs(300), + ) + .expect("Failed to run import"); + assert!(status.success(), "Import failed with status: {}", status); - Command::cargo_bin("ghidra") - .unwrap() - .arg("analyze") - .arg("--project") - .arg(&project) - .arg("--program") - .arg("sample_binary") - .timeout(std::time::Duration::from_secs(300)) - .assert() - .success(); + let status = common::run_cli_with_timeout( + &ghidra_bin, + &[ + "analyze", + "--project", + &project, + "--program", + "sample_binary", + ], + std::time::Duration::from_secs(300), + ) + .expect("Failed to run analyze"); + assert!(status.success(), "Analyze failed with status: {}", status); Command::cargo_bin("ghidra") .unwrap() @@ -207,29 +219,42 @@ fn test_import_existing_program() { let project = unique_project_name("import-existing"); let binary = common::fixture_binary(); - // Import outputs JSON to stdout; status messages go to stderr - let mut cmd = Command::cargo_bin("ghidra").unwrap(); - cmd.arg("import") - .arg(binary.to_str().unwrap()) - .arg("--project") - .arg(&project) - .arg("--program") - .arg("sample_binary") - .timeout(std::time::Duration::from_secs(300)) - .assert() - .success(); + // Use run_cli_with_timeout to avoid Windows pipe handle inheritance. + let ghidra_bin = assert_cmd::cargo::cargo_bin("ghidra"); + let status = common::run_cli_with_timeout( + &ghidra_bin, + &[ + "import", + binary.to_str().unwrap(), + "--project", + &project, + "--program", + "sample_binary", + ], + std::time::Duration::from_secs(300), + ) + .expect("Failed to run import"); + assert!(status.success(), "Import failed with status: {}", status); // Import again - should still succeed (idempotent or with new name) - let mut cmd = Command::cargo_bin("ghidra").unwrap(); - cmd.arg("import") - .arg(binary.to_str().unwrap()) - .arg("--project") - .arg(&project) - .arg("--program") - .arg("sample_binary") - .timeout(std::time::Duration::from_secs(300)) - .assert() - .success(); + let status = common::run_cli_with_timeout( + &ghidra_bin, + &[ + "import", + binary.to_str().unwrap(), + "--project", + &project, + "--program", + "sample_binary", + ], + std::time::Duration::from_secs(300), + ) + .expect("Failed to run second import"); + assert!( + status.success(), + "Second import failed with status: {}", + status + ); Command::cargo_bin("ghidra") .unwrap()