reovim_testing/
multi_client.rs1#![allow(clippy::missing_errors_doc)]
12#![allow(clippy::missing_panics_doc)]
13
14use std::{
15 io::Write,
16 sync::atomic::{AtomicU32, Ordering},
17 time::Duration,
18};
19
20use reovim_client_cli::GrpcClient;
21
22use super::harness::TestServerHarness;
23
24static MULTI_CLIENT_FILE_COUNTER: AtomicU32 = AtomicU32::new(0);
26
27pub struct TestClient {
31 addr: String,
32 id: usize,
33 client: Option<GrpcClient>,
34}
35
36#[cfg_attr(coverage_nightly, coverage(off))]
37impl TestClient {
38 async fn get_client(&mut self) -> Result<&mut GrpcClient, String> {
40 if self.client.is_none() {
41 self.client = Some(Self::connect_with_retry(&self.addr).await?);
42 }
43 Ok(self.client.as_mut().unwrap())
44 }
45
46 async fn connect_with_retry(addr: &str) -> Result<GrpcClient, String> {
48 let mut attempts = 0;
49 loop {
50 match GrpcClient::connect(addr).await {
51 Ok(c) => return Ok(c),
52 Err(_) if attempts < 20 => {
53 attempts += 1;
54 tokio::time::sleep(Duration::from_millis(50)).await;
55 }
56 Err(e) => return Err(format!("Failed to connect: {e}")),
57 }
58 }
59 }
60
61 #[allow(clippy::significant_drop_tightening)]
63 pub async fn send_keys(&mut self, keys: &str) -> Result<(), String> {
64 let client = self.get_client().await?;
65 client.send_keys(keys).await.map_err(|e| e.to_string())?;
66 Ok(())
67 }
68
69 #[allow(clippy::cast_possible_truncation, clippy::significant_drop_tightening)]
71 pub async fn get_cursor(&mut self) -> Result<(u16, u16), String> {
72 let client = self.get_client().await?;
73 let result = client.get_cursor().await.map_err(|e| e.to_string())?;
74 let (line, col) = result.position.map_or((0, 0), |pos| (pos.line, pos.column));
75 Ok((line as u16, col as u16))
76 }
77
78 #[allow(clippy::significant_drop_tightening)]
80 pub async fn get_buffer(&mut self) -> Result<String, String> {
81 let client = self.get_client().await?;
82 let result = client
83 .get_buffer_content(None)
84 .await
85 .map_err(|e| e.to_string())?;
86 Ok(result.lines.join("\n"))
87 }
88
89 #[allow(clippy::significant_drop_tightening)]
91 pub async fn open_buffer(&mut self, content: &str) -> Result<(), String> {
92 let path = format!("/tmp/reovim-multiclient-{}-{}.txt", std::process::id(), self.id);
93 let mut file = std::fs::File::create(&path).map_err(|e| e.to_string())?;
94 file.write_all(content.as_bytes())
95 .map_err(|e| e.to_string())?;
96
97 let client = self.get_client().await?;
98 client
99 .send_keys(&format!(":e {path}<CR>"))
100 .await
101 .map_err(|e| e.to_string())?;
102
103 Ok(())
104 }
105
106 #[must_use]
108 pub const fn id(&self) -> usize {
109 self.id
110 }
111}
112
113pub struct MultiClientTest {
129 harness: TestServerHarness,
130 client_count: usize,
131 initial_content: Option<String>,
132}
133
134#[cfg_attr(coverage_nightly, coverage(off))]
135impl MultiClientTest {
136 pub async fn with_clients(n: usize) -> Self {
144 let test_name = std::thread::current()
146 .name()
147 .unwrap_or("unknown_multi_test")
148 .to_string();
149
150 Self {
151 harness: TestServerHarness::spawn_with_name(&format!("{test_name}_server"))
152 .await
153 .expect("Failed to spawn server"),
154 client_count: n,
155 initial_content: None,
156 }
157 }
158
159 #[must_use]
161 pub fn log_path(&self) -> Option<&std::path::Path> {
162 self.harness.log_path()
163 }
164
165 #[must_use]
167 pub fn with_buffer(mut self, content: &str) -> Self {
168 self.initial_content = Some(content.to_string());
169 self
170 }
171
172 #[allow(clippy::significant_drop_tightening)]
174 pub async fn run<F, Fut>(self, test_fn: F)
175 where
176 F: FnOnce(Vec<TestClient>) -> Fut,
177 Fut: std::future::Future<Output = ()>,
178 {
179 let addr = format!("127.0.0.1:{}", self.harness.port());
180
181 let mut clients = Vec::with_capacity(self.client_count);
183 for id in 0..self.client_count {
184 clients.push(TestClient {
185 addr: addr.clone(),
186 id,
187 client: None,
188 });
189 }
190
191 for client in &mut clients {
193 assert!(
194 TestClient::connect_with_retry(&client.addr).await.is_ok(),
195 "Client {} failed to connect",
196 client.id
197 );
198 }
199
200 if self.initial_content.is_some() || !clients.is_empty() {
202 let temp_path = {
203 let id = MULTI_CLIENT_FILE_COUNTER.fetch_add(1, Ordering::SeqCst);
204 let path = format!("/tmp/reovim-multi-test-{}-{id}.txt", std::process::id());
205 let content = self.initial_content.as_deref().unwrap_or("");
206 let mut file = std::fs::File::create(&path).expect("Failed to create temp file");
207 file.write_all(content.as_bytes())
208 .expect("Failed to write temp file");
209 path
210 };
211 clients[0]
212 .send_keys(&format!(":e {temp_path}<CR>"))
213 .await
214 .expect("Failed to open buffer file");
215 tokio::time::sleep(Duration::from_millis(10)).await;
216 }
217
218 test_fn(clients).await;
219 }
221}