#![cfg(feature = "ipc")]
use std::path::PathBuf;
use std::time::Duration;
use acton_reactive::ipc::{IpcClient, IpcConfig, IpcEnvelope, IpcListenerHandle};
use acton_reactive::prelude::*;
use acton_test::prelude::*;
#[acton_message(ipc)]
struct CountRequest {
count: u32,
}
#[acton_message(ipc)]
struct Tick {
number: u32,
}
#[acton_actor]
struct CounterState;
async fn start_streaming_server(
socket_path: PathBuf,
) -> anyhow::Result<(ActorRuntime, IpcListenerHandle)> {
let mut runtime: ActorRuntime = ActonApp::launch_async().await;
let registry = runtime.ipc_registry();
registry.register::<CountRequest>("CountRequest");
registry.register::<Tick>("Tick");
let mut counter = runtime.new_actor_with_name::<CounterState>("counter".to_string());
counter.act_on::<CountRequest>(|_actor, envelope| {
let count = envelope.message().count;
let reply_envelope = envelope.reply_envelope();
Reply::pending(async move {
for number in 0..count {
reply_envelope.send(Tick { number }).await;
}
})
});
let handle = counter.start().await;
runtime.ipc_expose("counter", handle);
let mut config = IpcConfig::default();
config.socket.path = Some(socket_path);
let listener = runtime.start_ipc_listener_with_config(config).await?;
Ok((runtime, listener))
}
#[acton_test]
async fn test_request_stream_consumes_all_frames() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let socket_path = dir.path().join("ipc.sock");
let (mut runtime, listener) = start_streaming_server(socket_path.clone()).await?;
let client = IpcClient::connect(&socket_path).await?;
let envelope = IpcEnvelope::new_stream_request(
"counter",
"CountRequest",
serde_json::json!({ "count": 5 }),
);
let mut stream_rx = client.request_stream(envelope).await?;
let mut frames = Vec::new();
while let Some(frame) = tokio::time::timeout(Duration::from_secs(5), stream_rx.recv())
.await
.expect("stream should terminate, not hang")
{
frames.push(frame);
}
assert_eq!(frames.len(), 6);
for (expected_sequence, frame) in (0_u32..).zip(frames.iter()) {
assert_eq!(frame.sequence, expected_sequence);
assert!(frame.error.is_none());
}
for (expected_number, frame) in (0_u32..).zip(frames[..5].iter()) {
assert!(!frame.is_final);
let payload = frame.payload.as_ref().expect("data frame should have payload");
assert_eq!(payload["number"], serde_json::json!(expected_number));
}
let final_frame = frames.last().expect("frames should not be empty");
assert!(final_frame.is_final);
client.disconnect().await?;
listener.stop();
runtime.shutdown_all().await?;
Ok(())
}
#[acton_test]
async fn test_request_stream_unknown_actor_yields_error_frame() -> anyhow::Result<()> {
let dir = tempfile::tempdir()?;
let socket_path = dir.path().join("ipc.sock");
let (mut runtime, listener) = start_streaming_server(socket_path.clone()).await?;
let client = IpcClient::connect(&socket_path).await?;
let envelope = IpcEnvelope::new_stream_request(
"nonexistent",
"CountRequest",
serde_json::json!({ "count": 1 }),
);
let mut stream_rx = client.request_stream(envelope).await?;
let frame = tokio::time::timeout(Duration::from_secs(5), stream_rx.recv())
.await
.expect("stream should terminate, not hang")
.expect("should receive an error frame");
assert!(frame.is_final);
let error = frame.error.expect("error frame should carry a message");
assert!(error.contains("nonexistent"), "unexpected error: {error}");
let next = tokio::time::timeout(Duration::from_secs(5), stream_rx.recv())
.await
.expect("stream should terminate, not hang");
assert!(next.is_none());
client.disconnect().await?;
listener.stop();
runtime.shutdown_all().await?;
Ok(())
}