Files
ghidra-cli/tests
Alexander Kiselev 42ff0dadc3 fix: use main instead of add_numbers in macOS-sensitive tests
On macOS, add_numbers may be inlined by the compiler and not appear
as a named function in Ghidra. Switch decompile, graph callers, and
diff tests to use main which always exists. Also use unique symbol
names in rename test to avoid cached project state collisions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 09:25:31 -08:00
..
2026-02-06 08:40:18 -08:00
2026-01-25 22:55:56 -08:00
2026-02-05 17:14:19 -08:00
2026-02-04 14:26:33 -08:00
2026-02-06 08:40:18 -08:00

E2E Test Suite

Comprehensive end-to-end test coverage for ghidra-cli commands.

Architecture

Tests are organized by functional area into separate files:

tests/
├── common/
│   ├── mod.rs           # DaemonTestHarness, fixtures, helpers
│   ├── helpers.rs       # GhidraCommand builder, GhidraResult assertions
│   └── schemas.rs       # Response validation schemas
├── daemon_tests.rs      # Bridge lifecycle: start/stop/restart/status/ping
├── project_tests.rs     # Project: create/list/delete/info
├── reliability_tests.rs # Bridge restart recovery, stale file cleanup
├── command_tests.rs     # Basic commands: version/doctor/config/init
├── batch_tests.rs       # Batch command execution
├── comment_tests.rs     # Comment operations
├── diff_tests.rs        # Program diff operations
├── find_tests.rs        # Search operations
├── graph_tests.rs       # Call graph operations
├── program_tests.rs     # Program info/import/export
├── script_tests.rs      # Script execution
├── stats_tests.rs       # Statistics
├── symbol_tests.rs      # Symbol operations
├── type_tests.rs        # Type operations
├── output_format_integration.rs  # Output format detection
├── unimplemented_tests.rs  # Graceful error tests for stub commands
└── e2e.rs               # Lightweight smoke test

Per-Suite Bridge Lifecycle

Tests requiring bridge interaction use DaemonTestHarness from common/mod.rs. Each test suite starts its own bridge instance to amortize 5-30s startup overhead across all tests in that file.

Why per-suite instead of per-test: Starting a bridge for every test would add 5-30 minutes to CI time for 60+ tests. Per-suite bridges run tests serially within the suite but allow parallel execution across different test files.

Why not shared global bridge: State leakage between suites causes flaky tests and debugging nightmares. Each suite gets isolation.

Data Flow

Test Suite Start
      |
      v
DaemonTestHarness::new()
      |
      +---> Start bridge (analyzeHeadless + GhidraCliBridge.java)
      +---> Wait for port file
      +---> Verify with ping
      |
      v
Run tests (serial within suite)
      |
      v
DaemonTestHarness::drop()
      |
      +---> Send shutdown command via TCP
      +---> Cleanup port/PID files

Running Tests

Run all tests:

cargo test

Run specific test suite:

cargo test --test daemon_tests
cargo test --test command_tests

Run single test:

cargo test --test command_tests test_version

Run tests that don't need Ghidra:

cargo test --test e2e --test command_tests --test output_format_integration

Test Requirements

Ghidra Installation

Tests assume Ghidra is installed. Use require_ghidra!() in tests that need a fast, explicit availability check; it fails the test if ghidra doctor fails.

Test Fixtures

Sample binary fixture required: tests/fixtures/sample_binary

Build fixture:

rustc --edition 2021 -o tests/fixtures/sample_binary tests/fixtures/sample_binary.rs

Fixture contains functions: add, multiply, factorial, fibonacci, process_string, xor_encrypt, simple_hash, init_data, main

Adding New Tests

Non-Bridge Tests

Add to appropriate file (command_tests.rs, project_tests.rs):

#[test]
fn test_my_command() {
    require_ghidra!();

    Command::cargo_bin("ghidra")
        .unwrap()
        .arg("my-command")
        .assert()
        .success();
}

Bridge-Dependent Tests

Add to the appropriate test file (e.g., symbol_tests.rs, find_tests.rs):

#[test]
#[serial]
fn test_my_query() {
    require_ghidra!();

    ensure_test_project(TEST_PROJECT, TEST_PROGRAM);

    let harness =
        DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start bridge");

    Command::cargo_bin("ghidra")
        .unwrap()
        .arg("--project")
        .arg(TEST_PROJECT)
        .arg("my-query")
        .arg("--program")
        .arg(TEST_PROGRAM)
        .assert()
        .success();

    drop(harness);
}

Mark with #[serial] to prevent bridge state races within suite.

Invariants

  • Bridge must be fully started before any query test runs
  • Each test suite gets its own bridge instance (no sharing)
  • Port/PID files must be cleaned up even on test failure (Drop impl)
  • Tests must not assume specific function addresses (use name-based lookups)

Troubleshooting

"Bridge failed to start within timeout"

Ghidra cold start can be slow. Ensure:

  • Ghidra installation is valid (ghidra doctor)
  • Sufficient disk space for temporary Ghidra project
  • Not running on extremely constrained CI resources

"Test fixture not found"

Compile sample_binary:

rustc --edition 2021 -o tests/fixtures/sample_binary tests/fixtures/sample_binary.rs

"Port already in use" / stale port files

Each test suite uses project-name-based port file paths to prevent collisions. This error suggests:

  • Previous test run leaked bridge process (kill manually)
  • Stale port/PID files in ~/.local/share/ghidra-cli/

Find leaked processes:

ps aux | grep ghidra
kill <pid>

Tests hang or timeout

  • Check if bridge is stuck (check process list)
  • Verify TCP connectivity on localhost
  • Increase timeout in test (bridge startup can vary)

Import/analysis takes too long

Tests use 300s timeout for import/analyze operations. On slow systems:

  • Run fewer parallel test suites
  • Ensure Ghidra has adequate heap memory
  • Check disk I/O performance

Tradeoffs

Per-suite vs per-test bridge: Chose speed over maximum isolation. Tests within suite are serial, but suite-to-suite parallelism maintained.

Project-name-based port files vs random ports: Each test suite uses a unique project name which maps to a unique port file via MD5 hash, preventing collisions.

Testing unimplemented commands: Adds maintenance burden but documents gaps and ensures graceful failures for stub commands.