//! End-to-end tests: full Runtime + ExternalSender + ProcessActor. //! //! 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, n: usize, max_ticks: usize, ) -> Vec { 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::().unwrap(); let spawner_addr = rt.spawn(E2eSpawnerActor).unwrap(); let reply_inbox = rt.new_inbox::().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, ¬if_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::().unwrap(); let spawner_addr = rt.spawn(E2eSpawnerActor).unwrap(); let reply_inbox = rt.new_inbox::().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, ¬if_inbox, 1, 200); assert!( msgs.iter().any(|m| matches!(m, ProcessNotification::Error { .. })), "should receive Error notification for bad command, got: {:?}", msgs ); }