#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
use std::{
io::Write,
sync::atomic::{AtomicU32, Ordering},
time::Duration,
};
use reovim_client_cli::GrpcClient;
use super::harness::TestServerHarness;
static MULTI_CLIENT_FILE_COUNTER: AtomicU32 = AtomicU32::new(0);
pub struct TestClient {
addr: String,
id: usize,
client: Option<GrpcClient>,
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl TestClient {
async fn get_client(&mut self) -> Result<&mut GrpcClient, String> {
if self.client.is_none() {
self.client = Some(Self::connect_with_retry(&self.addr).await?);
}
Ok(self.client.as_mut().unwrap())
}
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: {e}")),
}
}
}
#[allow(clippy::significant_drop_tightening)]
pub async fn send_keys(&mut self, keys: &str) -> Result<(), String> {
let client = self.get_client().await?;
client.send_keys(keys).await.map_err(|e| e.to_string())?;
Ok(())
}
#[allow(clippy::cast_possible_truncation, clippy::significant_drop_tightening)]
pub async fn get_cursor(&mut self) -> Result<(u16, u16), String> {
let client = self.get_client().await?;
let result = client.get_cursor().await.map_err(|e| e.to_string())?;
let (line, col) = result.position.map_or((0, 0), |pos| (pos.line, pos.column));
Ok((line as u16, col as u16))
}
#[allow(clippy::significant_drop_tightening)]
pub async fn get_buffer(&mut self) -> Result<String, String> {
let client = self.get_client().await?;
let result = client
.get_buffer_content(None)
.await
.map_err(|e| e.to_string())?;
Ok(result.lines.join("\n"))
}
#[allow(clippy::significant_drop_tightening)]
pub async fn open_buffer(&mut self, content: &str) -> Result<(), String> {
let path = format!("/tmp/reovim-multiclient-{}-{}.txt", std::process::id(), self.id);
let mut file = std::fs::File::create(&path).map_err(|e| e.to_string())?;
file.write_all(content.as_bytes())
.map_err(|e| e.to_string())?;
let client = self.get_client().await?;
client
.send_keys(&format!(":e {path}<CR>"))
.await
.map_err(|e| e.to_string())?;
Ok(())
}
#[must_use]
pub const fn id(&self) -> usize {
self.id
}
}
pub struct MultiClientTest {
harness: TestServerHarness,
client_count: usize,
initial_content: Option<String>,
}
#[cfg_attr(coverage_nightly, coverage(off))]
impl MultiClientTest {
pub async fn with_clients(n: usize) -> Self {
let test_name = std::thread::current()
.name()
.unwrap_or("unknown_multi_test")
.to_string();
Self {
harness: TestServerHarness::spawn_with_name(&format!("{test_name}_server"))
.await
.expect("Failed to spawn server"),
client_count: n,
initial_content: None,
}
}
#[must_use]
pub fn log_path(&self) -> Option<&std::path::Path> {
self.harness.log_path()
}
#[must_use]
pub fn with_buffer(mut self, content: &str) -> Self {
self.initial_content = Some(content.to_string());
self
}
#[allow(clippy::significant_drop_tightening)]
pub async fn run<F, Fut>(self, test_fn: F)
where
F: FnOnce(Vec<TestClient>) -> 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 id in 0..self.client_count {
clients.push(TestClient {
addr: addr.clone(),
id,
client: None,
});
}
for client in &mut clients {
assert!(
TestClient::connect_with_retry(&client.addr).await.is_ok(),
"Client {} failed to connect",
client.id
);
}
if self.initial_content.is_some() || !clients.is_empty() {
let temp_path = {
let id = MULTI_CLIENT_FILE_COUNTER.fetch_add(1, Ordering::SeqCst);
let path = format!("/tmp/reovim-multi-test-{}-{id}.txt", std::process::id());
let content = self.initial_content.as_deref().unwrap_or("");
let mut file = std::fs::File::create(&path).expect("Failed to create temp file");
file.write_all(content.as_bytes())
.expect("Failed to write temp file");
path
};
clients[0]
.send_keys(&format!(":e {temp_path}<CR>"))
.await
.expect("Failed to open buffer file");
tokio::time::sleep(Duration::from_millis(10)).await;
}
test_fn(clients).await;
}
}