Add E2E test infrastructure: - DaemonTestHarness for managing daemon lifecycle in tests - Test fixtures and helpers in tests/common/ - Sample binary fixture for integration tests Add test coverage: - command_tests.rs: version, doctor, config commands - project_tests.rs: project create/list/info/delete, import, analyze - daemon_tests.rs: daemon start/status/ping/stop/clear-cache - query_tests.rs: function list, strings, memory, decompile, xref - unimplemented_tests.rs: 39 tests for graceful error messages Fix CLI bugs: - DisasmArgs: rename count to num_instructions (--instructions/-n) to avoid conflict with QueryOptions.count - GraphExportArgs: add unique arg id for format positional to avoid conflict with QueryOptions.format Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3.8 KiB
Common Test Utilities
Shared infrastructure for E2E tests.
DaemonTestHarness
Manages daemon lifecycle for test suites requiring Ghidra daemon interaction.
Usage
use common::{DaemonTestHarness, ensure_test_project};
const TEST_PROJECT: &str = "my-test";
const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
fn test_with_daemon() {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM)
.expect("Failed to start daemon");
let mut client = harness.client().unwrap();
// Use client for IPC calls
// Daemon automatically shuts down when harness drops
}
Shared Daemon Pattern
For multiple tests in same suite:
use once_cell::sync::Lazy;
static HARNESS: Lazy<DaemonTestHarness> = Lazy::new(|| {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM)
.expect("Failed to start daemon")
});
#[test]
#[serial]
fn test_one() {
let harness = &*HARNESS;
// Use harness
}
#[test]
#[serial]
fn test_two() {
let harness = &*HARNESS;
// Same daemon instance
}
All tests using shared daemon must be marked #[serial] to prevent state races.
Why Runtime Field Exists
DaemonTestHarness contains a tokio::runtime::Runtime field to:
- Prevent panic-during-panic: Creating Runtime during Drop panic unwinding causes abort. Pre-created runtime allows safe cleanup.
- Amortize overhead: Runtime creation takes ~10ms. Reusing across all async operations saves time.
Socket Path Isolation
Each harness instance generates UUID-based Unix socket path:
/tmp/ghidra-test-<uuid>.sock
UUID prevents collisions:
- Between parallel test suites
- Across test runs on long-running CI (PID can wrap)
Cleanup Guarantees
Drop implementation ensures best-effort cleanup:
- Send shutdown via IPC (ignores errors)
- Wait up to 5s for graceful exit
- Kill process if still running
- Remove socket file
Accepts minor leak risk on panic-during-panic (rare edge case).
Fixtures
fixture_binary()
Returns path to compiled sample_binary fixture.
let binary = fixture_binary();
assert!(binary.exists());
Binary must be compiled before tests:
rustc --edition 2021 -o tests/fixtures/sample_binary tests/fixtures/sample_binary.rs
ensure_test_project()
Idempotent project setup using Once::call_once. Imports and analyzes sample_binary if needed.
ensure_test_project("my-project", "sample_binary");
// Second call does nothing - project already exists
Handles "already exists" errors gracefully. Safe to call from multiple tests.
skip_if_no_ghidra! Macro
Tests should call this macro to skip gracefully when Ghidra unavailable:
#[test]
fn test_something() {
skip_if_no_ghidra!();
// Test code runs only if ghidra doctor succeeds
}
Runs ghidra doctor and returns early if fails. Prints message: "Skipping test: Ghidra not available"
Design Decisions
Exponential Backoff Parameters
wait_for_ready() uses:
- Initial delay: 100ms (responsive for fast starts)
- Multiplier: 2x
- Max attempts: 12
- Total timeout: 120s
Covers 100ms to ~200s range. Typical fast start exits in <5s.
ChildGuard Pattern
DaemonTestHarness::new() uses ChildGuard to prevent daemon process leaks:
struct ChildGuard(Option<Child>);
impl Drop for ChildGuard {
fn drop(&mut self) {
if let Some(mut child) = self.0.take() {
let _ = child.kill();
}
}
}
If wait_for_ready() returns early due to error, ChildGuard ensures daemon process is killed. Without this, failed initialization leaks processes.
Why 5s Shutdown Timeout
Most daemons shut down in <1s. 5s allows graceful cleanup without blocking tests indefinitely. If daemon hangs, hard kill prevents test suite deadlock.