1#![allow(clippy::missing_errors_doc)]
29#![allow(clippy::missing_panics_doc)]
30
31use std::{
32 collections::HashMap,
33 io::Write,
34 sync::atomic::{AtomicU32, Ordering},
35 time::Duration,
36};
37
38use reovim_client_cli::GrpcClient;
39
40use super::{harness::TestServerHarness, integration::RegisterInfo};
41
42static STEP_TEMP_FILE_COUNTER: AtomicU32 = AtomicU32::new(0);
44
45const MAX_CONNECT_ATTEMPTS: u32 = 20;
47const RETRY_BASE_MS: u64 = 50;
48const RETRY_MAX_MS: u64 = 200;
49
50const DEFAULT_STEP_DELAY_MS: u64 = 30;
52
53#[derive(Debug, Clone)]
55pub struct StateSnapshot {
56 pub key: String,
58 pub buffer: String,
60 pub cursor_line: u16,
62 pub cursor_column: u16,
64 pub mode_display: String,
66 pub edit_mode: String,
68 pub registers: HashMap<String, RegisterInfo>,
70}
71
72#[cfg_attr(coverage_nightly, coverage(off))]
73impl StateSnapshot {
74 #[must_use]
76 pub fn compact(&self) -> String {
77 format!(
78 "{} -> {} ({}:{})",
79 self.key, self.mode_display, self.cursor_line, self.cursor_column
80 )
81 }
82
83 #[must_use]
85 pub fn verbose(&self) -> String {
86 let buf_preview = if self.buffer.len() > 40 {
87 format!("{}...", &self.buffer[..40])
88 } else {
89 self.buffer.clone()
90 };
91 format!(
92 "{} -> {} ({}:{}) buf={:?}",
93 self.key, self.mode_display, self.cursor_line, self.cursor_column, buf_preview
94 )
95 }
96}
97
98#[derive(Debug, Clone)]
100enum StepExpectation {
101 Cursor(u16, u16),
103 Buffer(String),
105 BufferContains(String),
107 ModeContains(String),
109 EditMode(String),
111 Register(String, String, String),
113}
114
115struct TestStep {
117 keys: String,
119 delay_ms: u64,
121 expectations: Vec<StepExpectation>,
123}
124
125pub struct StepTest {
143 harness: TestServerHarness,
144 addr: String,
145 initial_content: Option<String>,
146 initial_cursor: Option<(u16, u16)>,
147 steps: Vec<TestStep>,
148 default_delay: u64,
149}
150
151#[cfg_attr(coverage_nightly, coverage(off))]
152impl StepTest {
153 pub async fn new() -> Self {
162 let harness = TestServerHarness::spawn()
163 .await
164 .expect("Failed to spawn server");
165 let addr = format!("127.0.0.1:{}", harness.port());
166 Self {
167 harness,
168 addr,
169 initial_content: None,
170 initial_cursor: None,
171 steps: Vec::new(),
172 default_delay: DEFAULT_STEP_DELAY_MS,
173 }
174 }
175
176 pub async fn with_modules(modules: &[&str]) -> Self {
184 let harness = TestServerHarness::spawn_with_modules(modules)
185 .await
186 .expect("Failed to spawn server with extra modules");
187 let addr = format!("127.0.0.1:{}", harness.port());
188 Self {
189 harness,
190 addr,
191 initial_content: None,
192 initial_cursor: None,
193 steps: Vec::new(),
194 default_delay: DEFAULT_STEP_DELAY_MS,
195 }
196 }
197
198 pub async fn with_env(env_vars: &[(&str, &str)]) -> Self {
206 let harness = TestServerHarness::spawn_with_env(env_vars)
207 .await
208 .expect("Failed to spawn server with env vars");
209 let addr = format!("127.0.0.1:{}", harness.port());
210 Self {
211 harness,
212 addr,
213 initial_content: None,
214 initial_cursor: None,
215 steps: Vec::new(),
216 default_delay: DEFAULT_STEP_DELAY_MS,
217 }
218 }
219
220 #[must_use]
224 pub fn log_path(&self) -> Option<&std::path::Path> {
225 self.harness.log_path()
226 }
227
228 async fn connect_with_retry(&self) -> Result<GrpcClient, String> {
230 let mut attempts = 0;
231 loop {
232 match GrpcClient::connect(&self.addr).await {
233 Ok(c) => return Ok(c),
234 Err(_) if attempts < MAX_CONNECT_ATTEMPTS => {
235 attempts += 1;
236 let delay = std::cmp::min(
237 RETRY_BASE_MS + u64::from(attempts) * RETRY_BASE_MS,
238 RETRY_MAX_MS,
239 );
240 tokio::time::sleep(Duration::from_millis(delay)).await;
241 }
242 Err(e) => {
243 return Err(format!(
244 "Failed to connect after {MAX_CONNECT_ATTEMPTS} attempts: {e}"
245 ));
246 }
247 }
248 }
249 }
250
251 async fn capture_state(&self, client: &mut GrpcClient, key: &str) -> StateSnapshot {
253 let buffer_response = client
254 .get_buffer_content(None)
255 .await
256 .expect("Failed to get buffer content");
257 let cursor_response = client.get_cursor().await.expect("Failed to get cursor");
258 let mode_response = client.get_mode().await.expect("Failed to get mode");
259 let register_response = client
260 .get_registers(vec![])
261 .await
262 .expect("Failed to get registers");
263
264 let (cursor_line, cursor_column) = cursor_response
266 .position
267 .map_or((0, 0), |pos| (pos.line, pos.column));
268
269 let registers = register_response
271 .registers
272 .into_iter()
273 .map(|entry| {
274 (
275 entry.name,
276 RegisterInfo {
277 content: entry.content,
278 yank_type: entry.yank_type,
279 },
280 )
281 })
282 .collect();
283
284 #[allow(clippy::cast_possible_truncation)]
285 StateSnapshot {
286 key: key.to_string(),
287 buffer: buffer_response.lines.join("\n"),
288 cursor_line: cursor_line as u16,
289 cursor_column: cursor_column as u16,
290 mode_display: mode_response.display,
291 edit_mode: mode_response.name,
292 registers,
293 }
294 }
295
296 #[must_use]
298 pub fn with_buffer(mut self, content: &str) -> Self {
299 self.initial_content = Some(content.to_string());
300 self
301 }
302
303 #[must_use]
305 #[allow(clippy::missing_const_for_fn)]
306 pub fn with_cursor_at(mut self, line: u16, col: u16) -> Self {
307 self.initial_cursor = Some((line, col));
308 self
309 }
310
311 #[must_use]
313 #[allow(clippy::missing_const_for_fn)]
314 pub fn with_delay(mut self, ms: u64) -> Self {
315 self.default_delay = ms;
316 self
317 }
318
319 #[must_use]
321 pub fn step(mut self, keys: &str) -> Self {
322 self.steps.push(TestStep {
323 keys: keys.to_string(),
324 delay_ms: self.default_delay,
325 expectations: Vec::new(),
326 });
327 self
328 }
329
330 #[must_use]
332 pub fn expect_cursor(mut self, line: u16, col: u16) -> Self {
333 if let Some(step) = self.steps.last_mut() {
334 step.expectations.push(StepExpectation::Cursor(line, col));
335 }
336 self
337 }
338
339 #[must_use]
341 pub fn expect_buffer(mut self, expected: &str) -> Self {
342 if let Some(step) = self.steps.last_mut() {
343 step.expectations
344 .push(StepExpectation::Buffer(expected.to_string()));
345 }
346 self
347 }
348
349 #[must_use]
351 pub fn expect_buffer_contains(mut self, substring: &str) -> Self {
352 if let Some(step) = self.steps.last_mut() {
353 step.expectations
354 .push(StepExpectation::BufferContains(substring.to_string()));
355 }
356 self
357 }
358
359 #[must_use]
361 pub fn expect_mode_contains(mut self, substring: &str) -> Self {
362 if let Some(step) = self.steps.last_mut() {
363 step.expectations
364 .push(StepExpectation::ModeContains(substring.to_string()));
365 }
366 self
367 }
368
369 #[must_use]
371 pub fn expect_edit_mode(mut self, mode: &str) -> Self {
372 if let Some(step) = self.steps.last_mut() {
373 step.expectations
374 .push(StepExpectation::EditMode(mode.to_string()));
375 }
376 self
377 }
378
379 #[must_use]
381 pub fn expect_register(mut self, reg: &str, content: &str, yank_type: &str) -> Self {
382 if let Some(step) = self.steps.last_mut() {
383 step.expectations.push(StepExpectation::Register(
384 reg.to_string(),
385 content.to_string(),
386 yank_type.to_string(),
387 ));
388 }
389 self
390 }
391
392 #[allow(clippy::too_many_lines)]
398 pub async fn run(self) -> StepTrace {
399 let temp_path = {
401 let id = STEP_TEMP_FILE_COUNTER.fetch_add(1, Ordering::SeqCst);
402 let path = format!("/tmp/reovim-step-test-{}-{id}.txt", std::process::id());
403 let content = self.initial_content.as_deref().unwrap_or("");
404 let mut file = std::fs::File::create(&path).expect("Failed to create temp file");
405 file.write_all(content.as_bytes())
406 .expect("Failed to write temp file");
407 path
408 };
409
410 let mut client = self.connect_with_retry().await.expect("Failed to connect");
411
412 client
414 .send_keys(&format!(":e {temp_path}<CR>"))
415 .await
416 .expect("Failed to open buffer file");
417 tokio::time::sleep(Duration::from_millis(50)).await;
418
419 if let Some((line, col)) = self.initial_cursor {
421 if line > 0 {
422 client
423 .send_keys(&format!("{line}j"))
424 .await
425 .expect("Failed to move cursor down");
426 }
427 if col > 0 {
428 client
429 .send_keys(&format!("{col}l"))
430 .await
431 .expect("Failed to move cursor right");
432 }
433 }
434
435 let initial = self.capture_state(&mut client, "(initial)").await;
437
438 let mut snapshots = Vec::new();
439 let mut failed_expectations: Vec<String> = Vec::new();
440
441 for step in &self.steps {
443 client
445 .send_keys(&step.keys)
446 .await
447 .expect("Failed to inject key");
448 tokio::time::sleep(Duration::from_millis(step.delay_ms)).await;
449
450 let snapshot = self.capture_state(&mut client, &step.keys).await;
452
453 for expectation in &step.expectations {
455 match expectation {
456 StepExpectation::Cursor(line, col) => {
457 if snapshot.cursor_line != *line || snapshot.cursor_column != *col {
458 failed_expectations.push(format!(
459 "After '{}': cursor expected ({}:{}), got ({}:{})",
460 step.keys, line, col, snapshot.cursor_line, snapshot.cursor_column
461 ));
462 }
463 }
464 StepExpectation::Buffer(expected) => {
465 if snapshot.buffer.trim_end() != expected.trim_end() {
466 failed_expectations.push(format!(
467 "After '{}': buffer expected {:?}, got {:?}",
468 step.keys, expected, snapshot.buffer
469 ));
470 }
471 }
472 StepExpectation::BufferContains(substring) => {
473 if !snapshot.buffer.contains(substring) {
474 failed_expectations.push(format!(
475 "After '{}': buffer expected to contain {:?}, got {:?}",
476 step.keys, substring, snapshot.buffer
477 ));
478 }
479 }
480 StepExpectation::ModeContains(substring) => {
481 let mode_lower = snapshot.mode_display.to_lowercase();
482 let edit_lower = snapshot.edit_mode.to_lowercase();
483 let sub_lower = substring.to_lowercase();
484 if !mode_lower.contains(&sub_lower) && !edit_lower.contains(&sub_lower) {
485 failed_expectations.push(format!(
486 "After '{}': mode expected to contain {:?}, got display={:?} edit={:?}",
487 step.keys, substring, snapshot.mode_display, snapshot.edit_mode
488 ));
489 }
490 }
491 StepExpectation::EditMode(mode) => {
492 if snapshot.edit_mode != *mode {
493 failed_expectations.push(format!(
494 "After '{}': edit_mode expected {:?}, got {:?}",
495 step.keys, mode, snapshot.edit_mode
496 ));
497 }
498 }
499 StepExpectation::Register(reg, content, yank_type) => {
500 if let Some(register) = snapshot.registers.get(reg) {
501 if register.content.trim_end() != content.trim_end() {
502 failed_expectations.push(format!(
503 "After '{}': register '{}' content expected {:?}, got {:?}",
504 step.keys, reg, content, register.content
505 ));
506 }
507 if register.yank_type != *yank_type {
508 failed_expectations.push(format!(
509 "After '{}': register '{}' type expected {:?}, got {:?}",
510 step.keys, reg, yank_type, register.yank_type
511 ));
512 }
513 } else {
514 failed_expectations.push(format!(
515 "After '{}': register '{}' not found (available: {:?})",
516 step.keys,
517 reg,
518 snapshot.registers.keys().collect::<Vec<_>>()
519 ));
520 }
521 }
522 }
523 }
524
525 snapshots.push(snapshot);
526 }
527 drop(client); StepTrace {
530 initial,
531 snapshots,
532 failed_expectations,
533 harness: self.harness,
534 temp_path: Some(temp_path),
535 }
536 }
537}
538
539pub struct StepTrace {
541 pub initial: StateSnapshot,
543 pub snapshots: Vec<StateSnapshot>,
545 pub failed_expectations: Vec<String>,
547 harness: TestServerHarness,
548 temp_path: Option<String>,
549}
550
551#[cfg_attr(coverage_nightly, coverage(off))]
552impl Drop for StepTrace {
553 fn drop(&mut self) {
554 if let Some(path) = &self.temp_path {
555 let _ = std::fs::remove_file(path);
556 }
557 }
558}
559
560#[cfg_attr(coverage_nightly, coverage(off))]
561impl StepTrace {
562 #[must_use]
566 pub fn log_path(&self) -> Option<&std::path::Path> {
567 self.harness.log_path()
568 }
569
570 fn log_hint(&self) -> String {
572 self.log_path()
573 .map(|p| format!("\n\n Server log: {}", p.display()))
574 .unwrap_or_default()
575 }
576
577 pub fn print_trace(&self) {
579 eprintln!("\n╔════════════════════════════════════════════════════════════╗");
580 eprintln!("║ STEP-BY-STEP TRACE ║");
581 eprintln!("╚════════════════════════════════════════════════════════════╝\n");
582
583 eprintln!(
584 "Initial: {} mode={} ({}:{})",
585 self.initial.key,
586 self.initial.mode_display,
587 self.initial.cursor_line,
588 self.initial.cursor_column
589 );
590 eprintln!(" Buffer: {:?}", self.initial.buffer);
591
592 for (i, snapshot) in self.snapshots.iter().enumerate() {
593 eprintln!(
594 "\nStep {}: '{}' -> {} ({}:{})",
595 i + 1,
596 snapshot.key,
597 snapshot.mode_display,
598 snapshot.cursor_line,
599 snapshot.cursor_column
600 );
601 eprintln!(" Buffer: {:?}", snapshot.buffer);
602 if !snapshot.registers.is_empty() {
603 eprintln!(" Registers:");
604 for (name, info) in &snapshot.registers {
605 eprintln!(" '{}': {:?} ({})", name, info.content, info.yank_type);
606 }
607 }
608 }
609
610 eprintln!("\n────────────────────────────────────────────────────────────");
611
612 if self.failed_expectations.is_empty() {
613 eprintln!("OK: All expectations passed");
614 } else {
615 eprintln!("FAIL: {} expectation(s) failed:", self.failed_expectations.len());
616 for failure in &self.failed_expectations {
617 eprintln!(" - {failure}");
618 }
619 }
620
621 if let Some(path) = self.log_path() {
623 eprintln!("\n Server log: {}", path.display());
624 }
625 eprintln!();
626 }
627
628 pub fn print_compact(&self) {
630 let line: String = std::iter::once(format!(
631 "[init {}:{}]",
632 self.initial.cursor_line, self.initial.cursor_column
633 ))
634 .chain(
635 self.snapshots
636 .iter()
637 .map(|s| format!("'{}' -> {}:{}", s.key, s.cursor_line, s.cursor_column)),
638 )
639 .collect::<Vec<_>>()
640 .join(" → ");
641 eprintln!("Trace: {line}");
642 }
643
644 pub fn assert_ok(&self) {
650 if !self.failed_expectations.is_empty() {
651 self.print_trace();
652 panic!(
653 "Step test failed with {} errors:\n{}{}",
654 self.failed_expectations.len(),
655 self.failed_expectations.join("\n"),
656 self.log_hint()
657 );
658 }
659 }
660
661 #[must_use]
663 pub fn final_state(&self) -> &StateSnapshot {
664 self.snapshots.last().unwrap_or(&self.initial)
665 }
666
667 #[must_use]
669 pub fn state_after(&self, step: usize) -> Option<&StateSnapshot> {
670 self.snapshots.get(step)
671 }
672}