//! Tests for the enforcement ladder. //! //! `expect`/`unwrap`/`panic` are allowed here, scoped to this module with the //! reason stated: a failed assertion or a missing `sleep` binary is a broken //! test, not a runtime condition to handle gracefully. #![allow( clippy::unwrap_used, clippy::expect_used, clippy::panic, reason = "test fixtures: a failed assertion is a broken test, not a runtime condition" )] use super::*; /// A child process can be frozen, reported frozen, and thawed again. /// /// Deliberately *not* done to the test process itself. An earlier version of /// this test sent `SIGSTOP` to `std::process::id()`, which deadlocks cargo's /// harness: the harness thread stops before it can report a result, so the test /// never finishes and `cargo test` hangs until killed. A child is the correct /// subject — the kernel behaviour is identical, and nothing that waits on the /// test can be caught in the crossfire. #[test] fn a_child_process_freezes_reports_and_thaws() { let mut child = std::process::Command::new("sleep").arg("30").spawn().expect("spawn sleep"); let pid = child.id(); assert!(!is_frozen(pid).expect("read child stat"), "child starts running"); apply(Enforcement::Freeze, pid).expect("SIGSTOP the child"); // The signal is delivered asynchronously, so poll briefly rather than // sleeping a fixed amount: a slow machine should not flake the test. let mut frozen = false; for _ in 0..50 { if is_frozen(pid).unwrap_or(false) { frozen = true; break; } std::thread::sleep(std::time::Duration::from_millis(20)); } assert!(frozen, "the child should report stopped after SIGSTOP"); thaw(pid).expect("SIGCONT the child"); let mut running = false; for _ in 0..50 { if !is_frozen(pid).unwrap_or(false) { running = true; break; } std::thread::sleep(std::time::Duration::from_millis(20)); } assert!(running, "the child should be running again after SIGCONT"); let _ = child.kill(); let _ = child.wait(); } /// Signalling a pid we cannot signal is an error, never a panic. /// /// CLAUDE.md section 0 rule 5: a fallible OS call must not abort the daemon. /// A daemon that panics takes enforcement down with it. #[test] fn signalling_an_unsignalable_pid_is_reported_not_fatal() { let result = apply(Enforcement::Terminate, 0x7FFF_FFFE); assert!(result.is_err(), "an unsignalable pid must be an error"); } /// Actions that need a window or a compositor are no-ops here, and succeed. #[test] fn compositor_actions_are_no_ops_at_this_layer() { assert!(apply(Enforcement::BlockScreen, 0).is_ok()); assert!(apply(Enforcement::Minimise, 0).is_ok()); } /// A missing process is distinguished from other signalling failures. #[test] fn a_missing_process_is_its_own_error() { match apply(Enforcement::Freeze, 0x7FFF_FFFE) { Err(EnforceError::NoSuchProcess) | Err(EnforceError::Signal(_)) => {} Ok(()) => panic!("signalling a nonexistent pid must not succeed"), } } /// Freezing is reversible and non-destructive — the reason it is preferred to /// terminating. #[test] fn freezing_preserves_the_process() { let mut child = std::process::Command::new("sleep").arg("30").spawn().expect("spawn sleep"); let pid = child.id(); apply(Enforcement::Freeze, pid).expect("SIGSTOP"); for _ in 0..50 { if is_frozen(pid).unwrap_or(false) { break; } std::thread::sleep(std::time::Duration::from_millis(20)); } thaw(pid).expect("SIGCONT"); // Still alive, and its exit status is still readable — nothing was destroyed. assert!(!is_frozen(pid).unwrap_or(true), "the child must be running again after SIGCONT"); assert_eq!( child.try_wait().expect("try_wait").map(|s| s.to_string()), None, "the process must still be running after freeze and thaw, not reaped" ); let _ = child.kill(); let _ = child.wait(); }