#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
use std::time::Duration;
use {reovim_client_cli::GrpcClient, reovim_protocol::v2::ClientPresence};
use super::harness::TestServerHarness;
pub struct PresenceTestClient {
client: GrpcClient,
client_id: Option<u64>,
display_name: String,
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl PresenceTestClient {
pub fn new(client: GrpcClient, display_name: &str) -> Self {
Self {
client,
client_id: None,
display_name: display_name.to_string(),
}
}
pub async fn join(&mut self) -> Result<reovim_protocol::v2::JoinResponse, String> {
let response = self
.client
.presence_join("test", &self.display_name)
.await
.map_err(|e| format!("Join failed: {e}"))?;
self.client_id = Some(response.client_id);
Ok(response)
}
pub async fn leave(&mut self) -> Result<(), String> {
if self.client_id.take().is_some() {
self.client
.presence_leave()
.await
.map_err(|e| format!("Leave failed: {e}"))?;
}
Ok(())
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn client_id(&self) -> u64 {
self.client_id
.expect("Client not joined - call join() first")
}
#[must_use]
pub const fn is_joined(&self) -> bool {
self.client_id.is_some()
}
#[must_use]
pub fn display_name(&self) -> &str {
&self.display_name
}
#[allow(clippy::unused_async)] pub async fn update_cursor(&mut self, _line: u64, _col: u64) -> Result<(), String> {
Ok(())
}
pub async fn update_buffer(&mut self, buffer_id: u64) -> Result<(), String> {
self.client
.presence_update(Some(buffer_id), None)
.await
.map_err(|e| format!("Update buffer failed: {e}"))?;
Ok(())
}
pub async fn update_mode(&mut self, mode: &str) -> Result<(), String> {
self.client
.presence_update(None, Some(mode.to_string()))
.await
.map_err(|e| format!("Update mode failed: {e}"))?;
Ok(())
}
pub async fn follow(&mut self, target_id: u64) -> Result<(), String> {
self.client
.presence_set_sync_mode(1, Some(target_id))
.await
.map_err(|e| format!("Set follow mode failed: {e}"))?;
Ok(())
}
pub async fn present(&mut self) -> Result<(), String> {
self.client
.presence_set_sync_mode(2, None)
.await
.map_err(|e| format!("Set present mode failed: {e}"))?;
Ok(())
}
pub async fn independent(&mut self) -> Result<(), String> {
self.client
.presence_set_sync_mode(0, None)
.await
.map_err(|e| format!("Set independent mode failed: {e}"))?;
Ok(())
}
pub async fn peers(&mut self) -> Result<Vec<ClientPresence>, String> {
let response = self
.client
.presence_list()
.await
.map_err(|e| format!("List peers failed: {e}"))?;
let my_id = self.client_id;
Ok(response
.clients
.into_iter()
.filter(|c| Some(c.client_id) != my_id)
.collect())
}
pub async fn all_clients(&mut self) -> Result<Vec<ClientPresence>, String> {
let response = self
.client
.presence_list()
.await
.map_err(|e| format!("List clients failed: {e}"))?;
Ok(response.clients)
}
#[allow(clippy::missing_const_for_fn)] pub fn grpc(&mut self) -> &mut GrpcClient {
&mut self.client
}
pub async fn send_keys(&mut self, keys: &str) -> Result<(), String> {
self.client
.send_keys(keys)
.await
.map_err(|e| format!("Send keys failed: {e}"))?;
Ok(())
}
pub async fn get_mode(&mut self) -> Result<reovim_protocol::v2::GetModeResponse, String> {
let id = self.client_id();
self.client
.get_mode_for_client(id)
.await
.map_err(|e| format!("Get mode failed: {e}"))
}
pub async fn get_cursor(&mut self) -> Result<(u64, u64), String> {
let id = self.client_id();
let response = self
.client
.get_cursor_for_client(id)
.await
.map_err(|e| format!("Get cursor failed: {e}"))?;
let pos = response.position.ok_or("No position in response")?;
Ok((pos.line, pos.column))
}
pub async fn get_buffer(&mut self) -> Result<String, String> {
let response = self
.client
.get_buffer_content(None)
.await
.map_err(|e| format!("Get buffer failed: {e}"))?;
Ok(response.lines.join("\n"))
}
}
pub struct MultiClientPresenceTest {
harness: TestServerHarness,
client_count: usize,
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl MultiClientPresenceTest {
pub async fn with_clients(n: usize) -> Self {
let test_name = std::thread::current()
.name()
.unwrap_or("unknown_presence_test")
.to_string();
Self {
harness: TestServerHarness::spawn_with_name(&format!("{test_name}_presence"))
.await
.expect("Failed to spawn server"),
client_count: n,
}
}
#[must_use]
pub fn log_path(&self) -> Option<&std::path::Path> {
self.harness.log_path()
}
#[must_use]
#[allow(clippy::missing_const_for_fn)] pub fn port(&self) -> u16 {
self.harness.port()
}
#[allow(clippy::significant_drop_tightening)]
pub async fn run<F, Fut>(self, test_fn: F)
where
F: FnOnce(Vec<PresenceTestClient>) -> Fut,
Fut: std::future::Future<Output = ()>,
{
let addr = format!("127.0.0.1:{}", self.harness.port());
let mut clients = Vec::with_capacity(self.client_count);
for i in 0..self.client_count {
let grpc = Self::connect_with_retry(&addr)
.await
.expect("Failed to connect");
let mut client = PresenceTestClient::new(grpc, &format!("client_{i}"));
client.join().await.expect("Failed to join presence");
tokio::time::sleep(Duration::from_millis(5)).await;
clients.push(client);
}
test_fn(clients).await;
}
async fn connect_with_retry(addr: &str) -> Result<GrpcClient, String> {
let mut attempts = 0;
loop {
match GrpcClient::connect(addr).await {
Ok(c) => return Ok(c),
Err(_) if attempts < 20 => {
attempts += 1;
tokio::time::sleep(Duration::from_millis(50)).await;
}
Err(e) => return Err(format!("Failed to connect after {attempts} attempts: {e}")),
}
}
}
}