test consolidation and CI fixes

This commit is contained in:
Alexander Kiselev
2026-02-06 08:40:18 -08:00
parent ae5daeb100
commit 660cb33014
16 changed files with 1674 additions and 1606 deletions
+151 -5
View File
@@ -10,7 +10,37 @@ env:
CARGO_TERM_COLOR: always
jobs:
test:
# Job 1: Unit tests and CLI tests (no Ghidra needed) - ~2 min
unit-and-cli:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust
uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --verbose
- name: Build test fixture
run: rustc --edition 2021 -o tests/fixtures/sample_binary tests/fixtures/sample_binary.rs
- name: Run unit tests
run: cargo test --lib --verbose
- name: Run CLI tests
run: cargo test --test e2e --test command_tests --test output_format_integration --verbose
# Job 2: Read-only integration tests (one bridge) - ~10-15 min
readonly-integration:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
@@ -41,6 +71,15 @@ jobs:
~/AppData/Local/ghidra-cli
key: ghidra-${{ matrix.os }}-v2
- name: Cache Ghidra projects
uses: actions/cache@v4
with:
path: |
~/.cache/ghidra-cli/projects
~/Library/Caches/ghidra-cli/projects
~/AppData/Local/ghidra-cli/cache/projects
key: ghidra-projects-${{ matrix.os }}-${{ hashFiles('tests/fixtures/sample_binary.rs') }}
- name: Build
run: cargo build --verbose
@@ -50,10 +89,117 @@ jobs:
- name: Setup Ghidra
run: cargo run -- setup --force
- name: Run unit tests
run: cargo test --lib --verbose
- name: Run read-only integration tests
run: cargo test --test readonly_tests --verbose
env:
RUST_LOG: info
- name: Run integration tests
run: cargo test --test '*' --verbose
# Job 3: Mutation tests (parallel with job 2) - ~10-15 min
mutation-integration:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- name: Install Java 21
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust
uses: Swatinem/rust-cache@v2
- name: Cache Ghidra
uses: actions/cache@v4
with:
path: |
~/.local/share/ghidra-cli
~/Library/Application Support/ghidra-cli
~/AppData/Local/ghidra-cli
key: ghidra-${{ matrix.os }}-v2
- name: Cache Ghidra projects
uses: actions/cache@v4
with:
path: |
~/.cache/ghidra-cli/projects
~/Library/Caches/ghidra-cli/projects
~/AppData/Local/ghidra-cli/cache/projects
key: ghidra-projects-${{ matrix.os }}-${{ hashFiles('tests/fixtures/sample_binary.rs') }}
- name: Build
run: cargo build --verbose
- name: Build test fixture
run: rustc --edition 2021 -o tests/fixtures/sample_binary tests/fixtures/sample_binary.rs
- name: Setup Ghidra
run: cargo run -- setup --force
- name: Run mutation integration tests
run: cargo test --test comment_tests --test symbol_tests --test patch_tests --test type_tests --test script_tests --verbose
env:
RUST_LOG: info
# Job 4: Infrastructure tests (parallel with jobs 2+3) - ~8-10 min
infrastructure:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- name: Install Java 21
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust
uses: Swatinem/rust-cache@v2
- name: Cache Ghidra
uses: actions/cache@v4
with:
path: |
~/.local/share/ghidra-cli
~/Library/Application Support/ghidra-cli
~/AppData/Local/ghidra-cli
key: ghidra-${{ matrix.os }}-v2
- name: Cache Ghidra projects
uses: actions/cache@v4
with:
path: |
~/.cache/ghidra-cli/projects
~/Library/Caches/ghidra-cli/projects
~/AppData/Local/ghidra-cli/cache/projects
key: ghidra-projects-${{ matrix.os }}-${{ hashFiles('tests/fixtures/sample_binary.rs') }}
- name: Build
run: cargo build --verbose
- name: Build test fixture
run: rustc --edition 2021 -o tests/fixtures/sample_binary tests/fixtures/sample_binary.rs
- name: Setup Ghidra
run: cargo run -- setup --force
- name: Run infrastructure tests
run: cargo test --test daemon_tests --test reliability_tests --test project_tests --verbose
env:
RUST_LOG: info
-173
View File
@@ -1,173 +0,0 @@
//! Tests for batch operations.
use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
use std::fs;
use std::path::PathBuf;
#[macro_use]
mod common;
use common::{ensure_test_project, DaemonTestHarness};
const TEST_PROJECT: &str = "batch-test";
const TEST_PROGRAM: &str = "sample_binary";
fn create_batch_file(content: &str) -> PathBuf {
let temp_dir = std::env::temp_dir();
let batch_file = temp_dir.join(format!("ghidra_batch_{}.txt", std::process::id()));
fs::write(&batch_file, content).expect("Failed to write batch file");
batch_file
}
#[test]
#[serial]
fn test_batch_multiple_queries() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let batch_content = r#"
# Test batch file
query --address 0x100000
query --function main
"#;
let batch_file = create_batch_file(batch_content);
Command::cargo_bin("ghidra")
.unwrap()
.arg("batch")
.arg("--project")
.arg(TEST_PROJECT)
.arg(batch_file.to_str().unwrap())
.assert()
.success()
.stdout(predicate::str::contains("commands_parsed"))
.stdout(predicate::str::contains("results"));
fs::remove_file(batch_file).ok();
drop(harness);
}
#[test]
#[serial]
fn test_batch_empty_file() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let batch_content = r#"
# Only comments
# More comments
"#;
let batch_file = create_batch_file(batch_content);
Command::cargo_bin("ghidra")
.unwrap()
.arg("batch")
.arg("--project")
.arg(TEST_PROJECT)
.arg(batch_file.to_str().unwrap())
.assert()
.success()
.stdout(predicate::str::contains("commands_parsed"));
fs::remove_file(batch_file).ok();
drop(harness);
}
#[test]
#[serial]
fn test_batch_with_comments() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let batch_content = r#"
# Query main function
query --function main
# Query by address
query --address 0x100000
# Another comment
"#;
let batch_file = create_batch_file(batch_content);
Command::cargo_bin("ghidra")
.unwrap()
.arg("batch")
.arg("--project")
.arg(TEST_PROJECT)
.arg(batch_file.to_str().unwrap())
.assert()
.success()
.stdout(predicate::str::contains("commands_parsed"))
.stdout(predicate::str::contains("2"));
fs::remove_file(batch_file).ok();
drop(harness);
}
#[test]
#[serial]
fn test_batch_invalid_file() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("batch")
.arg("--project")
.arg(TEST_PROJECT)
.arg("/nonexistent/batch/file.txt")
.assert()
.failure()
.stderr(predicate::str::contains("not found").or(predicate::str::contains("No such file")));
drop(harness);
}
#[test]
#[serial]
fn test_batch_with_invalid_command() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let batch_content = r#"
query --function main
invalid-command --arg value
query --address 0x100000
"#;
let batch_file = create_batch_file(batch_content);
Command::cargo_bin("ghidra")
.unwrap()
.arg("batch")
.arg("--project")
.arg(TEST_PROJECT)
.arg(batch_file.to_str().unwrap())
.assert()
.success()
.stdout(predicate::str::contains("commands_parsed"))
.stdout(predicate::str::contains("3"));
fs::remove_file(batch_file).ok();
drop(harness);
}
+20
View File
@@ -134,5 +134,25 @@ fn test_comment_delete() {
.assert()
.success();
// Verify comment is actually gone
let get_result = Command::cargo_bin("ghidra")
.unwrap()
.arg("comment")
.arg("get")
.arg(addr)
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.output()
.expect("Failed to run command");
let stdout = String::from_utf8_lossy(&get_result.stdout);
assert!(
!get_result.status.success() || !stdout.contains("to be deleted"),
"Comment should be deleted but was still found: {}",
stdout
);
drop(harness);
}
+6
View File
@@ -377,3 +377,9 @@ pub fn fixture_path(name: &str) -> PathBuf {
.join("fixtures")
.join(name)
}
/// Check if a function name matches an expected name, accounting for
/// platform differences (macOS adds underscore prefix to C symbols).
pub fn matches_function_name(actual: &str, expected: &str) -> bool {
actual == expected || actual == format!("_{}", expected)
}
+14 -1
View File
@@ -17,7 +17,6 @@ pub use schemas::Validate;
use anyhow::{Context, Result};
use std::path::PathBuf;
use std::process::Command;
use std::sync::Once;
use std::time::Duration;
@@ -31,6 +30,7 @@ pub fn fixture_binary() -> PathBuf {
/// Ensure test project exists with analyzed sample binary.
/// Uses Once::call_once for idempotent setup across multiple tests.
/// Skips import+analyze if the project already exists (supports CI caching).
pub fn ensure_test_project(project: &str, program: &str) {
static SETUP: Once = Once::new();
SETUP.call_once(|| {
@@ -42,6 +42,19 @@ pub fn ensure_test_project(project: &str, program: &str) {
);
}
// Check if project already exists (supports CI caching)
let project_dir = dirs::cache_dir()
.expect("Could not determine cache directory")
.join("ghidra-cli")
.join("projects")
.join(project);
let gpr_file = project_dir.join(format!("{}.gpr", project));
if gpr_file.exists() {
eprintln!("=== Using cached test project: {:?} ===", gpr_file);
return;
}
eprintln!("=== Setting up test project (import + analyze) ===");
// Step 1: Import the binary
-62
View File
@@ -1,62 +0,0 @@
//! Tests for diff operations.
use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
#[macro_use]
mod common;
use common::{ensure_test_project, DaemonTestHarness};
const TEST_PROJECT: &str = "diff-test";
const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
fn test_diff_programs() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
// diff programs compares two programs by name (no --program flag needed)
Command::cargo_bin("ghidra")
.unwrap()
.arg("diff")
.arg("programs")
.arg(TEST_PROGRAM)
.arg(TEST_PROGRAM)
.arg("--project")
.arg(TEST_PROJECT)
.assert()
.success();
drop(harness);
}
#[test]
#[serial]
fn test_diff_functions() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
// diff functions requires two function names/addresses
// Using main for both since we just want to verify command works
// (note: _start doesn't exist on macOS Mach-O binaries)
Command::cargo_bin("ghidra")
.unwrap()
.arg("diff")
.arg("functions")
.arg("main")
.arg("main")
.arg("--project")
.arg(TEST_PROJECT)
.assert()
.success();
drop(harness);
}
-379
View File
@@ -1,379 +0,0 @@
//! Tests for disassembly operations.
//!
//! These tests verify that disassembly commands work correctly by:
//! 1. Validating instruction schema structure
//! 2. Using dynamically resolved addresses
//! 3. Verifying instruction limits work correctly
//! 4. Testing error handling for invalid inputs
use serial_test::serial;
#[macro_use]
mod common;
use common::{
ensure_test_project, get_function_address, ghidra,
schemas::{DisasmResult, Instruction, Validate},
DaemonTestHarness,
};
const TEST_PROJECT: &str = "disasm-test";
const TEST_PROGRAM: &str = "sample_binary";
// ============================================================================
// Basic Disassembly Tests
// ============================================================================
/// Test disassembly at dynamically resolved main address.
#[test]
#[serial]
fn test_disasm_at_main() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
// Get main's address dynamically instead of hardcoding
let main_addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let result = ghidra(&harness)
.arg("disasm")
.arg(&main_addr)
.arg("--program")
.arg(TEST_PROGRAM)
.arg("--format")
.arg("json")
.run();
result.assert_success();
// Try to parse as DisasmResult
if let Some(disasm) = result.try_json::<DisasmResult>() {
assert!(
!disasm.results.is_empty(),
"Should have at least one instruction"
);
// Validate instruction structure
for instr in &disasm.results {
instr.assert_valid();
}
} else if let Some(instructions) = result.try_json::<Vec<Instruction>>() {
// Some outputs might be a direct array
assert!(
!instructions.is_empty(),
"Should have at least one instruction"
);
for instr in &instructions {
instr.assert_valid();
}
}
}
/// Test disassembly with instruction limit.
#[test]
#[serial]
fn test_disasm_with_instruction_limit() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let main_addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let limit = 5;
let result = ghidra(&harness)
.arg("disasm")
.arg(&main_addr)
.arg("--instructions")
.arg(limit.to_string())
.arg("--program")
.arg(TEST_PROGRAM)
.arg("--format")
.arg("json")
.run();
result.assert_success();
// Verify limit is respected
if let Some(disasm) = result.try_json::<DisasmResult>() {
assert!(
disasm.results.len() <= limit,
"Should return at most {} instructions, got {}",
limit,
disasm.results.len()
);
// Each instruction should be valid
for instr in &disasm.results {
instr.assert_valid();
}
} else if let Some(instructions) = result.try_json::<Vec<Instruction>>() {
assert!(
instructions.len() <= limit,
"Should return at most {} instructions, got {}",
limit,
instructions.len()
);
}
}
/// Test disassembly with very small limit.
#[test]
#[serial]
fn test_disasm_small_count() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let main_addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let result = ghidra(&harness)
.arg("disasm")
.arg(&main_addr)
.arg("--instructions")
.arg("1")
.arg("--program")
.arg(TEST_PROGRAM)
.arg("--format")
.arg("json")
.run();
result.assert_success();
// Should return exactly 1 instruction (or possibly 0 if at end)
if let Some(disasm) = result.try_json::<DisasmResult>() {
assert!(
disasm.results.len() <= 1,
"Should return at most 1 instruction, got {}",
disasm.results.len()
);
}
}
// ============================================================================
// Instruction Content Verification
// ============================================================================
/// Test that disassembly returns expected instruction fields.
#[test]
#[serial]
fn test_disasm_instruction_fields() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let main_addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let result = ghidra(&harness)
.arg("disasm")
.arg(&main_addr)
.arg("--instructions")
.arg("10")
.arg("--program")
.arg(TEST_PROGRAM)
.arg("--format")
.arg("json")
.run();
result.assert_success();
if let Some(disasm) = result.try_json::<DisasmResult>() {
assert!(!disasm.results.is_empty(), "Should have instructions");
let first = &disasm.results[0];
// Verify essential fields are present
assert!(!first.mnemonic.is_empty(), "Mnemonic should not be empty");
assert!(!first.address.is_empty(), "Address should not be empty");
// Verify address is valid hex (Ghidra may or may not include 0x prefix)
let addr_hex = first
.address
.strip_prefix("0x")
.or_else(|| first.address.strip_prefix("0X"))
.unwrap_or(&first.address);
assert!(
!addr_hex.is_empty() && addr_hex.bytes().all(|b| b.is_ascii_hexdigit()),
"Address should be hex format, got: {}",
first.address
);
// Function prologue typically starts with PUSH, SUB, ENDBR, or similar (x86)
// or STP, SUB, MOV, etc. (ARM64)
let common_first_instr = [
"PUSH", "SUB", "MOV", "ENDBR", "LEA", "XOR", "JMP", // x86
"STP", "STR", "BL", "NOP", "ADRP", "ADD", "RET", // ARM64
];
let mnemonic_upper = first.mnemonic.to_uppercase();
// This is a soft check - just log if unexpected
if !common_first_instr
.iter()
.any(|&m| mnemonic_upper.starts_with(m))
{
eprintln!(
"Note: First instruction is '{}' - unusual but not necessarily wrong",
first.mnemonic
);
}
}
}
// ============================================================================
// Error Handling Tests
// ============================================================================
/// Test disassembly at invalid address fails gracefully.
#[test]
#[serial]
fn test_disasm_invalid_address() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let result = ghidra(&harness)
.arg("disasm")
.arg("0xFFFFFFFFFFFFFFFF") // Unmapped address
.arg("--program")
.arg(TEST_PROGRAM)
.run();
// Should fail or return empty results
// (exact behavior depends on implementation)
if result.exit_code == 0 {
// If it succeeds, should have empty results or error indication
if let Some(_disasm) = result.try_json::<DisasmResult>() {
// Empty results are acceptable for unmapped address
// Or it might have an error field
}
} else {
// Failure is acceptable for unmapped address
// Should have some error message
assert!(
!result.stderr.is_empty() || !result.stdout.is_empty(),
"Should provide some output explaining the error"
);
}
}
/// Test disassembly with missing program argument.
#[test]
#[serial]
fn test_disasm_missing_program() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let result = ghidra(&harness)
.arg("disasm")
.arg("0x101000")
// --program is missing
.run();
// Should fail with helpful error
result.assert_failure();
}
/// Test disassembly with zero instruction count.
#[test]
#[serial]
fn test_disasm_zero_instructions() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let main_addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let result = ghidra(&harness)
.arg("disasm")
.arg(&main_addr)
.arg("--instructions")
.arg("0")
.arg("--program")
.arg(TEST_PROGRAM)
.run();
// Should either fail gracefully or return empty results
if result.exit_code == 0 {
if let Some(disasm) = result.try_json::<DisasmResult>() {
assert!(
disasm.results.is_empty(),
"Zero instruction count should return empty results"
);
}
}
// Failure with error message is also acceptable
}
// ============================================================================
// Snapshot Tests
// ============================================================================
/// Test that disassembly JSON output has expected structure.
#[test]
#[serial]
fn test_disasm_output_format_structure() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let main_addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let result = ghidra(&harness)
.arg("disasm")
.arg(&main_addr)
.arg("--instructions")
.arg("3")
.arg("--program")
.arg(TEST_PROGRAM)
.arg("--format")
.arg("json")
.run();
result.assert_success();
// Parse and validate JSON structure
let json: serde_json::Value =
serde_json::from_str(&result.stdout).expect("Output should be valid JSON");
// Should be an array (or object with results array)
let instructions = if let Some(arr) = json.as_array() {
arr.clone()
} else if let Some(obj) = json.as_object() {
obj.get("results")
.and_then(|v| v.as_array())
.expect("Object should have 'results' array")
.clone()
} else {
panic!("Expected JSON array or object with results");
};
assert!(
!instructions.is_empty() && instructions.len() <= 3,
"Expected 1-3 instructions, got {}",
instructions.len()
);
// Each instruction should have required fields
for instr in &instructions {
let obj = instr.as_object().expect("Instruction should be an object");
assert!(obj.contains_key("address"), "Missing 'address' field");
assert!(obj.contains_key("mnemonic"), "Missing 'mnemonic' field");
assert!(obj.contains_key("bytes"), "Missing 'bytes' field");
}
}
-205
View File
@@ -1,205 +0,0 @@
//! Tests for find/search operations.
use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
#[macro_use]
mod common;
use common::{ensure_test_project, DaemonTestHarness};
const TEST_PROJECT: &str = "find-test";
const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
fn test_find_string() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("find")
.arg("string")
.arg("test")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success();
drop(harness);
}
#[test]
#[serial]
fn test_find_bytes() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("find")
.arg("bytes")
.arg("4883ec08")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success();
drop(harness);
}
#[test]
#[serial]
fn test_find_function() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("find")
.arg("function")
.arg("main")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("main"));
drop(harness);
}
#[test]
#[serial]
fn test_find_function_glob() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("find")
.arg("function")
.arg("m*")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("main"));
drop(harness);
}
#[test]
#[serial]
fn test_find_calls() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
// Use main which exists on all platforms (add_numbers becomes _add_numbers on macOS)
Command::cargo_bin("ghidra")
.unwrap()
.arg("find")
.arg("calls")
.arg("main")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success();
drop(harness);
}
#[test]
#[serial]
fn test_find_crypto() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("find")
.arg("crypto")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success();
drop(harness);
}
#[test]
#[serial]
fn test_find_interesting() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("find")
.arg("interesting")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success();
drop(harness);
}
#[test]
#[serial]
fn test_find_string_no_matches() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("find")
.arg("string")
.arg("nonexistent_string_xyz123")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success();
drop(harness);
}
-110
View File
@@ -1,110 +0,0 @@
//! Tests for graph operations.
use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
#[macro_use]
mod common;
use common::{ensure_test_project, DaemonTestHarness};
const TEST_PROJECT: &str = "graph-test";
const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
fn test_graph_calls() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("graph")
.arg("calls")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("nodes"))
.stdout(predicate::str::contains("edges"));
drop(harness);
}
#[test]
#[serial]
fn test_graph_callers() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("graph")
.arg("callers")
.arg("main")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success();
drop(harness);
}
#[test]
#[serial]
fn test_graph_callees() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("graph")
.arg("callees")
.arg("main")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success();
drop(harness);
}
#[test]
#[serial]
fn test_graph_export_dot() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("graph")
.arg("export")
.arg("dot")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("digraph"));
drop(harness);
}
-125
View File
@@ -1,125 +0,0 @@
//! Tests for program operations.
use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
#[macro_use]
mod common;
use common::{ensure_test_project, DaemonTestHarness};
const TEST_PROJECT: &str = "program-test";
const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
fn test_program_info() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("program")
.arg("info")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("name"))
.stdout(predicate::str::contains("format"));
drop(harness);
}
#[test]
#[serial]
fn test_program_export_json() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let output = Command::cargo_bin("ghidra")
.unwrap()
.arg("program")
.arg("export")
.arg("json")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.output()
.expect("Failed to run command");
// export_program may not be implemented in the bridge
// Accept either success or "Unknown command" error
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("functions") || !stdout.is_empty(),
"Export should produce output"
);
}
drop(harness);
}
#[test]
#[serial]
fn test_program_close() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let output = Command::cargo_bin("ghidra")
.unwrap()
.arg("program")
.arg("close")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.output()
.expect("Failed to run command");
// close_program may not be implemented in the bridge
// Accept either success or "Unknown command" error
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success() || stderr.contains("Unknown command"),
"Expected success or 'Unknown command', got: {}",
stderr
);
drop(harness);
}
#[test]
#[serial]
fn test_program_info_no_program() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
// Without --program, bridge may return info for all programs (default behavior)
Command::cargo_bin("ghidra")
.unwrap()
.arg("program")
.arg("info")
.arg("--project")
.arg(TEST_PROJECT)
.assert()
.success();
drop(harness);
}
-436
View File
@@ -1,436 +0,0 @@
//! Tests for query commands that require daemon.
//!
//! These tests verify that query commands return properly structured data by:
//! 1. Validating JSON output against typed schemas
//! 2. Checking semantic correctness (e.g., functions have names and addresses)
//! 3. Testing filter and limit parameters work correctly
//! 4. Using snapshot testing for format regression detection
use once_cell::sync::Lazy;
use serial_test::serial;
#[macro_use]
mod common;
use common::{
ensure_test_project, get_function_address, ghidra,
schemas::{Function, MemoryBlock, StringData, Validate},
DaemonTestHarness,
};
const TEST_PROJECT: &str = "query-test";
const TEST_PROGRAM: &str = "sample_binary";
static HARNESS: Lazy<DaemonTestHarness> = Lazy::new(|| {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon")
});
// ============================================================================
// Function List Tests
// ============================================================================
/// Test function list returns valid, well-formed data.
#[test]
#[serial]
fn test_function_list_schema_validation() {
require_ghidra!();
let harness = &*HARNESS;
let result = ghidra(harness)
.arg("function")
.arg("list")
.with_project(TEST_PROJECT, TEST_PROGRAM)
.arg("--format")
.arg("json")
.run();
result.assert_success();
// Parse and validate the schema
let functions: Vec<Function> = result.json();
// Should have at least one function (main at minimum)
assert!(!functions.is_empty(), "Function list should not be empty");
// Validate each function has required fields
for func in &functions {
func.assert_valid();
}
// Verify expected functions exist
let has_main = functions.iter().any(|f| f.name == "main");
assert!(has_main, "Should contain main function");
}
/// Test function list contains expected functions from sample_binary.
#[test]
#[serial]
fn test_function_list_contains_expected_functions() {
require_ghidra!();
let harness = &*HARNESS;
let result = ghidra(harness)
.arg("function")
.arg("list")
.with_project(TEST_PROJECT, TEST_PROGRAM)
.arg("--format")
.arg("json")
.run();
result.assert_success();
let functions: Vec<Function> = result.json();
let names: Vec<&str> = functions.iter().map(|f| f.name.as_str()).collect();
// Check for known functions in sample_binary
assert!(
names.iter().any(|n| *n == "main"),
"Should have main function. Found: {:?}",
names
);
// The sample binary should have other functions too
assert!(
functions.len() >= 2,
"Should have multiple functions, found {}",
functions.len()
);
}
/// Test function list --limit parameter works correctly.
#[test]
#[serial]
fn test_function_list_limit() {
require_ghidra!();
let harness = &*HARNESS;
let limit = 3;
let result = ghidra(harness)
.arg("function")
.arg("list")
.with_project(TEST_PROJECT, TEST_PROGRAM)
.arg("--format")
.arg("json")
.arg("--limit")
.arg(limit.to_string())
.run();
result.assert_success();
let functions: Vec<Function> = result.json();
assert!(
functions.len() <= limit,
"Limit {} should return at most {} functions, got {}",
limit,
limit,
functions.len()
);
}
/// Test function list --filter parameter works correctly.
#[test]
#[serial]
fn test_function_list_filter() {
require_ghidra!();
let harness = &*HARNESS;
let result = ghidra(harness)
.arg("function")
.arg("list")
.with_project(TEST_PROJECT, TEST_PROGRAM)
.arg("--format")
.arg("json")
.arg("--filter")
.arg("main")
.run();
result.assert_success();
let functions: Vec<Function> = result.json();
// Should return at least one result
assert!(
!functions.is_empty(),
"Filter 'main' should match at least one function"
);
// At least one returned function should match the filter
let has_main = functions
.iter()
.any(|f| f.name.to_lowercase().contains("main"));
assert!(
has_main,
"At least one filtered result should contain 'main'. Got: {:?}",
functions.iter().map(|f| &f.name).collect::<Vec<_>>()
);
}
// ============================================================================
// Strings List Tests
// ============================================================================
/// Test strings list returns valid data.
#[test]
#[serial]
fn test_strings_list_schema_validation() {
require_ghidra!();
let harness = &*HARNESS;
let result = ghidra(harness)
.arg("strings")
.arg("list")
.with_project(TEST_PROJECT, TEST_PROGRAM)
.arg("--format")
.arg("json")
.arg("--limit")
.arg("50")
.run();
result.assert_success();
// Try to parse as StringData array
if let Some(strings) = result.try_json::<Vec<StringData>>() {
for s in &strings {
s.assert_valid();
}
}
}
// ============================================================================
// Memory Map Tests
// ============================================================================
/// Test memory map returns valid segment information.
#[test]
#[serial]
fn test_memory_map_schema_validation() {
require_ghidra!();
let harness = &*HARNESS;
let result = ghidra(harness)
.arg("memory")
.arg("map")
.with_project(TEST_PROJECT, TEST_PROGRAM)
.arg("--format")
.arg("json")
.run();
result.assert_success();
// Try to parse as MemoryBlock array
if let Some(blocks) = result.try_json::<Vec<MemoryBlock>>() {
assert!(
!blocks.is_empty(),
"Memory map should have at least one block"
);
for block in &blocks {
block.assert_valid();
}
// Should have a .text segment (code)
let has_text = blocks.iter().any(|b| {
b.name.contains("text") || b.name.contains("code") || b.name.contains(".text")
});
assert!(
has_text,
"Should have a text/code segment. Found: {:?}",
blocks.iter().map(|b| &b.name).collect::<Vec<_>>()
);
}
}
// ============================================================================
// Summary Tests
// ============================================================================
/// Test summary command returns expected fields.
#[test]
#[serial]
fn test_summary_contains_expected_fields() {
require_ghidra!();
let harness = &*HARNESS;
let result = ghidra(harness)
.arg("summary")
.with_project(TEST_PROJECT, TEST_PROGRAM)
.run();
result.assert_success();
// Summary should contain some output
assert!(
!result.stdout.trim().is_empty(),
"Summary should produce output"
);
}
// ============================================================================
// Decompile Tests
// ============================================================================
/// Test decompiling by function name.
#[test]
#[serial]
fn test_decompile_by_name() {
require_ghidra!();
let harness = &*HARNESS;
let result = ghidra(harness)
.arg("decompile")
.arg("main")
.with_project(TEST_PROJECT, TEST_PROGRAM)
.run();
result.assert_success();
// Decompiled output should contain C-like code
assert!(
result.stdout.contains("void")
|| result.stdout.contains("int")
|| result.stdout.contains("{")
|| result.stdout.contains("return"),
"Decompiled output should contain C-like code.\nGot: {}",
result.stdout
);
}
/// Test decompiling by address (using dynamically resolved address).
#[test]
#[serial]
fn test_decompile_by_address() {
require_ghidra!();
let harness = &*HARNESS;
// Get main's address dynamically
let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
let result = ghidra(harness)
.arg("decompile")
.arg(&main_addr)
.with_project(TEST_PROJECT, TEST_PROGRAM)
.run();
result.assert_success();
// Should produce some output (decompiled code)
assert!(
!result.stdout.trim().is_empty(),
"Decompile should produce output"
);
}
/// Test decompiling nonexistent function fails gracefully.
#[test]
#[serial]
fn test_decompile_nonexistent_function() {
require_ghidra!();
let harness = &*HARNESS;
let result = ghidra(harness)
.arg("decompile")
.arg("this_function_definitely_does_not_exist_xyz123")
.with_project(TEST_PROJECT, TEST_PROGRAM)
.run();
// Should fail or return empty/error
// (behavior depends on implementation)
if result.exit_code == 0 {
// If it "succeeds", should indicate no function found
assert!(
result.stdout.to_lowercase().contains("not found")
|| result.stdout.to_lowercase().contains("error")
|| result.stdout.trim().is_empty(),
"Should indicate function not found"
);
}
}
// ============================================================================
// XRef Tests
// ============================================================================
/// Test xref to address.
#[test]
#[serial]
fn test_xref_to() {
require_ghidra!();
let harness = &*HARNESS;
let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
let result = ghidra(harness)
.arg("xref")
.arg("to")
.arg(&main_addr)
.with_project(TEST_PROJECT, TEST_PROGRAM)
.run();
result.assert_success();
// XRefs to main might be empty (if nothing calls main) or have entries
// Just verify it doesn't crash
}
/// Test xref from address.
#[test]
#[serial]
fn test_xref_from() {
require_ghidra!();
let harness = &*HARNESS;
let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
let result = ghidra(harness)
.arg("xref")
.arg("from")
.arg(&main_addr)
.with_project(TEST_PROJECT, TEST_PROGRAM)
.run();
result.assert_success();
// main likely calls other functions, but even if not, shouldn't crash
}
// ============================================================================
// Snapshot Tests
// ============================================================================
/// Test that function list JSON output has expected structure.
#[test]
#[serial]
fn test_function_list_json_structure() {
require_ghidra!();
let harness = &*HARNESS;
let result = ghidra(harness)
.arg("function")
.arg("list")
.with_project(TEST_PROJECT, TEST_PROGRAM)
.arg("--format")
.arg("json")
.arg("--limit")
.arg("3")
.run();
result.assert_success();
// Validate JSON structure
let json: serde_json::Value =
serde_json::from_str(&result.stdout).expect("Output should be valid JSON");
// Should be an array of functions
let functions = json.as_array().expect("Expected JSON array of functions");
assert!(
!functions.is_empty() && functions.len() <= 3,
"Expected 1-3 functions, got {}",
functions.len()
);
// Each function should have name field
for func in functions {
let obj = func.as_object().expect("Function should be an object");
assert!(obj.contains_key("name"), "Missing 'name' field");
}
}
File diff suppressed because it is too large Load Diff
-98
View File
@@ -1,98 +0,0 @@
//! Tests for stats command.
use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
#[macro_use]
mod common;
use common::{ensure_test_project, DaemonTestHarness};
const TEST_PROJECT: &str = "stats-test";
const TEST_PROGRAM: &str = "sample_binary";
#[test]
#[serial]
fn test_stats_normal() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("stats")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("stats"))
.stdout(predicate::str::contains("functions"))
.stdout(predicate::str::contains("symbols"));
drop(harness);
}
#[test]
#[serial]
fn test_stats_has_all_fields() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
.unwrap()
.arg("stats")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("functions"))
.stdout(predicate::str::contains("symbols"))
.stdout(predicate::str::contains("strings"))
.stdout(predicate::str::contains("imports"))
.stdout(predicate::str::contains("exports"))
.stdout(predicate::str::contains("memory_size"))
.stdout(predicate::str::contains("sections"))
.stdout(predicate::str::contains("data_types"));
drop(harness);
}
#[test]
#[serial]
fn test_stats_json_format() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let output = Command::cargo_bin("ghidra")
.unwrap()
.arg("stats")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.get_output()
.stdout
.clone();
let output_str = String::from_utf8_lossy(&output);
assert!(
serde_json::from_str::<serde_json::Value>(&output_str).is_ok(),
"Output should be valid JSON"
);
drop(harness);
}
+48 -3
View File
@@ -22,7 +22,7 @@ fn test_symbol_list() {
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
Command::cargo_bin("ghidra")
let output = Command::cargo_bin("ghidra")
.unwrap()
.arg("symbol")
.arg("list")
@@ -30,8 +30,19 @@ fn test_symbol_list() {
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success();
.output()
.expect("Failed to run command");
assert!(output.status.success(), "symbol list should succeed");
let stdout = String::from_utf8_lossy(&output.stdout);
// Known functions should appear as symbols
// On macOS, names may have underscore prefix
assert!(
stdout.contains("main") || stdout.contains("_main"),
"symbol list should contain main. Output: {}",
stdout
);
drop(harness);
}
@@ -114,6 +125,40 @@ fn test_symbol_rename() {
.assert()
.success();
// Verify new symbol exists
Command::cargo_bin("ghidra")
.unwrap()
.arg("symbol")
.arg("get")
.arg("new_symbol")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("new_symbol"));
// Verify old symbol is gone
let old_result = Command::cargo_bin("ghidra")
.unwrap()
.arg("symbol")
.arg("get")
.arg("old_symbol")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.output()
.expect("Failed to run command");
let old_stdout = String::from_utf8_lossy(&old_result.stdout);
// Old symbol should either not be found or not appear in results
// (it might still exist if rename creates a copy rather than moving)
if old_result.status.success() {
eprintln!("Note: old_symbol still accessible after rename (may be expected)");
}
drop(harness);
}
+14
View File
@@ -80,6 +80,20 @@ fn test_type_create() {
.assert()
.success();
// Verify created type exists
Command::cargo_bin("ghidra")
.unwrap()
.arg("type")
.arg("get")
.arg("MyTestStruct")
.arg("--project")
.arg(TEST_PROJECT)
.arg("--program")
.arg(TEST_PROGRAM)
.assert()
.success()
.stdout(predicate::str::contains("MyTestStruct"));
drop(harness);
}
-9
View File
@@ -1,9 +0,0 @@
//! Tests for graceful error handling.
//!
//! Most commands are now implemented and require the daemon.
//! This file is kept for documentation but has no active tests.
//!
//! To run integration tests that require Ghidra:
//! cargo test -- --ignored
// No tests - all commands are now implemented