From 4646d14ce8b78e8b49e3d21b09add8cf1c833416 Mon Sep 17 00:00:00 2001 From: fei Date: Sun, 25 Jan 2026 11:15:26 +0800 Subject: [PATCH 1/7] feat: add unit tests and enhance CI - Add 36 unit tests covering Process, ProcessGroup, and Session modules - Process: 20 tests for PID management, parent-child relationships, process groups, sessions, thread management, and exit handling - ProcessGroup: 5 tests for PGID, session association, and process collection - Session: 9 tests for SID, process group collection, and terminal management - Enhance CI workflow: - Add code format checking (cargo fmt --check) - Separate unit tests and integration tests runs - Ensure all tests run correctly - Fix test initialization race conditions using atomic flags - Fix no_std compatibility issues in tests (format!, vec!, ToString) --- .github/workflows/ci.yml | 10 +- src/process.rs | 269 +++++++++++++++++++++++++++++++++++++++ src/process_group.rs | 89 +++++++++++++ src/session.rs | 157 +++++++++++++++++++++++ tests/process.rs | 2 +- tests/session.rs | 4 +- 6 files changed, 526 insertions(+), 5 deletions(-) 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..c6f740f 100644 --- a/src/process.rs +++ b/src/process.rs @@ -306,3 +306,272 @@ 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 super::*; + use alloc::vec; + + 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..5ac3d64 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 super::*; + use crate::Process; + use crate::process::is_init_initialized; + use alloc::{format, string::ToString}; + + 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..6eef9be 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 super::*; + use crate::Process; + use crate::process::is_init_initialized; + use alloc::{format, string::ToString}; + + 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.len() >= 1); + } + + #[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..1781a54 100644 --- a/tests/session.rs +++ b/tests/session.rs @@ -1,5 +1,5 @@ -use std::sync::Arc; use std::any::Any; +use std::sync::Arc; use starry_process::init_proc; @@ -129,4 +129,4 @@ fn terminal_set_unset() { assert!(session.unset_terminal(&term)); assert!(session.terminal().is_none()); -} \ No newline at end of file +} From 42bd25f71a41a9445a05d1c5ff3f09a1e872408e Mon Sep 17 00:00:00 2001 From: fei Date: Sun, 25 Jan 2026 11:19:20 +0800 Subject: [PATCH 2/7] chore: trigger CI From 118b36f90b33e9cb40766ff9a31d3d431a771e3c Mon Sep 17 00:00:00 2001 From: fei Date: Sun, 25 Jan 2026 11:21:40 +0800 Subject: [PATCH 3/7] fix: replace len() >= 1 with is_empty() to fix clippy warning --- src/session.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/session.rs b/src/session.rs index 6eef9be..a27b4bf 100644 --- a/src/session.rs +++ b/src/session.rs @@ -118,7 +118,7 @@ mod tests { let groups = session.process_groups(); assert!(groups.iter().any(|g| g.pgid() == init.pid())); - assert!(groups.len() >= 1); + assert!(!groups.is_empty()); } #[test] From d73c896681dbce456fda3afb12c80c2fa20a6f3f Mon Sep 17 00:00:00 2001 From: fei Date: Sun, 25 Jan 2026 11:24:02 +0800 Subject: [PATCH 4/7] fix: adjust import order to match rustfmt requirements --- src/process.rs | 3 ++- src/process_group.rs | 6 +++--- src/session.rs | 6 +++--- tests/session.rs | 3 +-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/process.rs b/src/process.rs index c6f740f..6894da5 100644 --- a/src/process.rs +++ b/src/process.rs @@ -314,9 +314,10 @@ pub(crate) fn is_init_initialized() -> bool { #[cfg(test)] mod tests { - use super::*; use alloc::vec; + use super::*; + fn ensure_init() { // Try to get init proc, if it fails, initialize it if INIT_PROC.get().is_none() { diff --git a/src/process_group.rs b/src/process_group.rs index 5ac3d64..38856cf 100644 --- a/src/process_group.rs +++ b/src/process_group.rs @@ -59,11 +59,11 @@ impl fmt::Debug for ProcessGroup { #[cfg(test)] mod tests { - use super::*; - use crate::Process; - use crate::process::is_init_initialized; 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() { diff --git a/src/session.rs b/src/session.rs index a27b4bf..6f1658c 100644 --- a/src/session.rs +++ b/src/session.rs @@ -73,11 +73,11 @@ impl fmt::Debug for Session { #[cfg(test)] mod tests { - use super::*; - use crate::Process; - use crate::process::is_init_initialized; 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() { diff --git a/tests/session.rs b/tests/session.rs index 1781a54..73d1fa6 100644 --- a/tests/session.rs +++ b/tests/session.rs @@ -1,5 +1,4 @@ -use std::any::Any; -use std::sync::Arc; +use std::{any::Any, sync::Arc}; use starry_process::init_proc; From 204f8c30f9e3836f468f116be6294cc15581004a Mon Sep 17 00:00:00 2001 From: fei Date: Sun, 25 Jan 2026 11:26:22 +0800 Subject: [PATCH 5/7] chore: trigger CI From e671e524dc000ec3a7f97ff49f90b80f09a0d976 Mon Sep 17 00:00:00 2001 From: fei Date: Sun, 25 Jan 2026 11:27:45 +0800 Subject: [PATCH 6/7] ci: remove deploy job to avoid GitHub Pages 404 errors --- .github/workflows/ci.yml | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cab356..bef3371 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,22 +46,3 @@ jobs: run: | cargo doc --all-features --no-deps printf '' $(cargo tree | head -1 | cut -d' ' -f1 | tr '-' '_') > target/doc/index.html - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: target/doc - - deploy: - runs-on: ubuntu-latest - needs: doc - permissions: - contents: read - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 From 3cfb960ed13c992f70e3ee92aa8178c04a4b8d0b Mon Sep 17 00:00:00 2001 From: fei Date: Sun, 25 Jan 2026 11:29:56 +0800 Subject: [PATCH 7/7] ci: restore deploy job for GitHub Pages --- .github/workflows/ci.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bef3371..7cab356 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,3 +46,22 @@ jobs: run: | cargo doc --all-features --no-deps printf '' $(cargo tree | head -1 | cut -d' ' -f1 | tr '-' '_') > target/doc/index.html + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: target/doc + + deploy: + runs-on: ubuntu-latest + needs: doc + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4