swactor/crates/process/tests/proptest_session.rs
Zachery Aaron Shores-Chmielewski e15678bddb feat: native process manager
Enable swactor to spawn and manage native processes using ssh.
2026-02-23 11:44:15 +07:00

195 lines
7.2 KiB
Rust

use std::collections::HashMap;
use proptest::prelude::*;
use swactor::actor::ActorAddress;
use swactor_process::*;
fn automated_spec() -> ProcessSpec {
ProcessSpec {
command: "test".into(),
args: vec![],
env: HashMap::new(),
working_dir: None,
mode: ProcessMode::Automated,
initial_pty_size: None,
kill_timeout: None,
stdin_buffer_limit: None,
}
}
fn addr(n: u8) -> ActorAddress {
let mut bytes = [0u8; 32];
bytes[0] = n;
ActorAddress(bytes)
}
fn arb_signal() -> impl Strategy<Value = Signal> {
prop_oneof![
Just(Signal::Terminate),
Just(Signal::Kill),
Just(Signal::Hangup),
Just(Signal::Interrupt),
(0..32i32).prop_map(Signal::Other),
]
}
fn arb_event() -> impl Strategy<Value = ProcessEvent> {
prop_oneof![
Just(ProcessEvent::Started),
Just(ProcessEvent::KillTimeout),
".*".prop_map(|reason| ProcessEvent::SpawnFailed { reason }),
proptest::collection::vec(any::<u8>(), 0..64)
.prop_map(|data| ProcessEvent::OutputReceived { data, is_stderr: false }),
proptest::collection::vec(any::<u8>(), 0..64)
.prop_map(|data| ProcessEvent::OutputReceived { data, is_stderr: true }),
prop_oneof![
any::<i32>().prop_map(ExitStatus::Code),
any::<i32>().prop_map(ExitStatus::Signal),
Just(ExitStatus::Unknown),
]
.prop_map(|status| ProcessEvent::Exited { status }),
".*".prop_map(|reason| ProcessEvent::ConnectionLost { reason }),
(0..5usize).prop_map(|n| ProcessEvent::StdinWritten { byte_count: n * 10 }),
Just(ProcessEvent::SignalSent),
Just(ProcessEvent::PtyResized),
proptest::collection::vec(any::<u8>(), 0..64)
.prop_map(|data| ProcessEvent::WriteStdin { data }),
arb_signal().prop_map(|signal| ProcessEvent::SendSignal { signal }),
Just(ProcessEvent::ResizePty {
size: PtySize { cols: 80, rows: 24 },
}),
Just(ProcessEvent::CloseStdin),
Just(ProcessEvent::CloseRequested),
(0..4u8).prop_map(|n| ProcessEvent::Subscribe { address: addr(n) }),
(0..4u8).prop_map(|n| ProcessEvent::Unsubscribe { address: addr(n) }),
]
}
// ──────────────────────────────────────────────
// 1. No panics for arbitrary event sequences
// ──────────────────────────────────────────────
proptest! {
#[test]
fn no_panics_on_arbitrary_events(events in proptest::collection::vec(arb_event(), 0..50)) {
let (mut session, _) = ProcessSession::new(automated_spec());
for event in events {
let _ = session.apply(event);
}
}
}
// ──────────────────────────────────────────────
// 2. Exited is terminal
// ──────────────────────────────────────────────
proptest! {
#[test]
fn exited_is_terminal(events in proptest::collection::vec(arb_event(), 0..50)) {
let (mut session, _) = ProcessSession::new(automated_spec());
let mut reached_exited = false;
for event in events {
let _ = session.apply(event);
if session.state() == ProcessState::Exited {
reached_exited = true;
}
if reached_exited {
prop_assert_eq!(session.state(), ProcessState::Exited);
}
}
}
}
// ──────────────────────────────────────────────
// 3. SelfTerminate always last action when entering Exited
// ──────────────────────────────────────────────
proptest! {
#[test]
fn self_terminate_is_last_when_entering_exited(events in proptest::collection::vec(arb_event(), 0..50)) {
let (mut session, _) = ProcessSession::new(automated_spec());
let mut was_exited = false;
for event in events {
let prev_state = session.state();
let actions = session.apply(event);
// If we just transitioned into Exited
if session.state() == ProcessState::Exited && !was_exited && prev_state != ProcessState::Exited {
prop_assert!(
matches!(actions.last(), Some(ProcessAction::SelfTerminate)),
"SelfTerminate must be last action when entering Exited, got: {:?}", actions
);
}
if session.state() == ProcessState::Exited {
was_exited = true;
}
}
}
}
// ──────────────────────────────────────────────
// 4. Subscriber count matches add/remove operations
// ──────────────────────────────────────────────
proptest! {
#[test]
fn subscriber_count_is_consistent(
ops in proptest::collection::vec(
prop_oneof![
(0..8u8).prop_map(|n| (true, n)),
(0..8u8).prop_map(|n| (false, n)),
],
0..30
)
) {
let (mut session, _) = ProcessSession::new(automated_spec());
let mut expected: Vec<u8> = Vec::new();
for (is_add, n) in ops {
if is_add {
session.apply(ProcessEvent::Subscribe { address: addr(n) });
if !expected.contains(&n) {
expected.push(n);
}
} else {
session.apply(ProcessEvent::Unsubscribe { address: addr(n) });
expected.retain(|&x| x != n);
}
prop_assert_eq!(session.subscriber_count(), expected.len());
}
}
}
// ──────────────────────────────────────────────
// 5. State monotonicity (never goes backward)
// ──────────────────────────────────────────────
fn state_ordinal(s: ProcessState) -> u8 {
match s {
ProcessState::Starting => 0,
ProcessState::Running => 1,
ProcessState::Stopping => 2,
ProcessState::Exited => 3,
}
}
proptest! {
#[test]
fn state_never_goes_backward(events in proptest::collection::vec(arb_event(), 0..50)) {
let (mut session, _) = ProcessSession::new(automated_spec());
let mut max_ordinal = state_ordinal(session.state());
for event in events {
let _ = session.apply(event);
let current = state_ordinal(session.state());
prop_assert!(
current >= max_ordinal,
"State went backward: ordinal {} -> {}", max_ordinal, current
);
max_ordinal = current;
}
}
}