mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
test consolidation and CI fixes
This commit is contained in:
+151
-5
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user