ghidra-cli 0.1.10

Rust CLI to run Ghidra headless for reverse engineering with Claude Code and other agents
Documentation
//! Tests for bridge reliability - bridge death detection and port file cleanup.

use serial_test::serial;
use std::time::Duration;

#[macro_use]
mod common;
use common::{ensure_test_project, DaemonTestHarness};

const TEST_PROJECT: &str = "ci-test";
const TEST_PROGRAM: &str = "sample_binary";

/// Try to create a DaemonTestHarness. Returns None (and prints skip message) if
/// the bridge fails to start due to "program file(s) not found" - a known
/// macOS issue where Ghidra can't find the imported program.
fn try_start_harness(context: &str) -> Option<DaemonTestHarness> {
    match DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM) {
        Ok(h) => Some(h),
        Err(e) => {
            let msg = format!("{}", e);
            if msg.contains("program file(s) not found") {
                eprintln!(
                    "Skipping ({}): bridge can't find program (known macOS issue)",
                    context
                );
                None
            } else {
                panic!("Failed to start bridge ({}): {}", context, e);
            }
        }
    }
}

/// Test that stale port files are cleaned up on bridge restart.
///
/// Simulates a crash scenario where port file remains but bridge is dead.
#[test]
#[serial]
fn test_stale_files_cleaned_on_restart() {
    require_ghidra!();

    ensure_test_project(TEST_PROJECT, TEST_PROGRAM);

    // First bridge - start and stop cleanly
    {
        let Some(_harness) = try_start_harness("first bridge") else {
            return;
        };

        // Verify bridge is working
        assert_cmd::cargo::cargo_bin_cmd!("ghidra")
            .arg("ping")
            .arg("--project")
            .arg(TEST_PROJECT)
            .timeout(Duration::from_secs(30))
            .assert()
            .success();

        // Drop will clean up
    }

    // Brief pause to ensure cleanup completes
    std::thread::sleep(Duration::from_millis(500));

    // Second bridge - should start without issues (no stale port file conflict)
    {
        let Some(_harness) = try_start_harness("second bridge after restart") else {
            return;
        };

        // Verify bridge is working
        assert_cmd::cargo::cargo_bin_cmd!("ghidra")
            .arg("ping")
            .arg("--project")
            .arg(TEST_PROJECT)
            .timeout(Duration::from_secs(30))
            .assert()
            .success();
    }
}

/// Test recovery after bridge crash (simulated via process kill).
///
/// After killing bridge, a new bridge should be able to start successfully.
#[test]
#[serial]
fn test_recovery_after_crash() {
    require_ghidra!();

    ensure_test_project(TEST_PROJECT, TEST_PROGRAM);

    // Start bridge and verify it works
    {
        let Some(_harness) = try_start_harness("initial bridge") else {
            return;
        };

        // Verify it's working
        assert_cmd::cargo::cargo_bin_cmd!("ghidra")
            .arg("ping")
            .arg("--project")
            .arg(TEST_PROJECT)
            .timeout(Duration::from_secs(30))
            .assert()
            .success();

        // Harness drop will kill bridge (simulating crash)
    }

    // Brief pause
    std::thread::sleep(Duration::from_millis(1000));

    // New bridge should start successfully after crash cleanup
    {
        let Some(_harness) = try_start_harness("bridge after crash") else {
            return;
        };

        // Verify new bridge is working
        assert_cmd::cargo::cargo_bin_cmd!("ghidra")
            .arg("ping")
            .arg("--project")
            .arg(TEST_PROJECT)
            .timeout(Duration::from_secs(30))
            .assert()
            .success();
    }
}

/// Test that bridge commands return appropriate errors when bridge is not ready.
#[test]
#[serial]
fn test_bridge_not_ready_error() {
    require_ghidra!();

    ensure_test_project(TEST_PROJECT, TEST_PROGRAM);

    let Some(harness) = try_start_harness("bridge") else {
        return;
    };

    // Ping should work
    assert_cmd::cargo::cargo_bin_cmd!("ghidra")
        .arg("ping")
        .arg("--project")
        .arg(TEST_PROJECT)
        .timeout(Duration::from_secs(30))
        .assert()
        .success();

    drop(harness);
}