diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e08aa31..7cab356 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,10 +23,16 @@ jobs: - name: Setup Rust toolchain run: | rustup default nightly - rustup component add clippy + rustup component add clippy rustfmt + - name: Check formatting + run: cargo fmt --all -- --check - name: Clippy run: cargo clippy --all-features --all-targets -- -Dwarnings - - name: Test + - name: Unit tests + run: cargo test --lib --all-features + - name: Integration tests + run: cargo test --test '*' --all-features + - name: All tests run: cargo test --all-features doc: diff --git a/src/process.rs b/src/process.rs index 3ccff29..6894da5 100644 --- a/src/process.rs +++ b/src/process.rs @@ -306,3 +306,273 @@ static INIT_PROC: LazyInit> = LazyInit::new(); pub fn init_proc() -> Arc { INIT_PROC.get().unwrap().clone() } + +#[cfg(test)] +pub(crate) fn is_init_initialized() -> bool { + INIT_PROC.get().is_some() +} + +#[cfg(test)] +mod tests { + use alloc::vec; + + use super::*; + + fn ensure_init() { + // Try to get init proc, if it fails, initialize it + if INIT_PROC.get().is_none() { + // Use a static flag to prevent multiple initializations + static INIT_FLAG: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + if !INIT_FLAG.swap(true, core::sync::atomic::Ordering::SeqCst) { + Process::new_init(alloc_pid()); + } else { + // Another thread/test already initialized, wait a bit and check again + // In single-threaded tests, this shouldn't happen, but be safe + while INIT_PROC.get().is_none() { + core::hint::spin_loop(); + } + } + } + } + + fn alloc_pid() -> Pid { + static COUNTER: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(1000); + COUNTER.fetch_add(1, core::sync::atomic::Ordering::SeqCst) + } + + #[test] + fn test_pid() { + ensure_init(); + let init = init_proc(); + assert_eq!(init.pid(), init.pid()); + } + + #[test] + fn test_is_init() { + ensure_init(); + let init = init_proc(); + assert!(init.is_init()); + + let child = init.fork(alloc_pid()); + assert!(!child.is_init()); + } + + #[test] + fn test_parent_child_relationship() { + ensure_init(); + let parent = init_proc(); + let child = parent.fork(alloc_pid()); + + assert_eq!(child.parent().unwrap().pid(), parent.pid()); + assert!(parent.children().iter().any(|c| c.pid() == child.pid())); + } + + #[test] + fn test_multiple_children() { + ensure_init(); + let parent = init_proc(); + let child1 = parent.fork(alloc_pid()); + let child2 = parent.fork(alloc_pid()); + + let children = parent.children(); + assert!(children.iter().any(|c| c.pid() == child1.pid())); + assert!(children.iter().any(|c| c.pid() == child2.pid())); + assert!(children.len() >= 2); + } + + #[test] + fn test_group_inheritance() { + ensure_init(); + let parent = init_proc(); + let child = parent.fork(alloc_pid()); + + assert_eq!(child.group().pgid(), parent.group().pgid()); + } + + #[test] + fn test_create_group() { + ensure_init(); + let parent = init_proc(); + let child = parent.fork(alloc_pid()); + + let new_group = child.create_group().unwrap(); + assert_eq!(new_group.pgid(), child.pid()); + assert_eq!(child.group().pgid(), child.pid()); + } + + #[test] + fn test_create_group_already_leader() { + ensure_init(); + let process = init_proc(); + + // Init process is already a group leader + assert!(process.create_group().is_none()); + } + + #[test] + fn test_create_session() { + ensure_init(); + let parent = init_proc(); + let child = parent.fork(alloc_pid()); + + let (session, group) = child.create_session().unwrap(); + assert_eq!(session.sid(), child.pid()); + assert_eq!(group.pgid(), child.pid()); + } + + #[test] + fn test_create_session_already_leader() { + ensure_init(); + let process = init_proc(); + + // Init process is already a session leader + assert!(process.create_session().is_none()); + } + + #[test] + fn test_move_to_group() { + ensure_init(); + let parent = init_proc(); + let child1 = parent.fork(alloc_pid()); + let child2 = parent.fork(alloc_pid()); + + let group1 = child1.create_group().unwrap(); + assert!(child2.move_to_group(&group1)); + assert_eq!(child2.group().pgid(), group1.pgid()); + } + + #[test] + fn test_move_to_same_group() { + ensure_init(); + let parent = init_proc(); + let child = parent.fork(alloc_pid()); + let group = child.group(); + + assert!(child.move_to_group(&group)); + } + + #[test] + fn test_move_to_different_session() { + ensure_init(); + let parent = init_proc(); + let child1 = parent.fork(alloc_pid()); + let child2 = parent.fork(alloc_pid()); + + let (_, group1) = child1.create_session().unwrap(); + assert!(!child2.move_to_group(&group1)); + } + + #[test] + fn test_thread_management() { + ensure_init(); + let process = init_proc(); + + process.add_thread(1); + process.add_thread(2); + process.add_thread(3); + + let mut threads = process.threads(); + threads.sort(); + assert_eq!(threads, vec![1, 2, 3]); + } + + #[test] + fn test_exit_thread() { + ensure_init(); + let child = init_proc().fork(alloc_pid()); + + child.add_thread(1); + child.add_thread(2); + + let last = child.exit_thread(1, 42); + assert!(!last); + assert_eq!(child.exit_code(), 42); + assert!(child.threads().contains(&2)); + assert!(!child.threads().contains(&1)); + + // Without group_exit, exit code will be updated + let last = child.exit_thread(2, 99); + assert!(last); + assert_eq!(child.exit_code(), 99); + assert!(child.threads().is_empty()); + } + + #[test] + fn test_group_exit() { + ensure_init(); + let child = init_proc().fork(alloc_pid()); + + child.add_thread(1); + child.group_exit(); + assert!(child.is_group_exited()); + + // Exit code should not change after group exit + let exit_code_before = child.exit_code(); + child.exit_thread(1, 99); + assert_eq!(child.exit_code(), exit_code_before); + } + + #[test] + fn test_is_zombie() { + ensure_init(); + let child = init_proc().fork(alloc_pid()); + + assert!(!child.is_zombie()); + } + + #[test] + fn test_exit() { + ensure_init(); + let parent = init_proc(); + let child = parent.fork(alloc_pid()); + + child.exit(); + assert!(child.is_zombie()); + assert!(parent.children().iter().any(|c| c.pid() == child.pid())); + } + + #[test] + fn test_exit_init_process() { + ensure_init(); + let init = init_proc(); + + // Exit init process should not panic, but should do nothing + init.exit(); + assert!(!init.is_zombie()); + } + + #[test] + fn test_exit_child_reaping() { + ensure_init(); + let init = init_proc(); + let parent = init.fork(alloc_pid()); + let child = parent.fork(alloc_pid()); + + parent.exit(); + assert_eq!(child.parent().unwrap().pid(), init.pid()); + } + + #[test] + fn test_free_zombie() { + ensure_init(); + let parent = init_proc(); + let child = parent.fork(alloc_pid()); + let child_pid = child.pid(); + + child.exit(); + assert!(child.is_zombie()); + assert!(parent.children().iter().any(|c| c.pid() == child_pid)); + + child.free(); + assert!(!parent.children().iter().any(|c| c.pid() == child_pid)); + } + + #[test] + #[should_panic(expected = "only zombie process can be freed")] + fn test_free_not_zombie() { + ensure_init(); + let process = init_proc().fork(alloc_pid()); + process.free(); + } +} diff --git a/src/process_group.rs b/src/process_group.rs index 8cdad68..38856cf 100644 --- a/src/process_group.rs +++ b/src/process_group.rs @@ -56,3 +56,92 @@ impl fmt::Debug for ProcessGroup { ) } } + +#[cfg(test)] +mod tests { + use alloc::{format, string::ToString}; + + use super::*; + use crate::{Process, process::is_init_initialized}; + + fn ensure_init() { + // Try to get init proc, if it fails, initialize it + if !is_init_initialized() { + // Use a static flag to prevent multiple initializations + static INIT_FLAG: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + if !INIT_FLAG.swap(true, core::sync::atomic::Ordering::SeqCst) { + Process::new_init(alloc_pid()); + } else { + // Another thread/test already initialized, wait a bit and check again + // In single-threaded tests, this shouldn't happen, but be safe + while !is_init_initialized() { + core::hint::spin_loop(); + } + } + } + } + + fn alloc_pid() -> Pid { + static COUNTER: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(2000); + COUNTER.fetch_add(1, core::sync::atomic::Ordering::SeqCst) + } + + #[test] + fn test_pgid() { + ensure_init(); + let pid = alloc_pid(); + let session = Session::new(pid); + let group = ProcessGroup::new(pid, &session); + + assert_eq!(group.pgid(), pid); + } + + #[test] + fn test_session() { + ensure_init(); + let pid = alloc_pid(); + let session = Session::new(pid); + let group = ProcessGroup::new(pid, &session); + + assert_eq!(group.session().sid(), session.sid()); + } + + #[test] + fn test_processes() { + ensure_init(); + let init = crate::init_proc(); + let group = init.group(); + + let processes_before = group.processes().len(); + let child = init.fork(alloc_pid()); + + let processes = group.processes(); + assert!(processes.iter().any(|p| p.pid() == init.pid())); + assert!(processes.iter().any(|p| p.pid() == child.pid())); + assert_eq!(processes.len(), processes_before + 1); + } + + #[test] + fn test_processes_empty_group() { + ensure_init(); + let pid = alloc_pid(); + let session = Session::new(pid); + let group = ProcessGroup::new(alloc_pid(), &session); + + let processes = group.processes(); + assert!(processes.is_empty()); + } + + #[test] + fn test_debug() { + ensure_init(); + let pid = alloc_pid(); + let session = Session::new(pid); + let group = ProcessGroup::new(pid, &session); + + let debug_str = format!("{:?}", group); + assert!(debug_str.contains("ProcessGroup")); + assert!(debug_str.contains(&pid.to_string())); + } +} diff --git a/src/session.rs b/src/session.rs index 116ad98..6f1658c 100644 --- a/src/session.rs +++ b/src/session.rs @@ -70,3 +70,160 @@ impl fmt::Debug for Session { write!(f, "Session({})", self.sid) } } + +#[cfg(test)] +mod tests { + use alloc::{format, string::ToString}; + + use super::*; + use crate::{Process, process::is_init_initialized}; + + fn ensure_init() { + // Try to get init proc, if it fails, initialize it + if !is_init_initialized() { + // Use a static flag to prevent multiple initializations + static INIT_FLAG: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + if !INIT_FLAG.swap(true, core::sync::atomic::Ordering::SeqCst) { + Process::new_init(alloc_pid()); + } else { + // Another thread/test already initialized, wait a bit and check again + // In single-threaded tests, this shouldn't happen, but be safe + while !is_init_initialized() { + core::hint::spin_loop(); + } + } + } + } + + fn alloc_pid() -> Pid { + static COUNTER: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(3000); + COUNTER.fetch_add(1, core::sync::atomic::Ordering::SeqCst) + } + + #[test] + fn test_sid() { + ensure_init(); + let pid = alloc_pid(); + let session = Session::new(pid); + + assert_eq!(session.sid(), pid); + } + + #[test] + fn test_process_groups() { + ensure_init(); + let init = crate::init_proc(); + let session = init.group().session(); + + let groups = session.process_groups(); + assert!(groups.iter().any(|g| g.pgid() == init.pid())); + assert!(!groups.is_empty()); + } + + #[test] + fn test_process_groups_multiple() { + ensure_init(); + let init = crate::init_proc(); + let session = init.group().session(); + + let groups_before = session.process_groups().len(); + let child1 = init.fork(alloc_pid()); + let child2 = init.fork(alloc_pid()); + let group1 = child1.create_group().unwrap(); + let group2 = child2.create_group().unwrap(); + + let groups = session.process_groups(); + assert!(groups.iter().any(|g| g.pgid() == group1.pgid())); + assert!(groups.iter().any(|g| g.pgid() == group2.pgid())); + assert_eq!(groups.len(), groups_before + 2); + } + + #[test] + fn test_set_terminal() { + ensure_init(); + let pid = alloc_pid(); + let session = Session::new(pid); + + let terminal: Arc = Arc::new(42u32); + assert!(session.set_terminal_with(|| terminal.clone())); + + let retrieved = session.terminal().unwrap(); + assert!(Arc::ptr_eq(&retrieved, &terminal)); + } + + #[test] + fn test_set_terminal_twice() { + ensure_init(); + let pid = alloc_pid(); + let session = Session::new(pid); + + let terminal1: Arc = Arc::new(42u32); + let terminal2: Arc = Arc::new(43u32); + + assert!(session.set_terminal_with(|| terminal1.clone())); + assert!(!session.set_terminal_with(|| terminal2.clone())); + + let retrieved = session.terminal().unwrap(); + assert!(Arc::ptr_eq(&retrieved, &terminal1)); + } + + #[test] + fn test_unset_terminal() { + ensure_init(); + let pid = alloc_pid(); + let session = Session::new(pid); + + let terminal: Arc = Arc::new(42u32); + session.set_terminal_with(|| terminal.clone()); + + assert!(session.unset_terminal(&terminal)); + assert!(session.terminal().is_none()); + } + + #[test] + fn test_unset_terminal_wrong() { + ensure_init(); + let pid = alloc_pid(); + let session = Session::new(pid); + + let terminal1: Arc = Arc::new(42u32); + let terminal2: Arc = Arc::new(43u32); + + session.set_terminal_with(|| terminal1.clone()); + assert!(!session.unset_terminal(&terminal2)); + + let retrieved = session.terminal().unwrap(); + assert!(Arc::ptr_eq(&retrieved, &terminal1)); + } + + #[test] + fn test_unset_terminal_none() { + ensure_init(); + let pid = alloc_pid(); + let session = Session::new(pid); + + let terminal: Arc = Arc::new(42u32); + assert!(!session.unset_terminal(&terminal)); + } + + #[test] + fn test_terminal_none() { + ensure_init(); + let pid = alloc_pid(); + let session = Session::new(pid); + + assert!(session.terminal().is_none()); + } + + #[test] + fn test_debug() { + ensure_init(); + let pid = alloc_pid(); + let session = Session::new(pid); + + let debug_str = format!("{:?}", session); + assert!(debug_str.contains("Session")); + assert!(debug_str.contains(&pid.to_string())); + } +} diff --git a/tests/process.rs b/tests/process.rs index b91eb0d..c60af2d 100644 --- a/tests/process.rs +++ b/tests/process.rs @@ -70,4 +70,4 @@ fn thread_exit() { let last2 = child.exit_thread(102, 3); assert!(last2); assert_eq!(child.exit_code(), 7); -} \ No newline at end of file +} diff --git a/tests/session.rs b/tests/session.rs index 71269ee..73d1fa6 100644 --- a/tests/session.rs +++ b/tests/session.rs @@ -1,5 +1,4 @@ -use std::sync::Arc; -use std::any::Any; +use std::{any::Any, sync::Arc}; use starry_process::init_proc; @@ -129,4 +128,4 @@ fn terminal_set_unset() { assert!(session.unset_terminal(&term)); assert!(session.terminal().is_none()); -} \ No newline at end of file +}