use std::path::{Path, PathBuf};
use std::process::{Command, Stdio, Child};
use std::time::Duration;
use std::thread;
use std::collections::HashMap;
use incode::lldb_manager::LldbManager;
use incode::error::{IncodeResult, IncodeError};
#[derive(Debug, Clone)]
pub enum TestMode {
Normal,
Threads,
Memory,
StepDebug,
CrashSegv,
CrashStack,
CrashAbort,
CrashDiv0,
Infinite,
}
impl TestMode {
pub fn as_arg(&self) -> &str {
match self {
TestMode::Normal => "normal",
TestMode::Threads => "threads",
TestMode::Memory => "memory",
TestMode::StepDebug => "step-debug",
TestMode::CrashSegv => "crash-segv",
TestMode::CrashStack => "crash-stack",
TestMode::CrashAbort => "crash-abort",
TestMode::CrashDiv0 => "crash-div0",
TestMode::Infinite => "infinite",
}
}
}
pub struct TestDebuggee {
binary_path: PathBuf,
process: Option<Child>,
pid: Option<u32>,
mode: TestMode,
}
impl TestDebuggee {
pub fn new(mode: TestMode) -> IncodeResult<Self> {
let binary_path = Self::find_test_binary()?;
Ok(TestDebuggee {
binary_path,
process: None,
pid: None,
mode,
})
}
pub fn binary_path(&self) -> &PathBuf {
&self.binary_path
}
pub fn mode(&self) -> &TestMode {
&self.mode
}
fn find_test_binary() -> IncodeResult<PathBuf> {
let possible_paths = [
"test_debuggee/test_debuggee",
"../test_debuggee/test_debuggee",
"./test_debuggee",
"../test_debuggee",
];
for path_str in &possible_paths {
let path = Path::new(path_str);
if path.exists() {
return Ok(path.to_path_buf());
}
}
println!("Test binary not found, attempting to build...");
Self::build_test_binary()?;
for path_str in &possible_paths {
let path = Path::new(path_str);
if path.exists() {
return Ok(path.to_path_buf());
}
}
Err(IncodeError::ProcessNotFound(
"Could not find or build test_debuggee binary".to_string()
))
}
fn build_test_binary() -> IncodeResult<()> {
let build_dirs = ["test_debuggee", "../test_debuggee"];
for build_dir in &build_dirs {
if Path::new(build_dir).exists() {
let output = Command::new("make")
.current_dir(build_dir)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output();
match output {
Ok(result) if result.status.success() => {
println!("Successfully built test_debuggee binary");
return Ok(());
}
Ok(result) => {
let stderr = String::from_utf8_lossy(&result.stderr);
return Err(IncodeError::ProcessError(
format!("Build failed: {}", stderr)
));
}
Err(e) => {
return Err(IncodeError::ProcessError(
format!("Failed to run make: {}", e)
));
}
}
}
}
Err(IncodeError::ProcessError(
"Could not find test_debuggee directory for building".to_string()
))
}
pub fn launch(&mut self) -> IncodeResult<u32> {
if self.process.is_some() {
return Err(IncodeError::LldbOperation(
"Process already running".to_string()
));
}
let mut command = Command::new(&self.binary_path);
command
.arg("--mode")
.arg(self.mode.as_arg())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if matches!(self.mode, TestMode::Infinite) {
command.stdin(Stdio::piped());
}
let mut child = command.spawn().map_err(|e| {
IncodeError::ProcessError(format!("Failed to launch test binary: {}", e))
})?;
let pid = child.id();
if matches!(self.mode, TestMode::CrashSegv | TestMode::CrashStack) {
thread::sleep(Duration::from_millis(100));
}
self.process = Some(child);
self.pid = Some(pid);
Ok(pid)
}
pub fn pid(&self) -> Option<u32> {
self.pid
}
pub fn is_running(&mut self) -> bool {
if let Some(ref mut process) = self.process {
match process.try_wait() {
Ok(Some(_)) => false, Ok(None) => true, Err(_) => false, }
} else {
false
}
}
pub fn wait_for_ready(&mut self, timeout: Duration) -> IncodeResult<()> {
let start_time = std::time::Instant::now();
while start_time.elapsed() < timeout {
if self.is_running() {
thread::sleep(Duration::from_millis(50));
return Ok(());
}
thread::sleep(Duration::from_millis(10));
}
if matches!(self.mode, TestMode::CrashSegv | TestMode::CrashStack) {
return Ok(()); }
Err(IncodeError::Timeout)
}
pub fn terminate(&mut self) -> IncodeResult<()> {
if let Some(mut process) = self.process.take() {
let _ = process.kill();
let _ = process.wait();
}
self.pid = None;
Ok(())
}
}
impl Drop for TestDebuggee {
fn drop(&mut self) {
let _ = self.terminate();
}
}
pub struct TestSession {
lldb_manager: LldbManager,
debuggee: TestDebuggee,
session_id: Option<uuid::Uuid>,
}
impl TestSession {
pub fn new(mode: TestMode) -> IncodeResult<Self> {
let lldb_manager = LldbManager::new(None)?;
let debuggee = TestDebuggee::new(mode)?;
Ok(TestSession {
lldb_manager,
debuggee,
session_id: None,
})
}
pub fn start(&mut self) -> IncodeResult<u32> {
let session_id = self.lldb_manager.create_session()?;
self.session_id = Some(session_id);
let args = vec![
"--mode".to_string(),
self.debuggee.mode.as_arg().to_string(),
];
let env = std::collections::HashMap::new();
match self.lldb_manager.launch_process(&self.debuggee.binary_path.to_string_lossy(), &args, &env) {
Ok(pid) => {
println!("Successfully launched target via LLDB: {}", pid);
Ok(pid)
}
Err(e) => {
println!("LLDB launch failed, falling back to attach method: {}", e);
let pid = self.debuggee.launch()?;
self.debuggee.wait_for_ready(Duration::from_secs(2))?;
if self.debuggee.is_running() {
match self.lldb_manager.attach_to_process(pid) {
Ok(_) => println!("Successfully attached LLDB to process {}", pid),
Err(e) => return Err(IncodeError::LldbOperation(
format!("Could not attach LLDB to process {}: {}", pid, e)
)),
}
}
Ok(pid)
}
}
}
pub fn start_crash_analysis(&mut self) -> IncodeResult<()> {
let session_id = self.lldb_manager.create_session()?;
self.session_id = Some(session_id);
let binary_path = self.debuggee.binary_path();
let args = vec![
"--mode".to_string(),
self.debuggee.mode().as_arg().to_string(),
];
let env = HashMap::new();
let _pid = self.lldb_manager.launch_process(
binary_path.to_str().unwrap(),
&args,
&env
)?;
thread::sleep(Duration::from_millis(500));
println!("Test binary launched for crash analysis");
Ok(())
}
pub fn lldb_manager(&mut self) -> &mut LldbManager {
&mut self.lldb_manager
}
pub fn pid(&self) -> Option<u32> {
self.debuggee.pid()
}
pub fn is_debuggee_running(&mut self) -> bool {
self.debuggee.is_running()
}
pub fn set_test_breakpoint(&mut self, location: &str) -> IncodeResult<()> {
match self.lldb_manager.set_breakpoint(location) {
Ok(bp_id) => {
println!("Set test breakpoint {} at {}", bp_id, location);
Ok(())
}
Err(e) => {
println!("Warning: Could not set breakpoint at {}: {}", location, e);
Ok(()) }
}
}
pub fn continue_execution(&mut self) -> IncodeResult<()> {
match self.lldb_manager.continue_execution() {
Ok(_) => {
println!("Continued execution");
Ok(())
}
Err(e) => {
println!("Continue execution result: {}", e);
Ok(()) }
}
}
pub fn interrupt_process(&mut self) -> IncodeResult<()> {
match self.lldb_manager.interrupt_execution() {
Ok(_) => {
println!("Process interrupted successfully");
Ok(())
}
Err(e) => {
println!("Interrupt process result: {}", e);
Ok(()) }
}
}
pub fn cleanup(&mut self) -> IncodeResult<()> {
if self.debuggee.is_running() {
let _ = self.lldb_manager.detach_process();
}
if let Some(session_id) = self.session_id {
let _ = self.lldb_manager.cleanup_session(&session_id);
}
self.debuggee.terminate()?;
Ok(())
}
}
impl Drop for TestSession {
fn drop(&mut self) {
let _ = self.cleanup();
}
}
pub struct TestUtils;
impl TestUtils {
pub fn wait_for_condition<F>(condition: F, timeout: Duration) -> bool
where
F: Fn() -> bool,
{
let start_time = std::time::Instant::now();
while start_time.elapsed() < timeout {
if condition() {
return true;
}
thread::sleep(Duration::from_millis(10));
}
false
}
pub fn get_test_breakpoint_locations() -> Vec<&'static str> {
vec![
"main",
"showcase_variables",
"run_threading_scenarios",
"run_memory_scenarios",
"step_debug_function",
"test_function_with_params",
]
}
pub fn verify_lldb_available() -> bool {
Command::new("lldb")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}
pub fn get_test_memory_locations() -> Vec<&'static str> {
vec![
"global_buffer",
"global_array",
"global_memory_struct",
"global_struct_ptr",
]
}
pub fn get_test_variable_names() -> Vec<&'static str> {
vec![
"global_int",
"global_float",
"global_string",
"local_int",
"local_float",
"param_int",
"param_string",
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_debuggee_creation() {
let debuggee_result = TestDebuggee::new(TestMode::StepDebug);
match debuggee_result {
Ok(_) => println!("✅ Test debuggee creation successful"),
Err(e) => println!("⚠️ Test debuggee creation failed: {}", e),
}
}
#[tokio::test]
async fn test_session_creation() {
let session_result = TestSession::new(TestMode::StepDebug);
match session_result {
Ok(mut session) => {
println!("✅ Test session creation successful");
let _ = session.cleanup();
}
Err(e) => println!("⚠️ Test session creation failed: {}", e),
}
}
#[tokio::test]
async fn test_lldb_availability() {
let lldb_available = TestUtils::verify_lldb_available();
if lldb_available {
println!("✅ LLDB is available on system");
} else {
println!("⚠️ LLDB is not available - some tests may fail");
}
}
}