use std::sync::Arc;
use std::time::Duration;
use serde_json::{Value, json};
use tempfile::TempDir;
use super::*;
use crate::uds::server::{RpcError, RpcRouter, RpcServeOptions, serve_until};
struct StreamDaemon {
socket: std::path::PathBuf,
_dir: TempDir,
stop: Option<tokio::sync::oneshot::Sender<()>>,
}
impl Drop for StreamDaemon {
fn drop(&mut self) {
if let Some(tx) = self.stop.take() {
let _ = tx.send(());
}
}
}
async fn stream_daemon(items: Vec<Result<Value, RpcError>>) -> StreamDaemon {
stream_daemon_with(items, false).await
}
async fn stream_daemon_with(items: Vec<Result<Value, RpcError>>, hold_open: bool) -> StreamDaemon {
let dir = TempDir::new().expect("tempdir");
let socket = dir.path().join("memory.sock");
let listener = crate::uds::bind_hardened(&socket).expect("bind");
let items = Arc::new(items);
let router = RpcRouter::new().typed_stream::<Value, _, _>(
"memory.activity_stream",
move |_params: Value| {
let items = Arc::clone(&items);
let hold_open = hold_open;
async move {
let (tx, rx) = tokio::sync::mpsc::channel(16);
tokio::spawn(async move {
for item in items.iter() {
let frame = match item {
Ok(v) => Ok(v.clone()),
Err(e) => Err(RpcError::new(e.code, e.message.clone())),
};
if tx.send(frame).await.is_err() {
break;
}
}
if hold_open {
std::future::pending::<()>().await;
}
});
Ok(rx)
}
},
);
let (stop, shutdown) = tokio::sync::oneshot::channel::<()>();
tokio::spawn(async move {
serve_until(
&listener,
Arc::new(router),
RpcServeOptions::default(),
async {
let _ = shutdown.await;
},
)
.await;
});
StreamDaemon {
socket,
_dir: dir,
stop: Some(stop),
}
}
async fn drain_until_nonempty(feed: &mut ActivityFeed) -> Vec<MemoryEvent> {
for _ in 0..200 {
let batch = feed.drain();
if !batch.is_empty() {
return batch;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
Vec::new()
}
#[tokio::test]
async fn feed_drains_what_the_stream_pushed() {
let daemon = stream_daemon(vec![
Ok(json!({ "type": "palace_created", "id": "p", "name": "alpha" })),
Ok(json!({ "type": "palace_created", "id": "q", "name": "beta" })),
])
.await;
let mut feed = ActivityFeed::open(&daemon.socket)
.await
.expect("the stream opens");
let mut seen = drain_until_nonempty(&mut feed).await;
for _ in 0..50 {
if seen.len() >= 2 {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
seen.extend(feed.drain());
}
assert_eq!(
seen,
vec![
MemoryEvent::PalaceCreated {
name: "alpha".to_string()
},
MemoryEvent::PalaceCreated {
name: "beta".to_string()
},
],
"both pushed events arrive, in order"
);
}
#[tokio::test]
async fn feed_reports_a_terminal_error_and_stops_being_live() {
let daemon = stream_daemon(vec![Err(RpcError::internal("the daemon fell over"))]).await;
let feed = ActivityFeed::open(&daemon.socket)
.await
.expect("the stream opens before the handler fails");
for _ in 0..200 {
if !feed.is_live() {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert!(!feed.is_live(), "a terminal error ends the feed");
let reason = feed.take_last_error().expect("the reason is recorded");
assert!(
reason.contains("fell over"),
"the daemon's own message must survive: {reason}"
);
}
#[tokio::test]
async fn feed_open_fails_against_an_absent_socket() {
let dir = TempDir::new().expect("tempdir");
let result = ActivityFeed::open(&dir.path().join("absent.sock")).await;
assert!(
result.is_err(),
"nothing is serving that path, and the caller has to learn so"
);
}
#[tokio::test]
async fn feed_records_a_lag_without_logging_it_as_an_event() {
let daemon = stream_daemon_with(
vec![
Ok(json!({ "type": "lagged", "lagged": 7 })),
Ok(json!({ "type": "palace_created", "id": "p", "name": "after-the-gap" })),
],
true,
)
.await;
let mut feed = ActivityFeed::open(&daemon.socket)
.await
.expect("the stream opens");
let seen = drain_until_nonempty(&mut feed).await;
assert_eq!(
seen,
vec![MemoryEvent::PalaceCreated {
name: "after-the-gap".to_string()
}],
"the lag notice is not an activity row"
);
let reason = feed.take_last_error().expect("the lag is recorded");
assert!(
reason.contains('7'),
"the reader has to be able to say how many it missed: {reason}"
);
assert!(
feed.is_live(),
"a lag is a hole in a working feed, not the end of one"
);
}
#[tokio::test]
async fn feed_take_last_error_clears_the_slot() {
let daemon = stream_daemon_with(vec![Ok(json!({ "type": "lagged", "lagged": 3 }))], true).await;
let feed = ActivityFeed::open(&daemon.socket)
.await
.expect("the stream opens");
let mut first = None;
for _ in 0..200 {
first = feed.take_last_error();
if first.is_some() {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert!(
first.is_some_and(|r| r.contains('3')),
"the first take reports the lag"
);
assert!(
feed.take_last_error().is_none(),
"a second take on an otherwise quiet feed reports nothing — the notice \
is rendered once, not on every tick until the stream dies"
);
}