fix: share bridge across mutation tests via OnceLock to prevent macOS data loss

On macOS, Ghidra stores project data in mmap pages that aren't flushed
to disk while the bridge runs. Per-test DaemonTestHarness stop/restart
cycles cause data loss. Use OnceLock<DaemonTestHarness> to share a
single bridge per test binary, matching the readonly_tests pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Kiselev
2026-02-22 15:57:50 -08:00
co-authored by Claude Opus 4.6
parent 6c2149c861
commit ad01eaf930
5 changed files with 97 additions and 132 deletions
+16 -21
View File
@@ -3,6 +3,7 @@
use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
use std::sync::OnceLock;
#[macro_use]
mod common;
@@ -13,17 +14,23 @@ use common::{
const TEST_PROJECT: &str = "ci-test";
const TEST_PROGRAM: &str = "sample_binary";
static HARNESS: OnceLock<DaemonTestHarness> = OnceLock::new();
fn harness() -> &'static DaemonTestHarness {
HARNESS.get_or_init(|| {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon")
})
}
#[test]
#[serial]
fn test_comment_set_and_get() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let harness = harness();
// Dynamically resolve an address with a code unit
let addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
Command::cargo_bin("ghidra")
.unwrap()
@@ -51,21 +58,16 @@ fn test_comment_set_and_get() {
.assert()
.success()
.stdout(predicate::str::contains("test comment"));
drop(harness);
}
#[test]
#[serial]
fn test_comment_list() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let harness = harness();
// Use a dynamically resolved function address
let addrs = get_function_addresses(&harness, TEST_PROJECT, TEST_PROGRAM, 2);
let addrs = get_function_addresses(harness, TEST_PROJECT, TEST_PROGRAM, 2);
let addr = &addrs[0];
Command::cargo_bin("ghidra")
@@ -92,21 +94,16 @@ fn test_comment_list() {
.assert()
.success()
.stdout(predicate::str::contains("another comment"));
drop(harness);
}
#[test]
#[serial]
fn test_comment_delete() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let harness = harness();
// Use a dynamically resolved function address
let addrs = get_function_addresses(&harness, TEST_PROJECT, TEST_PROGRAM, 3);
let addrs = get_function_addresses(harness, TEST_PROJECT, TEST_PROGRAM, 3);
let addr = &addrs[addrs.len() - 1];
Command::cargo_bin("ghidra")
@@ -153,6 +150,4 @@ fn test_comment_delete() {
"Comment should be deleted but was still found: {}",
stdout
);
drop(harness);
}
+35 -52
View File
@@ -7,6 +7,7 @@
//! 4. Using snapshot testing for output format regression detection
use serial_test::serial;
use std::sync::OnceLock;
#[macro_use]
mod common;
@@ -15,6 +16,15 @@ use common::{ensure_test_project, get_function_address, ghidra, DaemonTestHarnes
const TEST_PROJECT: &str = "ci-test";
const TEST_PROGRAM: &str = "sample_binary";
static HARNESS: OnceLock<DaemonTestHarness> = OnceLock::new();
fn harness() -> &'static DaemonTestHarness {
HARNESS.get_or_init(|| {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon")
})
}
/// Test patching bytes at a dynamically resolved address.
///
/// Verifies:
@@ -25,15 +35,12 @@ const TEST_PROGRAM: &str = "sample_binary";
#[serial]
fn test_patch_bytes_success() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let harness = harness();
// Dynamically get a valid code address
let main_addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let main_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
let result = ghidra(&harness)
let result = ghidra(harness)
.arg("patch")
.arg("bytes")
.arg(&main_addr)
@@ -57,14 +64,11 @@ fn test_patch_bytes_success() {
#[serial]
fn test_patch_nop_success() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = harness();
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 main_addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let result = ghidra(&harness)
let result = ghidra(harness)
.arg("patch")
.arg("nop")
.arg(&main_addr)
@@ -87,15 +91,12 @@ fn test_patch_nop_success() {
#[serial]
fn test_patch_export() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let harness = harness();
// Use a unique output path to avoid conflicts
let output_path = format!("/tmp/ghidra-test-export-{}.bin", uuid::Uuid::new_v4());
let result = ghidra(&harness)
let result = ghidra(harness)
.arg("patch")
.arg("export")
.arg("--output")
@@ -123,16 +124,13 @@ fn test_patch_export() {
#[serial]
fn test_patch_at_function_boundary() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let harness = harness();
// Get any function's entry point
let func_addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let func_addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
// Patch with RET instruction (c3 on x86)
let result = ghidra(&harness)
let result = ghidra(harness)
.arg("patch")
.arg("bytes")
.arg(&func_addr)
@@ -158,13 +156,10 @@ fn test_patch_at_function_boundary() {
#[serial]
fn test_patch_invalid_address_fails() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let harness = harness();
// Use an address that's definitely outside the program's memory
let result = ghidra(&harness)
let result = ghidra(harness)
.arg("patch")
.arg("bytes")
.arg("0xffffffffffffffff") // Very high address, unlikely to be mapped
@@ -193,14 +188,11 @@ fn test_patch_invalid_address_fails() {
#[serial]
fn test_patch_invalid_hex_fails() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = harness();
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 main_addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let result = ghidra(&harness)
let result = ghidra(harness)
.arg("patch")
.arg("bytes")
.arg(&main_addr)
@@ -218,14 +210,11 @@ fn test_patch_invalid_hex_fails() {
#[serial]
fn test_patch_odd_hex_length() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = harness();
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 main_addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let _result = ghidra(&harness)
let _result = ghidra(harness)
.arg("patch")
.arg("bytes")
.arg(&main_addr)
@@ -248,14 +237,11 @@ fn test_patch_odd_hex_length() {
#[serial]
fn test_patch_without_program_arg() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = harness();
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 main_addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let result = ghidra(&harness)
let result = ghidra(harness)
.arg("patch")
.arg("bytes")
.arg(&main_addr)
@@ -280,14 +266,11 @@ fn test_patch_without_program_arg() {
#[serial]
fn test_patch_output_format_structure() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = harness();
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 main_addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let result = ghidra(&harness)
let result = ghidra(harness)
.arg("patch")
.arg("bytes")
.arg(&main_addr)
+14 -16
View File
@@ -4,6 +4,7 @@ use assert_cmd::Command;
use serial_test::serial;
use std::fs;
use std::path::PathBuf;
use std::sync::OnceLock;
#[macro_use]
mod common;
@@ -12,6 +13,15 @@ use common::{ensure_test_project, DaemonTestHarness};
const TEST_PROJECT: &str = "ci-test";
const TEST_PROGRAM: &str = "sample_binary";
static HARNESS: OnceLock<DaemonTestHarness> = OnceLock::new();
fn harness() -> &'static DaemonTestHarness {
HARNESS.get_or_init(|| {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon")
})
}
fn get_test_script_path() -> PathBuf {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("tests");
@@ -39,10 +49,7 @@ print("Test script executed")
#[serial]
fn test_script_list() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let _harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let _harness = harness();
// script list does not accept --project/--program arguments,
// so it may fail with "no project specified" unless a default is configured
@@ -67,12 +74,9 @@ fn test_script_list() {
#[serial]
fn test_script_run() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let script_path = create_test_script();
let _harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let _harness = harness();
let output = Command::cargo_bin("ghidra")
.unwrap()
@@ -105,10 +109,7 @@ fn test_script_run() {
#[serial]
fn test_script_python_inline() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let _harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let _harness = harness();
let output = Command::cargo_bin("ghidra")
.unwrap()
@@ -136,10 +137,7 @@ fn test_script_python_inline() {
#[serial]
fn test_script_run_nonexistent() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let _harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let _harness = harness();
Command::cargo_bin("ghidra")
.unwrap()
+16 -22
View File
@@ -3,6 +3,7 @@
use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
use std::sync::OnceLock;
#[macro_use]
mod common;
@@ -13,14 +14,20 @@ use common::{
const TEST_PROJECT: &str = "ci-test";
const TEST_PROGRAM: &str = "sample_binary";
static HARNESS: OnceLock<DaemonTestHarness> = OnceLock::new();
fn harness() -> &'static DaemonTestHarness {
HARNESS.get_or_init(|| {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon")
})
}
#[test]
#[serial]
fn test_symbol_list() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let harness = harness();
let output = Command::cargo_bin("ghidra")
.unwrap()
@@ -51,12 +58,9 @@ fn test_symbol_list() {
#[serial]
fn test_symbol_create_and_get() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = harness();
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
Command::cargo_bin("ghidra")
.unwrap()
@@ -83,20 +87,15 @@ fn test_symbol_create_and_get() {
.assert()
.success()
.stdout(predicate::str::contains("test_symbol"));
drop(harness);
}
#[test]
#[serial]
fn test_symbol_rename() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = harness();
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let addrs = get_function_addresses(&harness, TEST_PROJECT, TEST_PROGRAM, 2);
let addrs = get_function_addresses(harness, TEST_PROJECT, TEST_PROGRAM, 2);
let addr = &addrs[1];
// Use unique names to avoid collisions with cached project state
@@ -142,18 +141,13 @@ fn test_symbol_rename() {
.assert()
.success()
.stdout(predicate::str::contains(&*new_name));
drop(harness);
}
#[test]
#[serial]
fn test_symbol_get_nonexistent() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let harness = harness();
Command::cargo_bin("ghidra")
.unwrap()
+16 -21
View File
@@ -3,6 +3,7 @@
use assert_cmd::Command;
use predicates::prelude::*;
use serial_test::serial;
use std::sync::OnceLock;
#[macro_use]
mod common;
@@ -11,14 +12,20 @@ use common::{ensure_test_project, get_function_address, DaemonTestHarness};
const TEST_PROJECT: &str = "ci-test";
const TEST_PROGRAM: &str = "sample_binary";
static HARNESS: OnceLock<DaemonTestHarness> = OnceLock::new();
fn harness() -> &'static DaemonTestHarness {
HARNESS.get_or_init(|| {
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon")
})
}
#[test]
#[serial]
fn test_type_list() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let harness = harness();
Command::cargo_bin("ghidra")
.unwrap()
@@ -38,10 +45,7 @@ fn test_type_list() {
#[serial]
fn test_type_get_primitive() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let harness = harness();
Command::cargo_bin("ghidra")
.unwrap()
@@ -63,10 +67,7 @@ fn test_type_get_primitive() {
#[serial]
fn test_type_create() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let harness = harness();
Command::cargo_bin("ghidra")
.unwrap()
@@ -101,12 +102,9 @@ fn test_type_create() {
#[serial]
fn test_type_apply() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness = harness();
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let addr = get_function_address(&harness, TEST_PROJECT, TEST_PROGRAM, "main");
let addr = get_function_address(harness, TEST_PROJECT, TEST_PROGRAM, "main");
let output = Command::cargo_bin("ghidra")
.unwrap()
@@ -138,10 +136,7 @@ fn test_type_apply() {
#[serial]
fn test_type_get_nonexistent() {
require_ghidra!();
ensure_test_project(TEST_PROJECT, TEST_PROGRAM);
let harness =
DaemonTestHarness::new(TEST_PROJECT, TEST_PROGRAM).expect("Failed to start daemon");
let harness = harness();
Command::cargo_bin("ghidra")
.unwrap()