diff options
Diffstat (limited to 'src/server/headless/tests/pane_graphics.rs')
| -rw-r--r-- | src/server/headless/tests/pane_graphics.rs | 1133 |
1 files changed, 0 insertions, 1133 deletions
diff --git a/src/server/headless/tests/pane_graphics.rs b/src/server/headless/tests/pane_graphics.rs index 3e36a8fc..190eaa80 100644 --- a/src/server/headless/tests/pane_graphics.rs +++ b/src/server/headless/tests/pane_graphics.rs @@ -5,93 +5,6 @@ fn receive_render(receiver: &std::sync::mpsc::Receiver<Vec<u8>>, timeout: Durati } #[tokio::test] -async fn client_shell_surface_sends_complete_placements_and_each_live_asset_once() { - let (mut server, _control_rx, client_rx, pane_id) = - retained_test_server_with_control(b"client shell graphics"); - let client = server.clients.get_mut(&1).unwrap(); - client.mode = ClientConnectionMode::ClientShell; - client.render_state = - crate::server::render_stream::ClientRenderState::new(RenderEncoding::SemanticFrame); - client.cell_size = crate::kitty_graphics::HostCellSize { - width_px: 10, - height_px: 20, - }; - set_graphics_layer(&mut server, pane_id, vec![1, 2, 3, 4]); - - server.render_and_stream(); - let first = read_server_message(receive_render(&client_rx, Duration::from_millis(100))); - let ServerMessage::PaneSurface(first) = first else { - panic!("expected client shell pane surface"); - }; - assert_eq!(first.graphics.placements.len(), 1); - assert_eq!(first.graphics.assets.len(), 1); - assert_eq!(first.graphics.assets[0].data, vec![1, 2, 3, 4]); - assert!(matches!( - first.graphics.placements[0].asset.source, - crate::protocol::SurfaceGraphicsSource::PaneLayer { .. } - )); - - server.clients.get_mut(&1).unwrap().request_repaint(); - server.render_and_stream(); - let second = read_server_message(receive_render(&client_rx, Duration::from_millis(100))); - let ServerMessage::PaneSurface(second) = second else { - panic!("expected replacement client shell pane surface"); - }; - assert_eq!(second.graphics.placements, first.graphics.placements); - assert!(second.graphics.assets.is_empty()); - - // A post-commit typed surface.set(true) resets only this viewer's delivery cache, so the - // already selected target receives the asset again without relying on graphics that may have - // arrived during the frozen source frame. - assert!(server.set_client_shell_surface_active(1, true).is_some()); - server.render_and_stream(); - let replay = read_server_message(receive_render(&client_rx, Duration::from_millis(100))); - let ServerMessage::PaneSurface(replay) = replay else { - panic!("expected post-commit graphics replay"); - }; - assert_eq!(replay.graphics.placements, first.graphics.placements); - assert_eq!(replay.graphics.assets.len(), 1); - assert_eq!(replay.graphics.assets[0].data, vec![1, 2, 3, 4]); -} - -#[tokio::test] -async fn client_shell_asset_delivery_is_bounded_to_the_current_live_scene() { - let (mut server, _control_rx, client_rx, pane_id) = - retained_test_server_with_control(b"client shell graphics"); - let client = server.clients.get_mut(&1).unwrap(); - client.mode = ClientConnectionMode::ClientShell; - client.render_state = - crate::server::render_stream::ClientRenderState::new(RenderEncoding::SemanticFrame); - client.cell_size = crate::kitty_graphics::HostCellSize { - width_px: 10, - height_px: 20, - }; - set_graphics_layer(&mut server, pane_id, vec![1, 2, 3, 4]); - - server.render_and_stream(); - let _first = receive_render(&client_rx, Duration::from_millis(100)); - server.app.pane_graphics.slots.clear(); - server.app.pane_graphics.mark_changed(); - server.clients.get_mut(&1).unwrap().request_repaint(); - server.render_and_stream(); - let removed = read_server_message(receive_render(&client_rx, Duration::from_millis(100))); - let ServerMessage::PaneSurface(removed) = removed else { - panic!("expected removed client shell scene"); - }; - assert!(removed.graphics.placements.is_empty()); - - set_graphics_layer(&mut server, pane_id, vec![1, 2, 3, 4]); - server.clients.get_mut(&1).unwrap().request_repaint(); - server.render_and_stream(); - let restored = read_server_message(receive_render(&client_rx, Duration::from_millis(100))); - let ServerMessage::PaneSurface(restored) = restored else { - panic!("expected restored client shell scene"); - }; - assert_eq!(restored.graphics.assets.len(), 1); - assert_eq!(restored.graphics.assets[0].data, vec![1, 2, 3, 4]); -} - -#[tokio::test] async fn first_kitty_image_updates_retained_surface_without_full_redraw() { let (mut server, _control_rx, client_rx, pane_id) = retained_test_server_with_control(b"text before image"); @@ -409,87 +322,6 @@ async fn client_shell_delivers_equal_pixels_for_distinct_terminal_image_ids() { assert!(surface.graphics.assets.is_empty()); } -#[tokio::test] -async fn full_client_shell_render_lane_does_not_commit_graphics_delivery() { - let (mut server, _control_rx, client_rx, pane_id) = - retained_test_server_with_control(b"client shell graphics"); - let client = server.clients.get_mut(&1).unwrap(); - client.mode = ClientConnectionMode::ClientShell; - client.render_state = - crate::server::render_stream::ClientRenderState::new(RenderEncoding::SemanticFrame); - client.cell_size = crate::kitty_graphics::HostCellSize { - width_px: 10, - height_px: 20, - }; - set_graphics_layer(&mut server, pane_id, vec![5, 6, 7, 8]); - - fill_render_lane(&server); - server.render_and_stream(); - let _older = receive_render(&client_rx, Duration::from_millis(100)); - server.render_and_stream(); - let message = read_server_message(receive_render(&client_rx, Duration::from_millis(100))); - let ServerMessage::PaneSurface(surface) = message else { - panic!("expected client shell pane surface"); - }; - assert_eq!(surface.graphics.assets.len(), 1); - assert_eq!(surface.graphics.assets[0].data, vec![5, 6, 7, 8]); -} - -fn graphics_key(pane_id: crate::layout::PaneId) -> crate::app::pane_graphics::Key { - (pane_id, api::schema::PANE_GRAPHICS_PRIMARY_LAYER_ID.into()) -} - -fn active_gate() -> std::sync::Arc<std::sync::atomic::AtomicBool> { - std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)) -} - -fn set_graphics_layer(server: &mut HeadlessServer, pane_id: crate::layout::PaneId, data: Vec<u8>) { - set_named_graphics_layer( - server, - pane_id, - api::schema::PANE_GRAPHICS_PRIMARY_LAYER_ID, - data, - 0, - ); -} - -fn set_named_graphics_layer( - server: &mut HeadlessServer, - pane_id: crate::layout::PaneId, - layer_id: &str, - data: Vec<u8>, - z_index: i32, -) { - let key = (pane_id, layer_id.into()); - let host_image_id = server.app.pane_graphics.reserve_image_id(&key).unwrap(); - let layer = crate::app::pane_graphics::Layer::inline( - api::schema::PaneGraphicsFormat::Png, - 1, - 1, - data, - Default::default(), - z_index, - ); - server.app.pane_graphics.slots.insert( - key, - crate::app::pane_graphics::Slot::test(host_image_id, Some(layer)), - ); -} - -fn set_stream_owner(server: &mut HeadlessServer, pane_id: crate::layout::PaneId, owner: &str) { - let key = graphics_key(pane_id); - if let Some(slot) = server.app.pane_graphics.slots.get_mut(&key) { - slot.stream_owner = Some(owner.into()); - slot.stream_active = Some(active_gate()); - } else { - let host_image_id = server.app.pane_graphics.reserve_image_id(&key).unwrap(); - let mut slot = crate::app::pane_graphics::Slot::test(host_image_id, None); - slot.stream_owner = Some(owner.into()); - slot.stream_active = Some(active_gate()); - server.app.pane_graphics.slots.insert(key, slot); - } -} - fn fill_render_lane(server: &HeadlessServer) { let queued = HeadlessServer::frame_server_message(&ServerMessage::ReloadSoundConfig) .expect("dummy frame"); @@ -500,105 +332,6 @@ fn fill_render_lane(server: &HeadlessServer) { .test_fill_render(queued); } -fn stream_set_message( - id: &str, - pane_id: &str, - owner: &str, - data: Vec<u8>, -) -> (api::ApiRequestMessage, std::sync::mpsc::Receiver<String>) { - let (respond_to, response_rx) = std::sync::mpsc::channel(); - ( - api::ApiRequestMessage { - request: api::schema::Request { - id: id.into(), - method: api::schema::Method::PaneGraphicsStreamSet( - api::schema::PaneGraphicsSetParams { - pane_id: pane_id.into(), - layer_id: None, - z_index: 0, - owner: owner.into(), - format: api::schema::PaneGraphicsFormat::Png, - image_width: 1, - image_height: 1, - data: Some(data), - data_base64: String::new(), - placement: api::schema::PaneGraphicsPlacementParams::default(), - }, - ), - }, - respond_to, - response_write_complete: None, - stream_active: None, - }, - response_rx, - ) -} - -#[cfg(unix)] -fn sparse_direct_frame( - server: &HeadlessServer, - name: &str, - image_width: u32, - image_height: u32, -) -> String { - use std::os::unix::fs::OpenOptionsExt as _; - - let path = server - .app - .pane_graphics_files - .source_directory() - .unwrap() - .join(name); - let file = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) - .open(&path) - .unwrap(); - file.set_len(u64::from(image_width) * u64::from(image_height) * 4) - .unwrap(); - path.to_string_lossy().into_owned() -} - -#[cfg(unix)] -fn direct_stream_message( - id: &str, - pane_id: &str, - owner: &str, - path: String, - image_width: u32, - image_height: u32, - sequence: u64, -) -> (api::ApiRequestMessage, std::sync::mpsc::Receiver<String>) { - let (respond_to, response_rx) = std::sync::mpsc::channel(); - ( - api::ApiRequestMessage { - request: api::schema::Request { - id: id.into(), - method: api::schema::Method::PaneGraphicsStreamDirect( - api::schema::PaneGraphicsDirectParams { - pane_id: pane_id.into(), - layer_id: None, - z_index: 0, - owner: owner.into(), - image_width, - image_height, - format: api::schema::PaneGraphicsFormat::Rgba, - path, - sequence, - revision: 1, - placement: Default::default(), - }, - ), - }, - respond_to, - response_write_complete: None, - stream_active: None, - }, - response_rx, - ) -} - #[tokio::test] async fn pixel_mouse_activation_follows_child_1016_without_graphics_demand() { let (mut server, _client_rx, _pane_id) = @@ -614,7 +347,6 @@ async fn pixel_mouse_activation_follows_child_1016_without_graphics_demand() { }; client.host_mouse_capture_active = None; client.host_sgr_pixels_active = None; - server.app.direct_graphics_available = false; server.stream_host_mouse_capture_mode(); assert!(matches!( @@ -625,868 +357,3 @@ async fn pixel_mouse_activation_follows_child_1016_without_graphics_demand() { } )); } - -#[tokio::test] -async fn graphics_pruning_preserves_live_panes_and_removes_closed_panes() { - let (mut server, _client_rx, pane_id) = retained_test_server(b"aaaa"); - set_graphics_layer(&mut server, pane_id, vec![1, 2, 3]); - - assert!(!server - .app - .pane_graphics - .retain_live_panes(&server.app.state)); - assert!(server - .app - .pane_graphics - .slots - .contains_key(&graphics_key(pane_id))); - - server.app.state.workspaces.clear(); - assert!(server - .app - .pane_graphics - .retain_live_panes(&server.app.state)); - assert!(server.app.pane_graphics.slots.is_empty()); -} - -#[test] -fn stream_open_gate_is_owned_by_the_layer_and_cancels_on_removal() { - let mut server = test_headless_server(); - server.app.state.kitty_graphics_enabled = true; - let workspace = crate::workspace::Workspace::test_new("gated"); - let pane_id = workspace.tabs[0].root_pane; - let public = format!("{}:p1", workspace.id); - server.app.state.workspaces = vec![workspace]; - server.app.state.active = Some(0); - let active = active_gate(); - let (respond_to, response_rx) = std::sync::mpsc::channel(); - - server.handle_api_request_with_shutdown_check(api::ApiRequestMessage { - request: api::schema::Request { - id: "open-gated".into(), - method: api::schema::Method::PaneGraphicsStreamOpen( - api::schema::PaneGraphicsStreamParams { - pane_id: public.clone(), - layer_id: None, - z_index: 0, - owner: "worker-1".into(), - }, - ), - }, - respond_to, - response_write_complete: None, - stream_active: Some(active.clone()), - }); - assert!( - serde_json::from_str::<api::schema::SuccessResponse>(&response_rx.recv().unwrap()).is_ok() - ); - let (frame, frame_response) = - stream_set_message("gated-frame", &public, "worker-1", vec![1, 2, 3]); - assert_eq!( - server.handle_api_request_with_render_impact(frame), - RenderImpact::Graphics - ); - assert!(frame_response.recv().is_ok()); - assert!(active.load(std::sync::atomic::Ordering::Acquire)); - active.store(false, std::sync::atomic::Ordering::Release); - let (delayed, delayed_response) = - stream_set_message("delayed-frame", &public, "worker-1", vec![4, 5, 6]); - assert_eq!( - server.handle_api_request_with_render_impact(delayed), - RenderImpact::None - ); - let error: api::schema::ErrorResponse = - serde_json::from_str(&delayed_response.recv().unwrap()).unwrap(); - assert_eq!(error.error.code, "stream_closed"); - assert!(server - .app - .pane_graphics - .slots - .remove(&graphics_key(pane_id)) - .is_some()); - assert!(!active.load(std::sync::atomic::Ordering::Acquire)); -} - -#[test] -fn stream_set_has_graphics_only_render_impact() { - let mut server = test_headless_server(); - let workspace = crate::workspace::Workspace::test_new("graphics"); - let pane_id = workspace.tabs[0].root_pane; - let public_pane_id = format!("{}:p1", workspace.id); - server.app.state.workspaces = vec![workspace]; - server.app.state.active = Some(0); - server.app.state.selected = 0; - server.app.state.kitty_graphics_enabled = true; - set_stream_owner(&mut server, pane_id, "owner-a"); - - let (request, response_rx) = - stream_set_message("wrong-owner", &public_pane_id, "owner-b", vec![1, 2, 3]); - assert_eq!( - server.handle_api_request_with_render_impact(request), - RenderImpact::None - ); - assert!(serde_json::from_str::<api::schema::ErrorResponse>( - &response_rx - .recv_timeout(Duration::from_millis(100)) - .unwrap() - ) - .is_ok()); - - let (request, response_rx) = - stream_set_message("stream-frame", &public_pane_id, "owner-a", vec![1, 2, 3]); - assert_eq!( - server.handle_api_request_with_render_impact(request), - RenderImpact::Graphics - ); - assert!(serde_json::from_str::<api::schema::SuccessResponse>( - &response_rx - .recv_timeout(Duration::from_millis(100)) - .unwrap() - ) - .is_ok()); - - server - .app - .event_tx - .try_send(AppEvent::UpdateReady { - version: "9.9.9".into(), - install_command: "herdr update".into(), - }) - .unwrap(); - let (request, _response_rx) = stream_set_message( - "stream-frame-with-internal-event", - &public_pane_id, - "owner-a", - vec![4, 5, 6], - ); - assert_eq!( - server.handle_api_request_with_render_impact(request), - RenderImpact::Full - ); - - server.app.pane_graphics.clear(); - let (respond_to, _response_rx) = std::sync::mpsc::channel(); - let impact = server.handle_api_request_with_render_impact(api::ApiRequestMessage { - request: api::schema::Request { - id: "direct-frame".into(), - method: api::schema::Method::PaneGraphicsSet(api::schema::PaneGraphicsSetParams { - pane_id: public_pane_id, - layer_id: None, - z_index: 0, - owner: String::new(), - format: api::schema::PaneGraphicsFormat::Png, - image_width: 1, - image_height: 1, - data: Some(vec![1, 2, 3]), - data_base64: String::new(), - placement: api::schema::PaneGraphicsPlacementParams::default(), - }), - }, - respond_to, - response_write_complete: None, - stream_active: None, - }); - assert_eq!(impact, RenderImpact::Full); -} - -#[test] -fn rejected_or_stale_requests_do_not_schedule_rendering() { - let mut server = test_headless_server(); - let workspace = crate::workspace::Workspace::test_new("graphics"); - let pane_id = workspace.tabs[0].root_pane; - let public_pane_id = format!("{}:p1", workspace.id); - server.app.state.workspaces = vec![workspace]; - server.app.state.active = Some(0); - server.app.state.selected = 0; - server.app.state.kitty_graphics_enabled = false; - - let (respond_to, response_rx) = std::sync::mpsc::channel(); - let changed = server.handle_api_request_with_shutdown_check(api::ApiRequestMessage { - request: api::schema::Request { - id: "disabled-set".into(), - method: api::schema::Method::PaneGraphicsSet(api::schema::PaneGraphicsSetParams { - pane_id: public_pane_id.clone(), - layer_id: None, - z_index: 0, - owner: String::new(), - format: api::schema::PaneGraphicsFormat::Png, - image_width: 1, - image_height: 1, - data: Some(vec![1, 2, 3]), - data_base64: String::new(), - placement: api::schema::PaneGraphicsPlacementParams::default(), - }), - }, - respond_to, - response_write_complete: None, - stream_active: None, - }); - assert!(!changed); - let response = response_rx - .recv_timeout(Duration::from_millis(100)) - .unwrap(); - assert_eq!( - serde_json::from_str::<api::schema::ErrorResponse>(&response) - .unwrap() - .error - .code, - "feature_disabled" - ); - - server.app.state.kitty_graphics_enabled = true; - set_stream_owner(&mut server, pane_id, "current-owner"); - let (respond_to, response_rx) = std::sync::mpsc::channel(); - let impact = server.handle_api_request_with_render_impact(api::ApiRequestMessage { - request: api::schema::Request { - id: "stale-close".into(), - method: api::schema::Method::PaneGraphicsStreamClose( - api::schema::PaneGraphicsStreamParams { - pane_id: public_pane_id, - layer_id: None, - z_index: 0, - owner: "stale-owner".into(), - }, - ), - }, - respond_to, - response_write_complete: None, - stream_active: None, - }); - assert_eq!(impact, RenderImpact::None); - assert_eq!( - server - .app - .pane_graphics - .slots - .get(&graphics_key(pane_id)) - .and_then(|slot| slot.stream_owner.as_deref()), - Some("current-owner") - ); - assert!(serde_json::from_str::<api::schema::SuccessResponse>( - &response_rx - .recv_timeout(Duration::from_millis(100)) - .unwrap() - ) - .is_ok()); -} - -#[cfg(unix)] -#[tokio::test] -async fn direct_graphics_availability_follows_foreground_client_with_background_clients() { - let (mut server, _client_rx, _pane_id) = retained_test_server(b"direct eligibility"); - let foreground = server.clients.get_mut(&1).unwrap(); - foreground.direct_graphics = true; - foreground.pixel_mouse = true; - - let (background_writer, _background_control_rx, _background_render_rx) = test_client_writer(); - server.clients.insert( - 2, - ClientConnection::new( - (80, 24), - crate::kitty_graphics::HostCellSize::default(), - 2, - RenderEncoding::SemanticFrame, - Some(background_writer), - ), - ); - - server.sync_foreground_client_state(); - assert!(server.direct_graphics_available()); - assert!(server.app.direct_graphics_available); - - server.foreground_client_id = Some(2); - server.sync_foreground_client_state(); - assert!(!server.direct_graphics_available()); - assert!(!server.app.direct_graphics_available); -} - -#[cfg(unix)] -#[test] -fn direct_graphics_routing_prefers_the_target_stream_owner() { - let (mut server, first_key, response_rx) = direct_gate_server(&[1, 2, 3, 4]); - add_direct_client(&mut server, 7); - add_direct_client(&mut server, 8); - server.clients.get_mut(&8).unwrap().last_activity = 2; - let (transfer_id, image_id) = direct_ids(&server, &first_key); - assert!(server.complete_direct_graphics(7, transfer_id, image_id, true)); - assert_eq!(response_rx.recv().unwrap(), "ack"); - - let second_key = (first_key.0, "second".into()); - let second_layer = crate::app::pane_graphics::Layer { - format: crate::api::schema::PaneGraphicsFormat::Rgba, - image_width: 1, - image_height: 1, - backing: crate::app::pane_graphics::Backing::Resident { - len: 4, - client_id: 8, - }, - data_fingerprint: 2, - render: Default::default(), - z_index: 0, - }; - server.app.pane_graphics.slots.insert( - second_key.clone(), - crate::app::pane_graphics::Slot::test((1 << 31) | 901, Some(second_layer)), - ); - - assert_eq!(server.direct_graphics_client_for_key(&first_key), Some(7)); - assert_eq!(server.direct_graphics_client_for_key(&second_key), Some(8)); - assert_eq!( - server.direct_graphics_client_for_key(&(first_key.0, "new".into())), - Some(8) - ); -} - -#[cfg(unix)] -#[test] -fn resident_direct_stream_survives_non_direct_client_becoming_foreground() { - let (mut server, key, response_rx) = direct_gate_server(&[1, 2, 3, 4]); - add_direct_client(&mut server, 7); - server.foreground_client_id = Some(7); - server.sync_foreground_client_state(); - let (transfer_id, image_id) = direct_ids(&server, &key); - assert!(server.complete_direct_graphics(7, transfer_id, image_id, true)); - assert_eq!(response_rx.recv().unwrap(), "ack"); - - let (writer, _control_rx, _render_rx) = test_client_writer(); - server.clients.insert( - 8, - ClientConnection::new( - (80, 24), - crate::kitty_graphics::HostCellSize::default(), - 2, - RenderEncoding::SemanticFrame, - Some(writer), - ), - ); - server.foreground_client_id = Some(8); - server.sync_foreground_client_state(); - - assert!(server.direct_graphics_available()); - assert!(server.app.direct_graphics_available); - assert_eq!( - server.app.pane_graphics.slots[&key] - .layer - .as_ref() - .and_then(crate::app::pane_graphics::Layer::resident_client), - Some(7) - ); - - server.remove_client_and_resize_if_needed(7); - assert!(!server.app.pane_graphics.slots.contains_key(&key)); - assert!(!server.direct_graphics_available()); - assert!(!server.app.direct_graphics_available); -} - -#[cfg(unix)] -#[tokio::test] -async fn client_shell_direct_graphics_uploads_without_server_authored_coordinates() { - let (mut server, _control_rx, client_rx, pane_id) = - retained_test_server_with_control(b"client shell direct"); - server.app.state.kitty_graphics_enabled = true; - let client = server.clients.get_mut(&1).unwrap(); - client.mode = ClientConnectionMode::ClientShell; - client.render_state = - crate::server::render_stream::ClientRenderState::new(RenderEncoding::SemanticFrame); - client.cell_size = crate::kitty_graphics::HostCellSize { - width_px: 10, - height_px: 20, - }; - client.direct_graphics = true; - client.pixel_mouse = true; - server.app.direct_graphics_available = true; - let (background_writer, _background_control_rx, background_render_rx) = test_client_writer(); - server.clients.insert( - 2, - ClientConnection::new( - (80, 24), - crate::kitty_graphics::HostCellSize { - width_px: 10, - height_px: 20, - }, - 2, - RenderEncoding::SemanticFrame, - Some(background_writer), - ), - ); - set_stream_owner(&mut server, pane_id, "browser"); - let public_pane_id = server.app.public_pane_id(0, pane_id).unwrap(); - let path = sparse_direct_frame(&server, "client-shell-direct.rgba", 1, 1); - let (message, response_rx) = - direct_stream_message("shell-direct", &public_pane_id, "browser", path, 1, 1, 1); - - assert_eq!( - server.handle_pane_graphics_stream_frame(message), - RenderImpact::None - ); - let (transfer_id, image_id, asset) = match read_server_message( - client_rx - .recv_timeout(Duration::from_secs(1)) - .expect("client shell direct upload"), - ) { - ServerMessage::GraphicsFile { - transfer_id, - image_id, - leading, - control, - surface_asset: Some(asset), - .. - } => { - assert!(leading.is_empty()); - assert!(control.starts_with("a=t,"), "{control}"); - assert!(!control.contains("\u{1b}["), "{control}"); - (transfer_id, image_id, asset) - } - other => panic!("expected client shell graphics file, got {other:?}"), - }; - assert!(background_render_rx.try_recv().is_err()); - assert_eq!( - image_id, - crate::kitty_graphics::surface::host_image_id(&server.client_shell_boot_id, &asset) - ); - let (pending, _) = crate::server::client_shell_graphics::collect( - &server.app, - &[], - &[], - None, - Some(crate::ui::TabSurfaceTarget { - workspace_index: 0, - tab_index: 0, - }), - crate::kitty_graphics::HostCellSize { - width_px: 10, - height_px: 20, - }, - &crate::kitty_graphics::surface::DeliveryCache::default(), - 1, - ); - assert_eq!(pending.retained_assets, vec![asset.clone()]); - - server.start_direct_graphics_response(1, transfer_id, image_id); - assert!(server.complete_direct_graphics(1, transfer_id, image_id, true)); - assert!(serde_json::from_str::<api::schema::SuccessResponse>( - &response_rx.recv_timeout(Duration::from_secs(1)).unwrap() - ) - .is_ok()); - - server.foreground_client_id = Some(2); - server.sync_foreground_client_state(); - let next_path = sparse_direct_frame(&server, "client-shell-direct-next.rgba", 1, 1); - let (next_message, next_response_rx) = direct_stream_message( - "shell-direct-next", - &public_pane_id, - "browser", - next_path, - 1, - 1, - 2, - ); - assert_eq!( - server.handle_pane_graphics_stream_frame(next_message), - RenderImpact::None - ); - let (next_transfer_id, next_image_id, next_asset) = match read_server_message( - client_rx - .recv_timeout(Duration::from_secs(1)) - .expect("sticky owner direct upload"), - ) { - ServerMessage::GraphicsFile { - transfer_id, - image_id, - surface_asset: Some(asset), - .. - } => (transfer_id, image_id, asset), - other => panic!("expected sticky owner graphics file, got {other:?}"), - }; - assert!(background_render_rx.try_recv().is_err()); - server.start_direct_graphics_response(1, next_transfer_id, next_image_id); - assert!(server.complete_direct_graphics(1, next_transfer_id, next_image_id, true)); - assert!(serde_json::from_str::<api::schema::SuccessResponse>( - &next_response_rx - .recv_timeout(Duration::from_secs(1)) - .unwrap() - ) - .is_ok()); - assert!(server.app.direct_graphics_available); - - server.clients.get_mut(&1).unwrap().request_repaint(); - server.render_and_stream(); - let surface = read_server_message( - client_rx - .recv_timeout(Duration::from_secs(1)) - .expect("resident client shell scene"), - ); - let ServerMessage::PaneSurface(surface) = surface else { - panic!("expected client shell pane surface"); - }; - assert_eq!(surface.graphics.placements.len(), 1); - assert!(surface.graphics.assets.is_empty()); - assert_eq!(surface.graphics.placements[0].asset, next_asset); - assert_eq!(surface.graphics.retained_assets, vec![next_asset.clone()]); - - let background_surface = read_server_message( - background_render_rx - .recv_timeout(Duration::from_secs(1)) - .expect("non-owning client shell scene"), - ); - let ServerMessage::PaneSurface(background_surface) = background_surface else { - panic!("expected non-owning client shell pane surface"); - }; - assert!(background_surface.graphics.assets.is_empty()); - assert!(background_surface.graphics.placements.is_empty()); - assert!(background_surface.graphics.retained_assets.is_empty()); - - let (hidden, _) = crate::server::client_shell_graphics::collect( - &server.app, - &[], - &[], - None, - Some(crate::ui::TabSurfaceTarget { - workspace_index: 0, - tab_index: 0, - }), - crate::kitty_graphics::HostCellSize { - width_px: 10, - height_px: 20, - }, - &crate::kitty_graphics::surface::DeliveryCache::default(), - 1, - ); - assert!(hidden.placements.is_empty()); - assert_eq!(hidden.retained_assets, vec![next_asset]); -} - -#[cfg(unix)] -fn direct_gate_server( - data: &[u8], -) -> ( - HeadlessServer, - crate::app::pane_graphics::Key, - std::sync::mpsc::Receiver<String>, -) { - direct_gate_server_with_file(data.len(), Some(data)) -} - -#[cfg(unix)] -fn direct_gate_server_with_file( - len: usize, - data: Option<&[u8]>, -) -> ( - HeadlessServer, - crate::app::pane_graphics::Key, - std::sync::mpsc::Receiver<String>, -) { - use std::io::Write as _; - use std::os::unix::fs::OpenOptionsExt as _; - let mut server = test_headless_server(); - let workspace = crate::workspace::Workspace::test_new("direct-gate"); - let pane_id = workspace.tabs[0].root_pane; - server.app.state.workspaces = vec![workspace]; - server.app.state.active = Some(0); - let key = graphics_key(pane_id); - let path = server - .app - .pane_graphics_files - .source_directory() - .unwrap() - .join("gate-frame"); - let mut file = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) - .open(&path) - .unwrap(); - if let Some(data) = data { - file.write_all(data).unwrap(); - } else { - file.set_len(len as u64).unwrap(); - } - drop(file); - let lease = server.app.pane_graphics_files.lease(&path, len).unwrap(); - let (respond_to, response_rx) = std::sync::mpsc::channel(); - let layer = - crate::app::pane_graphics::Layer::direct(1, 1, lease.clone(), Default::default(), 0); - let mut slot = crate::app::pane_graphics::Slot::test((1 << 31) | 900, Some(layer)); - slot.stream_owner = Some("owner".into()); - slot.stream_active = Some(active_gate()); - slot.direct_gate = Some(crate::app::pane_graphics::DirectGate { - transfer_id: lease.fingerprint(), - image_id: (1 << 31) | 900, - client_id: 7, - deadline: std::time::Instant::now() + Duration::from_secs(1), - written: true, - success_response: "ack".into(), - respond_to, - }); - server.app.pane_graphics.slots.insert(key.clone(), slot); - (server, key, response_rx) -} - -#[cfg(unix)] -fn direct_ids(server: &HeadlessServer, key: &crate::app::pane_graphics::Key) -> (u64, u32) { - let slot = &server.app.pane_graphics.slots[key]; - let gate = slot.direct_gate.as_ref().unwrap(); - (gate.transfer_id, gate.image_id) -} - -#[cfg(unix)] -fn add_direct_client(server: &mut HeadlessServer, client_id: u64) { - let (writer, control_rx, render_rx) = test_client_writer(); - std::mem::forget((control_rx, render_rx)); - let mut client = ClientConnection::new( - (80, 24), - crate::kitty_graphics::HostCellSize { - width_px: 10, - height_px: 20, - }, - 1, - RenderEncoding::SemanticFrame, - Some(writer), - ); - client.direct_graphics = true; - client.pixel_mouse = true; - server.clients.insert(client_id, client); -} - -#[cfg(unix)] -#[test] -fn terminal_response_deadline_starts_only_after_client_flush() { - let (mut server, key, _response_rx) = direct_gate_server(&[1, 2, 3, 4]); - let slot = server.app.pane_graphics.slots.get_mut(&key).unwrap(); - let gate = slot.direct_gate.as_mut().unwrap(); - gate.written = false; - let (transfer_id, image_id) = (gate.transfer_id, slot.host_image_id); - assert!(!server.complete_direct_graphics(7, transfer_id, image_id, true)); - assert!(!server.start_direct_graphics_response(7, transfer_id, image_id)); - let gate = server.app.pane_graphics.slots[&key] - .direct_gate - .as_ref() - .unwrap(); - assert!(gate.written && gate.deadline > std::time::Instant::now()); -} - -#[cfg(unix)] -#[test] -fn outer_timeout_covers_both_direct_phases_and_cancellation_blocks_late_results() { - assert!( - crate::app::pane_graphics::DIRECT_OUTER_TIMEOUT - > crate::app::pane_graphics::DIRECT_DELIVERY_TIMEOUT - + crate::app::pane_graphics::DIRECT_RESPONSE_TIMEOUT - ); - let (mut server, key, response_rx) = direct_gate_server(&[1, 2, 3, 4]); - let slot = server.app.pane_graphics.slots.get_mut(&key).unwrap(); - slot.stream_active - .as_ref() - .unwrap() - .store(false, std::sync::atomic::Ordering::Release); - let (transfer_id, image_id) = ( - slot.direct_gate.as_ref().unwrap().transfer_id, - slot.host_image_id, - ); - - assert!(!server.complete_direct_graphics(7, transfer_id, image_id, true)); - assert!(response_rx.try_recv().is_err()); - assert!(server.app.pane_graphics.slots[&key] - .layer - .as_ref() - .unwrap() - .direct_lease() - .is_some()); -} - -#[cfg(unix)] -#[test] -fn matching_terminal_ok_releases_producer_and_acknowledges() { - let (mut server, key, response_rx) = direct_gate_server(&[1, 2, 3, 4]); - let (transfer_id, image_id) = direct_ids(&server, &key); - - assert!(server.complete_direct_graphics(7, transfer_id, image_id, true)); - - assert_eq!(response_rx.recv().unwrap(), "ack"); - let layer = server.app.pane_graphics.slots[&key].layer.as_ref().unwrap(); - assert!(layer.terminal_only()); - assert!(layer.direct_lease().is_none()); -} - -#[cfg(unix)] -#[test] -fn unwritten_direct_full_falls_back_without_stickiness_but_disconnect_retires() { - for error in [ - std::sync::mpsc::TrySendError::Full(Vec::new()), - std::sync::mpsc::TrySendError::Disconnected(Vec::new()), - ] { - let should_ack = matches!(error, std::sync::mpsc::TrySendError::Full(_)); - let (mut server, key, response_rx) = direct_gate_server(&[1, 2, 3, 4]); - add_direct_client(&mut server, 7); - let gate = server - .app - .pane_graphics - .slots - .get_mut(&key) - .and_then(|slot| slot.direct_gate.take()) - .unwrap(); - let result = server.handle_unwritten_direct_failure( - &key, - gate.success_response, - gate.respond_to, - error, - ); - let inline = server - .app - .pane_graphics - .slots - .get(&key) - .and_then(|slot| slot.layer.as_ref()?.inline_data()) - .is_some(); - assert_eq!( - ( - result, - response_rx.try_recv().ok().as_deref() == Some("ack"), - inline, - server.clients[&7].direct_graphics, - ), - (should_ack, should_ack, should_ack, true) - ); - } -} - -#[cfg(unix)] -#[test] -fn eligibility_loss_cancels_the_queued_direct_upload() { - let (mut server, key, response_rx) = direct_gate_server(&[1, 2, 3, 4]); - let (writer, control_rx, _render_rx) = test_client_writer(); - let mut client = ClientConnection::new( - (80, 24), - crate::kitty_graphics::HostCellSize { - width_px: 10, - height_px: 20, - }, - 1, - RenderEncoding::SemanticFrame, - Some(writer), - ); - client.direct_graphics = true; - client.pixel_mouse = true; - server.clients.insert(7, client); - let (transfer_id, image_id) = direct_ids(&server, &key); - - server.retire_all_direct_graphics(); - - assert!(matches!( - read_server_message( - control_rx - .recv_timeout(Duration::from_millis(100)) - .expect("direct retirement") - ), - ServerMessage::GraphicsTransmissionRetired { - transfer_id: retired_transfer, - image_id: retired_image, - } if retired_transfer == transfer_id && retired_image == image_id - )); - assert!(!server.app.pane_graphics.slots.contains_key(&key)); - assert!(response_rx.recv().is_err()); -} - -#[cfg(unix)] -#[test] -fn client_loss_retires_only_its_direct_stream() { - let (mut pending, key, response_rx) = direct_gate_server(&[1, 2, 3, 4]); - pending.retire_direct_graphics_for_client(8); - assert!(pending.app.pane_graphics.slots.contains_key(&key)); - pending.retire_direct_graphics_for_client(7); - assert!(!pending.app.pane_graphics.slots.contains_key(&key)); - assert!(response_rx.recv().is_err()); - - let (mut resident, key, response_rx) = direct_gate_server(&[1, 2, 3, 4]); - let slot = resident.app.pane_graphics.slots.get(&key).unwrap(); - assert!(resident.complete_direct_graphics( - 7, - slot.direct_gate.as_ref().unwrap().transfer_id, - slot.host_image_id, - true, - )); - assert_eq!(response_rx.recv().unwrap(), "ack"); - resident.retire_direct_graphics_for_client(8); - assert!(resident.app.pane_graphics.slots.contains_key(&key)); - resident.retire_direct_graphics_for_client(7); - assert!(!resident.app.pane_graphics.slots.contains_key(&key)); -} - -#[cfg(unix)] -#[test] -fn pane_removal_cancels_the_pending_client_upload() { - let (mut server, key, response_rx) = direct_gate_server(&[1, 2, 3, 4]); - let (writer, control_rx, _render_rx) = test_client_writer(); - let mut client = ClientConnection::new( - (80, 24), - crate::kitty_graphics::HostCellSize { - width_px: 10, - height_px: 20, - }, - 1, - RenderEncoding::SemanticFrame, - Some(writer), - ); - client.direct_graphics = true; - client.pixel_mouse = true; - server.clients.insert(7, client); - let (transfer_id, image_id) = direct_ids(&server, &key); - server.app.state.workspaces.clear(); - - assert!(server.retain_live_pane_graphics()); - - assert!(matches!( - read_server_message( - control_rx - .recv_timeout(Duration::from_millis(100)) - .expect("pane removal retirement") - ), - ServerMessage::GraphicsTransmissionRetired { - transfer_id: retired_transfer, - image_id: retired_image, - } if retired_transfer == transfer_id && retired_image == image_id - )); - assert!(!server.app.pane_graphics.slots.contains_key(&key)); - assert!(response_rx.recv().is_err()); -} - -#[cfg(unix)] -#[test] -fn pane_removal_and_shutdown_drop_direct_without_ack() { - let setups: [fn(&mut HeadlessServer); 2] = [ - |server| server.app.state.workspaces.clear(), - |server| server.shutting_down = true, - ]; - for setup in setups { - let (mut server, key, response_rx) = direct_gate_server(&[1, 2, 3, 4]); - let (transfer_id, image_id) = direct_ids(&server, &key); - setup(&mut server); - assert!(!server.complete_direct_graphics(7, transfer_id, image_id, true)); - assert!(response_rx.recv().is_err()); - assert!(!server.app.pane_graphics.slots.contains_key(&key)); - } -} - -#[cfg(unix)] -#[test] -fn timeout_retires_stream_without_producer_ack() { - let (mut server, key, response_rx) = direct_gate_server(&[1, 2, 3, 4]); - add_direct_client(&mut server, 7); - server - .app - .pane_graphics - .slots - .get_mut(&key) - .unwrap() - .direct_gate - .as_mut() - .unwrap() - .deadline = std::time::Instant::now() - Duration::from_millis(1); - - assert!(server.expire_direct_graphics(std::time::Instant::now())); - - assert!(response_rx.recv().is_err()); - assert!(!server.app.pane_graphics.slots.contains_key(&key)); - assert!(!server.clients[&7].direct_graphics); - assert!(server.clients[&7].pixel_mouse); -} |
