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>
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.