swactor/crates/process/tests/e2e_process.rs

190 lines
5.7 KiB
Rust
Raw Normal View History

//! End-to-end tests: full Runtime + ExternalSender + ProcessActor<LocalDriver>.
//!
//! Spawns real OS processes through the actor system and verifies the
//! complete notification flow.
use std::collections::HashMap;
use swactor::actor::{ActorAddress, ActorInterface, Ctx};
use swactor::runtime::{ExternalSender, Inbox, Runtime, RuntimeConfig};
use swactor_process::*;
fn automated_spec(cmd: &str, args: &[&str]) -> ProcessSpec {
ProcessSpec {
command: cmd.into(),
args: args.iter().map(|s| s.to_string()).collect(),
env: HashMap::new(),
working_dir: None,
mode: ProcessMode::Automated,
initial_pty_size: None,
kill_timeout: None,
stdin_buffer_limit: None,
}
}
/// Tick and collect up to `n` notifications, with a max tick budget.
fn tick_collect(
rt: &Runtime,
inbox: &Inbox<ProcessNotification>,
n: usize,
max_ticks: usize,
) -> Vec<ProcessNotification> {
let mut msgs = Vec::new();
for _ in 0..max_ticks {
rt.tick();
// Small sleep to let I/O threads produce events
std::thread::sleep(std::time::Duration::from_millis(5));
while let Some(m) = inbox.try_recv() {
msgs.push(m);
if msgs.len() >= n {
return msgs;
}
}
}
msgs
}
// ── Spawner actor (needed because spawn_local_process requires &Ctx) ────────
#[derive(Clone)]
struct E2eSpawnRequest {
spec: ProcessSpec,
subscriber: ActorAddress,
reply_to: ActorAddress,
sender: ExternalSender,
}
#[derive(Clone, Debug)]
struct E2eSpawned(ActorAddress);
struct E2eSpawnerActor;
impl ActorInterface for E2eSpawnerActor {
type Incoming = E2eSpawnRequest;
type Response = E2eSpawned;
fn handle(&mut self, ctx: &Ctx, msg: E2eSpawnRequest) {
let addr = spawn_local_process(ctx, &msg.sender, msg.spec)
.expect("spawn_local_process failed");
// Subscribe the notification inbox
let _ = ctx.send(addr, ProcessCommand::Subscribe { address: msg.subscriber });
let _ = ctx.send(msg.reply_to, E2eSpawned(addr));
}
}
// ── Tests ───────────────────────────────────────────────────────────────────
#[test]
fn echo_hello_full_lifecycle() {
let rt = Runtime::new(RuntimeConfig::default());
let sender = rt.create_sender();
let notif_inbox = rt.new_inbox::<ProcessNotification>().unwrap();
let spawner_addr = rt.spawn(E2eSpawnerActor).unwrap();
let reply_inbox = rt.new_inbox::<E2eSpawned>().unwrap();
rt.tick();
rt.send_to(
spawner_addr,
E2eSpawnRequest {
spec: automated_spec("echo", &["hello"]),
subscriber: *notif_inbox.addr(),
reply_to: *reply_inbox.addr(),
sender: sender.clone(),
},
)
.unwrap();
// Tick enough for the spawner to process + the process actor to start
for _ in 0..10 {
rt.tick();
std::thread::sleep(std::time::Duration::from_millis(5));
}
let spawned = reply_inbox.try_recv().expect("should get spawned address");
let _proc_addr = spawned.0;
// Collect notifications: Started, Output("hello\n"), Exited(0)
let msgs = tick_collect(&rt, &notif_inbox, 3, 200);
let has_started = msgs.iter().any(|m| matches!(m, ProcessNotification::Started { .. }));
let has_output = msgs.iter().any(|m| {
if let ProcessNotification::Output { data, .. } = m {
String::from_utf8_lossy(data).contains("hello")
} else {
false
}
});
let has_exited = msgs.iter().any(|m| {
matches!(
m,
ProcessNotification::Exited {
status: ExitStatus::Code(0),
..
}
)
});
assert!(has_started, "should receive Started notification, got: {:?}", msgs);
assert!(has_output, "should receive Output with 'hello', got: {:?}", msgs);
assert!(has_exited, "should receive Exited(0) notification, got: {:?}", msgs);
// Verify ordering: Started before Output before Exited
let started_idx = msgs
.iter()
.position(|m| matches!(m, ProcessNotification::Started { .. }))
.unwrap();
let output_idx = msgs
.iter()
.position(|m| matches!(m, ProcessNotification::Output { .. }))
.unwrap();
let exited_idx = msgs
.iter()
.position(|m| matches!(m, ProcessNotification::Exited { .. }))
.unwrap();
assert!(
started_idx < output_idx,
"Started should come before Output"
);
assert!(
output_idx < exited_idx,
"Output should come before Exited"
);
}
#[test]
fn bad_command_reports_error_e2e() {
let rt = Runtime::new(RuntimeConfig::default());
let sender = rt.create_sender();
let notif_inbox = rt.new_inbox::<ProcessNotification>().unwrap();
let spawner_addr = rt.spawn(E2eSpawnerActor).unwrap();
let reply_inbox = rt.new_inbox::<E2eSpawned>().unwrap();
rt.tick();
rt.send_to(
spawner_addr,
E2eSpawnRequest {
spec: automated_spec("/nonexistent/binary/xyz", &[]),
subscriber: *notif_inbox.addr(),
reply_to: *reply_inbox.addr(),
sender: sender.clone(),
},
)
.unwrap();
for _ in 0..10 {
rt.tick();
std::thread::sleep(std::time::Duration::from_millis(5));
}
let msgs = tick_collect(&rt, &notif_inbox, 1, 200);
assert!(
msgs.iter().any(|m| matches!(m, ProcessNotification::Error { .. })),
"should receive Error notification for bad command, got: {:?}",
msgs
);
}