feat(daemon): actor Snapshot message + dirty tracking + final snapshot on exit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ use spacesh_proto::workspace::SurfaceSpec;
|
|||||||
use spacesh_pty::{PtyHandle, SpawnSpec};
|
use spacesh_pty::{PtyHandle, SpawnSpec};
|
||||||
use tokio::sync::{broadcast, mpsc, oneshot};
|
use tokio::sync::{broadcast, mpsc, oneshot};
|
||||||
use tokio::time::{Duration, Instant};
|
use tokio::time::{Duration, Instant};
|
||||||
|
use crate::snapshot_store::SnapshotMsg;
|
||||||
|
|
||||||
/// Spawn (or restart) a surface actor from a persisted spec. Injects
|
/// Spawn (or restart) a surface actor from a persisted spec. Injects
|
||||||
/// SPACESH_SURFACE_ID into the child env, mirroring `new_surface`.
|
/// SPACESH_SURFACE_ID into the child env, mirroring `new_surface`.
|
||||||
@@ -24,6 +25,7 @@ pub fn spawn_from_spec(
|
|||||||
hooks_active: bool,
|
hooks_active: bool,
|
||||||
state_tx: mpsc::UnboundedSender<(SurfaceId, SurfaceState)>,
|
state_tx: mpsc::UnboundedSender<(SurfaceId, SurfaceState)>,
|
||||||
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
||||||
|
snapshot_tx: mpsc::UnboundedSender<SnapshotMsg>,
|
||||||
) -> std::io::Result<SurfaceHandle> {
|
) -> std::io::Result<SurfaceHandle> {
|
||||||
let mut env = vec![("SPACESH_SURFACE_ID".to_string(), id.0.clone())];
|
let mut env = vec![("SPACESH_SURFACE_ID".to_string(), id.0.clone())];
|
||||||
env.extend(extra_env);
|
env.extend(extra_env);
|
||||||
@@ -35,7 +37,7 @@ pub fn spawn_from_spec(
|
|||||||
rows: spec.rows,
|
rows: spec.rows,
|
||||||
env,
|
env,
|
||||||
};
|
};
|
||||||
Ok(spawn_surface_deferred(id, workspace_id, spawn_spec, spec.cols, spec.rows, hooks_active, state_tx, exit_tx))
|
Ok(spawn_surface_deferred(id, workspace_id, spawn_spec, spec.cols, spec.rows, hooks_active, state_tx, exit_tx, snapshot_tx))
|
||||||
}
|
}
|
||||||
|
|
||||||
const BROADCAST_CAP: usize = 1024;
|
const BROADCAST_CAP: usize = 1024;
|
||||||
@@ -53,6 +55,8 @@ pub enum SurfaceMsg {
|
|||||||
Attach { reply: oneshot::Sender<broadcast::Receiver<Vec<u8>>> },
|
Attach { reply: oneshot::Sender<broadcast::Receiver<Vec<u8>>> },
|
||||||
/// Attach with snapshot: subscribe AND capture the grid in one actor turn.
|
/// Attach with snapshot: subscribe AND capture the grid in one actor turn.
|
||||||
AttachSnapshot { reply: oneshot::Sender<(Snapshot, broadcast::Receiver<Vec<u8>>)> },
|
AttachSnapshot { reply: oneshot::Sender<(Snapshot, broadcast::Receiver<Vec<u8>>)> },
|
||||||
|
/// On-demand snapshot without subscribing; bool = dirty since last snapshot.
|
||||||
|
Snapshot { reply: oneshot::Sender<(Snapshot, bool)> },
|
||||||
Close,
|
Close,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,10 +80,11 @@ pub fn spawn_surface(
|
|||||||
hooks_active: bool,
|
hooks_active: bool,
|
||||||
state_tx: mpsc::UnboundedSender<(SurfaceId, SurfaceState)>,
|
state_tx: mpsc::UnboundedSender<(SurfaceId, SurfaceState)>,
|
||||||
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
||||||
|
snapshot_tx: mpsc::UnboundedSender<SnapshotMsg>,
|
||||||
) -> SurfaceHandle {
|
) -> SurfaceHandle {
|
||||||
let (tx, rx) = mpsc::channel::<SurfaceMsg>(64);
|
let (tx, rx) = mpsc::channel::<SurfaceMsg>(64);
|
||||||
let (bcast, _) = broadcast::channel::<Vec<u8>>(BROADCAST_CAP);
|
let (bcast, _) = broadcast::channel::<Vec<u8>>(BROADCAST_CAP);
|
||||||
tokio::spawn(run_actor(id.clone(), pty, cols, rows, hooks_active, bcast, rx, state_tx, exit_tx, Vec::new()));
|
tokio::spawn(run_actor(id.clone(), pty, cols, rows, hooks_active, bcast, rx, state_tx, exit_tx, snapshot_tx, Vec::new()));
|
||||||
SurfaceHandle { id, workspace_id, tx }
|
SurfaceHandle { id, workspace_id, tx }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +102,7 @@ pub fn spawn_surface_deferred(
|
|||||||
hooks_active: bool,
|
hooks_active: bool,
|
||||||
state_tx: mpsc::UnboundedSender<(SurfaceId, SurfaceState)>,
|
state_tx: mpsc::UnboundedSender<(SurfaceId, SurfaceState)>,
|
||||||
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
||||||
|
snapshot_tx: mpsc::UnboundedSender<SnapshotMsg>,
|
||||||
) -> SurfaceHandle {
|
) -> SurfaceHandle {
|
||||||
let (tx, mut rx) = mpsc::channel::<SurfaceMsg>(64);
|
let (tx, mut rx) = mpsc::channel::<SurfaceMsg>(64);
|
||||||
let (bcast, _) = broadcast::channel::<Vec<u8>>(BROADCAST_CAP);
|
let (bcast, _) = broadcast::channel::<Vec<u8>>(BROADCAST_CAP);
|
||||||
@@ -122,6 +128,10 @@ pub fn spawn_surface_deferred(
|
|||||||
let snap = snapshot_ansi(&GridSurface::new(cols, rows));
|
let snap = snapshot_ansi(&GridSurface::new(cols, rows));
|
||||||
let _ = reply.send((snap, sub));
|
let _ = reply.send((snap, sub));
|
||||||
}
|
}
|
||||||
|
Some(SurfaceMsg::Snapshot { reply }) => {
|
||||||
|
let snap = snapshot_ansi(&GridSurface::new(cols, rows));
|
||||||
|
let _ = reply.send((snap, false));
|
||||||
|
}
|
||||||
Some(SurfaceMsg::Close) | None => break false,
|
Some(SurfaceMsg::Close) | None => break false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,7 +145,7 @@ pub fn spawn_surface_deferred(
|
|||||||
spec.cols = cols;
|
spec.cols = cols;
|
||||||
spec.rows = rows;
|
spec.rows = rows;
|
||||||
match PtyHandle::spawn(spec) {
|
match PtyHandle::spawn(spec) {
|
||||||
Ok(pty) => run_actor(actor_id, pty, cols, rows, hooks_active, bcast, rx, state_tx, exit_tx, prebuf).await,
|
Ok(pty) => run_actor(actor_id, pty, cols, rows, hooks_active, bcast, rx, state_tx, exit_tx, snapshot_tx, prebuf).await,
|
||||||
Err(_) => { let _ = exit_tx.send((actor_id, 127)); }
|
Err(_) => { let _ = exit_tx.send((actor_id, 127)); }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -156,6 +166,7 @@ async fn run_actor(
|
|||||||
mut rx: mpsc::Receiver<SurfaceMsg>,
|
mut rx: mpsc::Receiver<SurfaceMsg>,
|
||||||
state_tx: mpsc::UnboundedSender<(SurfaceId, SurfaceState)>,
|
state_tx: mpsc::UnboundedSender<(SurfaceId, SurfaceState)>,
|
||||||
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
exit_tx: mpsc::UnboundedSender<(SurfaceId, i32)>,
|
||||||
|
snapshot_tx: mpsc::UnboundedSender<SnapshotMsg>,
|
||||||
prebuffered_input: Vec<u8>,
|
prebuffered_input: Vec<u8>,
|
||||||
) {
|
) {
|
||||||
let actor_id = id.clone();
|
let actor_id = id.clone();
|
||||||
@@ -173,6 +184,7 @@ async fn run_actor(
|
|||||||
// (hooks active, or any OSC 133 marker observed).
|
// (hooks active, or any OSC 133 marker observed).
|
||||||
let mut deterministic = hooks_active;
|
let mut deterministic = hooks_active;
|
||||||
let mut last_state = SurfaceState::Idle;
|
let mut last_state = SurfaceState::Idle;
|
||||||
|
let mut dirty = false;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Copy the deadline into an owned local so the timer future doesn't
|
// Copy the deadline into an owned local so the timer future doesn't
|
||||||
@@ -202,8 +214,15 @@ async fn run_actor(
|
|||||||
// this snapshot. Broadcasting here would double-render on reattach.
|
// this snapshot. Broadcasting here would double-render on reattach.
|
||||||
let sub = bcast.subscribe();
|
let sub = bcast.subscribe();
|
||||||
let snap = snapshot_ansi(&grid);
|
let snap = snapshot_ansi(&grid);
|
||||||
|
dirty = false;
|
||||||
let _ = reply.send((snap, sub));
|
let _ = reply.send((snap, sub));
|
||||||
}
|
}
|
||||||
|
Some(SurfaceMsg::Snapshot { reply }) => {
|
||||||
|
let snap = snapshot_ansi(&grid);
|
||||||
|
let was_dirty = dirty;
|
||||||
|
dirty = false;
|
||||||
|
let _ = reply.send((snap, was_dirty));
|
||||||
|
}
|
||||||
Some(SurfaceMsg::Close) | None => { pty.kill(); break; }
|
Some(SurfaceMsg::Close) | None => { pty.kill(); break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,6 +230,7 @@ async fn run_actor(
|
|||||||
match chunk {
|
match chunk {
|
||||||
Some(bytes) => {
|
Some(bytes) => {
|
||||||
pending.extend_from_slice(&bytes);
|
pending.extend_from_slice(&bytes);
|
||||||
|
dirty = true;
|
||||||
if flush_deadline.is_none() {
|
if flush_deadline.is_none() {
|
||||||
flush_deadline = Some(Instant::now() + FLUSH_INTERVAL);
|
flush_deadline = Some(Instant::now() + FLUSH_INTERVAL);
|
||||||
}
|
}
|
||||||
@@ -233,6 +253,8 @@ async fn run_actor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let final_snap = snapshot_ansi(&grid);
|
||||||
|
let _ = snapshot_tx.send(SnapshotMsg::Save(actor_id.clone(), final_snap));
|
||||||
let code = pty.wait();
|
let code = pty.wait();
|
||||||
let _ = exit_tx.send((actor_id, code));
|
let _ = exit_tx.send((actor_id, code));
|
||||||
}
|
}
|
||||||
@@ -307,7 +329,8 @@ mod tests {
|
|||||||
let pty = PtyHandle::spawn(spec("printf HELLO; sleep 0.3")).unwrap();
|
let pty = PtyHandle::spawn(spec("printf HELLO; sleep 0.3")).unwrap();
|
||||||
let (state_tx, _state_rx) = mpsc::unbounded_channel();
|
let (state_tx, _state_rx) = mpsc::unbounded_channel();
|
||||||
let (exit_tx, _exit_rx) = mpsc::unbounded_channel();
|
let (exit_tx, _exit_rx) = mpsc::unbounded_channel();
|
||||||
let handle = spawn_surface(SurfaceId("s_1".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx);
|
let (snap_tx, _snap_rx) = mpsc::unbounded_channel();
|
||||||
|
let handle = spawn_surface(SurfaceId("s_1".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx, snap_tx);
|
||||||
|
|
||||||
let (reply_tx, reply_rx) = oneshot::channel();
|
let (reply_tx, reply_rx) = oneshot::channel();
|
||||||
handle.tx.send(SurfaceMsg::Attach { reply: reply_tx }).await.unwrap();
|
handle.tx.send(SurfaceMsg::Attach { reply: reply_tx }).await.unwrap();
|
||||||
@@ -332,7 +355,8 @@ mod tests {
|
|||||||
let pty = PtyHandle::spawn(spec("exit 7")).unwrap();
|
let pty = PtyHandle::spawn(spec("exit 7")).unwrap();
|
||||||
let (state_tx, _state_rx) = mpsc::unbounded_channel();
|
let (state_tx, _state_rx) = mpsc::unbounded_channel();
|
||||||
let (exit_tx, mut exit_rx) = mpsc::unbounded_channel();
|
let (exit_tx, mut exit_rx) = mpsc::unbounded_channel();
|
||||||
let _handle = spawn_surface(SurfaceId("s_2".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx);
|
let (snap_tx, _snap_rx) = mpsc::unbounded_channel();
|
||||||
|
let _handle = spawn_surface(SurfaceId("s_2".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx, snap_tx);
|
||||||
let (sid, code) = tokio::time::timeout(tokio::time::Duration::from_secs(3), exit_rx.recv())
|
let (sid, code) = tokio::time::timeout(tokio::time::Duration::from_secs(3), exit_rx.recv())
|
||||||
.await.unwrap().unwrap();
|
.await.unwrap().unwrap();
|
||||||
assert_eq!(sid, SurfaceId("s_2".into()));
|
assert_eq!(sid, SurfaceId("s_2".into()));
|
||||||
@@ -345,7 +369,8 @@ mod tests {
|
|||||||
let pty = PtyHandle::spawn(spec("printf SNAPME; sleep 0.5")).unwrap();
|
let pty = PtyHandle::spawn(spec("printf SNAPME; sleep 0.5")).unwrap();
|
||||||
let (state_tx, _state_rx) = mpsc::unbounded_channel();
|
let (state_tx, _state_rx) = mpsc::unbounded_channel();
|
||||||
let (exit_tx, _exit_rx) = mpsc::unbounded_channel();
|
let (exit_tx, _exit_rx) = mpsc::unbounded_channel();
|
||||||
let handle = spawn_surface(SurfaceId("s_s".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx);
|
let (snap_tx, _snap_rx) = mpsc::unbounded_channel();
|
||||||
|
let handle = spawn_surface(SurfaceId("s_s".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx, snap_tx);
|
||||||
|
|
||||||
// Give the child time to write and the actor time to flush into the grid.
|
// Give the child time to write and the actor time to flush into the grid.
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||||
@@ -367,7 +392,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let (state_tx, _state_rx) = mpsc::unbounded_channel();
|
let (state_tx, _state_rx) = mpsc::unbounded_channel();
|
||||||
let (exit_tx, _rx) = mpsc::unbounded_channel();
|
let (exit_tx, _rx) = mpsc::unbounded_channel();
|
||||||
let handle = spawn_from_spec(SurfaceId("s_r".into()), WorkspaceId("w_1".into()), &spec, vec![], false, state_tx, exit_tx).unwrap();
|
let (snap_tx, _snap_rx) = mpsc::unbounded_channel();
|
||||||
|
let handle = spawn_from_spec(SurfaceId("s_r".into()), WorkspaceId("w_1".into()), &spec, vec![], false, state_tx, exit_tx, snap_tx).unwrap();
|
||||||
let (reply_tx, reply_rx) = oneshot::channel();
|
let (reply_tx, reply_rx) = oneshot::channel();
|
||||||
handle.tx.send(SurfaceMsg::Attach { reply: reply_tx }).await.unwrap();
|
handle.tx.send(SurfaceMsg::Attach { reply: reply_tx }).await.unwrap();
|
||||||
let mut sub = reply_rx.await.unwrap();
|
let mut sub = reply_rx.await.unwrap();
|
||||||
@@ -412,7 +438,8 @@ mod tests {
|
|||||||
let _serial = crate::test_support::serial();
|
let _serial = crate::test_support::serial();
|
||||||
let (state_tx, _s) = mpsc::unbounded_channel();
|
let (state_tx, _s) = mpsc::unbounded_channel();
|
||||||
let (exit_tx, _e) = mpsc::unbounded_channel();
|
let (exit_tx, _e) = mpsc::unbounded_channel();
|
||||||
let handle = spawn_surface_deferred(SurfaceId("s_d".into()), WorkspaceId("w_1".into()), stty_spec(), 80, 24, false, state_tx, exit_tx);
|
let (snap_tx, _snap_rx) = mpsc::unbounded_channel();
|
||||||
|
let handle = spawn_surface_deferred(SurfaceId("s_d".into()), WorkspaceId("w_1".into()), stty_spec(), 80, 24, false, state_tx, exit_tx, snap_tx);
|
||||||
|
|
||||||
let (rtx, rrx) = oneshot::channel();
|
let (rtx, rrx) = oneshot::channel();
|
||||||
handle.tx.send(SurfaceMsg::Attach { reply: rtx }).await.unwrap();
|
handle.tx.send(SurfaceMsg::Attach { reply: rtx }).await.unwrap();
|
||||||
@@ -429,7 +456,8 @@ mod tests {
|
|||||||
let _serial = crate::test_support::serial();
|
let _serial = crate::test_support::serial();
|
||||||
let (state_tx, _s) = mpsc::unbounded_channel();
|
let (state_tx, _s) = mpsc::unbounded_channel();
|
||||||
let (exit_tx, _e) = mpsc::unbounded_channel();
|
let (exit_tx, _e) = mpsc::unbounded_channel();
|
||||||
let handle = spawn_surface_deferred(SurfaceId("s_f".into()), WorkspaceId("w_1".into()), stty_spec(), 80, 24, false, state_tx, exit_tx);
|
let (snap_tx, _snap_rx) = mpsc::unbounded_channel();
|
||||||
|
let handle = spawn_surface_deferred(SurfaceId("s_f".into()), WorkspaceId("w_1".into()), stty_spec(), 80, 24, false, state_tx, exit_tx, snap_tx);
|
||||||
|
|
||||||
let (rtx, rrx) = oneshot::channel();
|
let (rtx, rrx) = oneshot::channel();
|
||||||
handle.tx.send(SurfaceMsg::Attach { reply: rtx }).await.unwrap();
|
handle.tx.send(SurfaceMsg::Attach { reply: rtx }).await.unwrap();
|
||||||
@@ -445,7 +473,8 @@ mod tests {
|
|||||||
let pty = PtyHandle::spawn(spec("printf '\\033]133;C\\007'; printf working; printf '\\033]133;D;0\\007'; sleep 0.3")).unwrap();
|
let pty = PtyHandle::spawn(spec("printf '\\033]133;C\\007'; printf working; printf '\\033]133;D;0\\007'; sleep 0.3")).unwrap();
|
||||||
let (state_tx, mut state_rx) = mpsc::unbounded_channel();
|
let (state_tx, mut state_rx) = mpsc::unbounded_channel();
|
||||||
let (exit_tx, _exit_rx) = mpsc::unbounded_channel();
|
let (exit_tx, _exit_rx) = mpsc::unbounded_channel();
|
||||||
let _h = spawn_surface(SurfaceId("s_o".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx);
|
let (snap_tx, _snap_rx) = mpsc::unbounded_channel();
|
||||||
|
let _h = spawn_surface(SurfaceId("s_o".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx, snap_tx);
|
||||||
let mut seen = Vec::new();
|
let mut seen = Vec::new();
|
||||||
let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(2);
|
let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(2);
|
||||||
while tokio::time::Instant::now() < deadline {
|
while tokio::time::Instant::now() < deadline {
|
||||||
@@ -457,4 +486,45 @@ mod tests {
|
|||||||
assert!(seen.contains(&SurfaceState::Work), "states: {seen:?}");
|
assert!(seen.contains(&SurfaceState::Work), "states: {seen:?}");
|
||||||
assert!(seen.contains(&SurfaceState::Done), "states: {seen:?}");
|
assert!(seen.contains(&SurfaceState::Done), "states: {seen:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn snapshot_msg_returns_grid_and_tracks_dirty() {
|
||||||
|
let _serial = crate::test_support::serial();
|
||||||
|
let pty = PtyHandle::spawn(spec("printf DIRTYME; sleep 0.4")).unwrap();
|
||||||
|
let (state_tx, _s) = mpsc::unbounded_channel();
|
||||||
|
let (exit_tx, _e) = mpsc::unbounded_channel();
|
||||||
|
let (snap_tx, _snap_rx) = mpsc::unbounded_channel();
|
||||||
|
let handle = spawn_surface(SurfaceId("s_1".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx, snap_tx);
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||||
|
let (reply_tx, reply_rx) = oneshot::channel();
|
||||||
|
handle.tx.send(SurfaceMsg::Snapshot { reply: reply_tx }).await.unwrap();
|
||||||
|
let (snap, dirty) = reply_rx.await.unwrap();
|
||||||
|
assert!(snap.ansi.contains("DIRTYME"), "snapshot: {:?}", snap.ansi);
|
||||||
|
assert!(dirty, "first snapshot after output should be dirty");
|
||||||
|
|
||||||
|
let (reply_tx, reply_rx) = oneshot::channel();
|
||||||
|
handle.tx.send(SurfaceMsg::Snapshot { reply: reply_tx }).await.unwrap();
|
||||||
|
let (_snap2, dirty2) = reply_rx.await.unwrap();
|
||||||
|
assert!(!dirty2, "second snapshot with no new output should be clean");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn final_snapshot_sent_on_exit() {
|
||||||
|
let _serial = crate::test_support::serial();
|
||||||
|
let pty = PtyHandle::spawn(spec("printf BYE")).unwrap(); // exits immediately
|
||||||
|
let (state_tx, _s) = mpsc::unbounded_channel();
|
||||||
|
let (exit_tx, _e) = mpsc::unbounded_channel();
|
||||||
|
let (snap_tx, mut snap_rx) = mpsc::unbounded_channel();
|
||||||
|
let _handle = spawn_surface(SurfaceId("s_x".into()), WorkspaceId("w_1".into()), pty, 80, 24, false, state_tx, exit_tx, snap_tx);
|
||||||
|
|
||||||
|
let msg = tokio::time::timeout(Duration::from_secs(2), snap_rx.recv()).await.unwrap().unwrap();
|
||||||
|
match msg {
|
||||||
|
crate::snapshot_store::SnapshotMsg::Save(sid, snap) => {
|
||||||
|
assert_eq!(sid.0, "s_x");
|
||||||
|
assert!(snap.ansi.contains("BYE"), "final snapshot: {:?}", snap.ansi);
|
||||||
|
}
|
||||||
|
_ => panic!("expected a Save message on exit"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user