#![allow(clippy::expect_used)] #![doc = "Controllable fake-child coverage for synchronous worker execution."] use std::{ ffi::OsString, fs, os::unix::fs::PermissionsExt as _, path::{Path, PathBuf}, sync::atomic::{AtomicU32, Ordering}, time::Duration, }; use ghidra_cli::{ cli::SandboxMode, domain::{InvocationId, Limits, PositiveU64}, operation::{ADAPTER_PROTOCOL_VERSION, Operation}, process::{ Cancellation, Invocation, InvocationFailureKind, NativePhaseTimeouts, NeverCancel, run, }, protocol::{AdapterRequest, InvocationFiles, RequestArguments}, sandbox::WorkerCommand, }; use tempfile::TempDir; fn positive(value: u64) -> PositiveU64 { PositiveU64::try_from(value).expect("positive") } fn request() -> AdapterRequest { AdapterRequest { protocol_version: ADAPTER_PROTOCOL_VERSION, invocation_id: InvocationId::parse("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").expect("identifier"), operation: Operation::Doctor, staged_sample: None, analysis_path: None, limits: Limits { max_heap_mib: positive(2048), max_cpu: positive(2), analysis_timeout_seconds: positive(600), decompile_timeout_seconds: None, child_watchdog_seconds: positive(120), max_sample_bytes: positive(1_073_741_824), max_inline_bytes: positive(65_536), }, arguments: RequestArguments::Doctor, } } fn fixture(behavior: &str, extra: &[&str]) -> (TempDir, InvocationFiles, WorkerCommand) { let root = tempfile::tempdir().expect("temporary directory"); let invocation_directory = root.path().join("invocation"); fs::create_dir(&invocation_directory).expect("invocation directory"); let files = InvocationFiles::new(invocation_directory).expect("protocol paths"); let mut arguments = vec![ OsString::from(behavior), files.request_path().into_os_string(), files.response_path().into_os_string(), ]; arguments.extend(extra.iter().map(OsString::from)); let command = WorkerCommand::for_policy( SandboxMode::Off, PathBuf::from(env!("CARGO_BIN_EXE_ghidr-fake-child")), arguments, None, ) .expect("worker command"); (root, files, command) } fn invocation<'a>( request: &'a AdapterRequest, files: &'a InvocationFiles, command: &'a WorkerCommand, ) -> Invocation<'a> { Invocation::new( request, files, command, NativePhaseTimeouts { analysis_seconds: None, decompile_seconds: None, }, ) .expect("derived Invocation") } #[test] fn one_attempt_writes_private_request_and_accepts_only_strict_final_response() { let (_root, files, command) = fixture("success", &[]); let request = request(); let completed = run(invocation(&request, &files, &command), &NeverCancel).expect("completed invocation"); assert_eq!(completed.response.operation, Operation::Doctor); assert_eq!( fs::metadata(files.request_path()) .expect("request metadata") .permissions() .mode() & 0o777, 0o600, ); } #[test] fn temporary_response_is_never_accepted() { let (_root, files, command) = fixture("temporary-only", &[]); let request = request(); let failure = run(invocation(&request, &files, &command), &NeverCancel) .expect_err("temporary response must fail"); assert_eq!(failure.kind, InvocationFailureKind::ResponseProtocol); } #[test] fn mismatched_echo_is_a_protocol_violation() { let (_root, files, command) = fixture("mismatch", &[]); let request = request(); let failure = run(invocation(&request, &files, &command), &NeverCancel).expect_err("mismatch must fail"); assert_eq!(failure.kind, InvocationFailureKind::ResponseProtocol); } #[test] fn both_full_pipes_are_drained_without_deadlock_and_capture_is_bounded() { let count = (9 * 1024 * 1024).to_string(); let (_root, files, command) = fixture("flood", &[&count]); let request = request(); let completed = run(invocation(&request, &files, &command), &NeverCancel).expect("flood invocation"); assert!(completed.diagnostics.truncated()); } #[test] fn watchdog_terminates_and_reaps_the_process_group() { let (_root, files, command) = fixture("sleep", &["5000"]); let request = request(); let failure = run( invocation(&request, &files, &command).with_test_watchdog(Duration::from_millis(30)), &NeverCancel, ) .expect_err("watchdog must fire"); assert_eq!(failure.kind, InvocationFailureKind::WatchdogTimeout); } struct Interrupts(AtomicU32); impl Cancellation for Interrupts { fn interrupt_count(&self) -> u32 { self.0.load(Ordering::SeqCst) } } #[test] fn second_interrupt_escalates_without_waiting_for_grace() { let (_root, files, command) = fixture("sleep", &["5000"]); let request = request(); let interrupts = Interrupts(AtomicU32::new(2)); let before = std::time::Instant::now(); let failure = run(invocation(&request, &files, &command), &interrupts).expect_err("interrupt must win"); assert_eq!(failure.kind, InvocationFailureKind::Interrupted); assert!(before.elapsed() < Duration::from_secs(2)); } #[test] fn protocol_paths_are_absolute_in_fixture() { let (_root, files, _command) = fixture("success", &[]); assert!(Path::new(&files.request_path()).is_absolute()); } #[test] fn invocation_rejects_a_competing_total_watchdog_value() { let (_root, files, command) = fixture("success", &[]); let mut request = request(); request.limits.child_watchdog_seconds = positive(121); assert!( Invocation::new( &request, &files, &command, NativePhaseTimeouts { analysis_seconds: None, decompile_seconds: None, }, ) .is_err() ); }