Skip to content

Commit b3d6481

Browse files
committed
test(tui): empty-prompt DEL waits prove the DELs ran; grep waits on visible text
Six tests sent "DEL DEL DEL" on an empty minibuffer prompt and then waited a fixed duration. The awaited outcome is a no-op, so no screen predicate can distinguish "processed" from "still queued", and the tests compared whatever the blind window happened to catch. CommandTrace installs a post-command-hook in both editors that appends each executed command to a per-session file (write-region under inhibit-message, so the trace leaves no echo-area difference of its own). Waiting for the line count to advance is an ordered event; the fixed read_both tails stay as the redisplay drain. grep_via_mx_lists_matching_file_lines waited for a row that the screen never shows: the grep buffer displays "1:alpha needle one", not "grep-usage.txt:1:alpha needle one". Both read_until calls therefore burned their full deadlines (27s of the file 32s wall) and the comparison raced the render. The readiness predicate now names the visible text; the assertions are unchanged. shell_compile goes 32.2s -> 6.7s.
1 parent d904bfe commit b3d6481

2 files changed

Lines changed: 111 additions & 7 deletions

File tree

crates/neomacs-tui-tests/src/pair.rs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
use crate::*;
44
use std::fs;
5-
use std::path::Path;
5+
use std::path::{Path, PathBuf};
66
use std::time::{Duration, Instant};
77

88
/// Maximum total time for GNU Emacs to reach the startup predicate.
@@ -568,6 +568,96 @@ pub fn assert_home_file_contents(gnu: &TuiSession, neo: &TuiSession, name: &str,
568568
);
569569
}
570570

571+
// ── Event-ordered barriers ─────────────────────────────────────────────
572+
573+
/// A per-command record written by the editor's own `post-command-hook`.
574+
///
575+
/// Some waits cannot be phrased as a screen predicate because the awaited
576+
/// event's visible effect is a no-op — `DEL` on an empty minibuffer prompt
577+
/// must be *processed* while leaving the screen unchanged, so "still looks
578+
/// right" is true both before and after. Waiting a duration guesses; this
579+
/// barrier observes the editor's command loop directly: every executed
580+
/// command appends its name to a per-session trace file, so waiting for the
581+
/// line count to advance by N is an ordered event, independent of machine
582+
/// speed.
583+
///
584+
/// Install before the interaction under test (the install itself is input,
585+
/// so it cannot run once a minibuffer is open), take a baseline with
586+
/// [`CommandTrace::mark`], then wait for the expected number of commands.
587+
/// Follow with a drain (the suite's fixed `read_both` tails do this):
588+
/// `post-command-hook` runs before redisplay, so the trace proves input was
589+
/// consumed while the drain lets the resulting frame be painted.
590+
pub struct CommandTrace {
591+
gnu_path: PathBuf,
592+
neo_path: PathBuf,
593+
}
594+
595+
impl CommandTrace {
596+
/// Install the trace hook in both editors and return their log paths.
597+
pub fn install(gnu: &mut TuiSession, neo: &mut TuiSession) -> Self {
598+
let gnu_path = gnu.temp_dir().join("command-trace");
599+
let neo_path = neo.temp_dir().join("command-trace");
600+
for path in [&gnu_path, &neo_path] {
601+
let _ = fs::remove_file(path);
602+
}
603+
let install = |path: &Path| {
604+
format!(
605+
r#"(progn
606+
(defun neomacs-tui--trace-command ()
607+
(ignore-errors
608+
(let ((inhibit-message t))
609+
(write-region (format "%S\n" this-command) nil {path:?} 'append))))
610+
(add-hook 'post-command-hook #'neomacs-tui--trace-command))"#,
611+
)
612+
};
613+
eval_expression_one(gnu, &install(&gnu_path));
614+
eval_expression_one(neo, &install(&neo_path));
615+
Self { gnu_path, neo_path }
616+
}
617+
618+
/// Count commands executed so far, as `(gnu, neomacs)`.
619+
pub fn mark(&self) -> (usize, usize) {
620+
(trace_lines(&self.gnu_path), trace_lines(&self.neo_path))
621+
}
622+
623+
/// Block until both editors have executed `delta` more commands than
624+
/// `baseline`, re-reading their PTYs while waiting.
625+
pub fn wait_for(
626+
&self,
627+
gnu: &mut TuiSession,
628+
neo: &mut TuiSession,
629+
baseline: (usize, usize),
630+
delta: usize,
631+
timeout: Duration,
632+
) {
633+
let deadline = Instant::now() + timeout;
634+
loop {
635+
let now = self.mark();
636+
if now.0 >= baseline.0 + delta && now.1 >= baseline.1 + delta {
637+
return;
638+
}
639+
let remaining = deadline.saturating_duration_since(Instant::now());
640+
if remaining.is_zero() {
641+
break;
642+
}
643+
read_both(gnu, neo, remaining.min(Duration::from_millis(100)));
644+
}
645+
panic!(
646+
"command trace did not advance: baseline {baseline:?} want +{delta}, got {:?}\n\
647+
GNU trace:\n{}\nNeomacs trace:\n{}",
648+
self.mark(),
649+
fs::read_to_string(&self.gnu_path).unwrap_or_default(),
650+
fs::read_to_string(&self.neo_path).unwrap_or_default(),
651+
);
652+
}
653+
}
654+
655+
fn trace_lines(path: &Path) -> usize {
656+
fs::read_to_string(path)
657+
.map(|contents| contents.lines().filter(|line| !line.is_empty()).count())
658+
.unwrap_or(0)
659+
}
660+
571661
// ── File helpers ──────────────────────────────────────────────────────
572662

573663
pub fn write_home_file(session: &TuiSession, name: &str, contents: &str) {

crates/neomacs-tui-tests/tests/shell_compile.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,17 @@ fn shell_command_via_mbang_displays_short_output() {
3838
#[test]
3939
fn shell_command_empty_prompt_multiple_del_keeps_prompt() {
4040
let (mut gnu, mut neo) = boot_pair("");
41+
let trace = CommandTrace::install(&mut gnu, &mut neo);
4142

4243
send_both(&mut gnu, &mut neo, "M-!");
4344
let prompt_ready = |grid: &[String]| grid.iter().any(|row| row.contains("Shell command:"));
4445
gnu.read_until(Duration::from_secs(6), prompt_ready);
4546
neo.read_until(Duration::from_secs(8), prompt_ready);
4647
read_both(&mut gnu, &mut neo, Duration::from_millis(300));
4748

49+
let baseline = trace.mark();
4850
send_both(&mut gnu, &mut neo, "DEL DEL DEL");
51+
trace.wait_for(&mut gnu, &mut neo, baseline, 3, Duration::from_secs(8));
4952

5053
let prompt_intact = |grid: &[String]| {
5154
grid.iter().any(|row| row.contains("Shell command:"))
@@ -160,6 +163,7 @@ fn async_shell_command_via_mampersand_displays_output_buffer() {
160163
#[test]
161164
fn async_shell_command_empty_prompt_multiple_del_keeps_prompt() {
162165
let (mut gnu, mut neo) = boot_pair("");
166+
let trace = CommandTrace::install(&mut gnu, &mut neo);
163167

164168
send_both(&mut gnu, &mut neo, "M-&");
165169
let prompt_ready =
@@ -168,7 +172,9 @@ fn async_shell_command_empty_prompt_multiple_del_keeps_prompt() {
168172
neo.read_until(Duration::from_secs(8), prompt_ready);
169173
read_both(&mut gnu, &mut neo, Duration::from_millis(300));
170174

175+
let baseline = trace.mark();
171176
send_both(&mut gnu, &mut neo, "DEL DEL DEL");
177+
trace.wait_for(&mut gnu, &mut neo, baseline, 3, Duration::from_secs(8));
172178

173179
let prompt_intact = |grid: &[String]| {
174180
grid.iter().any(|row| row.contains("Async shell command:"))
@@ -443,14 +449,17 @@ fn compile_via_mx_runs_command_in_compilation_buffer() {
443449
#[test]
444450
fn compile_empty_prompt_multiple_del_keeps_prompt() {
445451
let (mut gnu, mut neo) = boot_pair("");
452+
let trace = CommandTrace::install(&mut gnu, &mut neo);
446453

447454
invoke_mx_command(&mut gnu, &mut neo, "compile");
448455
let prompt_ready = |grid: &[String]| grid.iter().any(|row| row.contains("Compile command:"));
449456
gnu.read_until(Duration::from_secs(8), prompt_ready);
450457
neo.read_until(Duration::from_secs(10), prompt_ready);
451458
read_both(&mut gnu, &mut neo, Duration::from_millis(300));
452459

460+
let baseline = trace.mark();
453461
send_both(&mut gnu, &mut neo, "C-a C-k DEL DEL DEL");
462+
trace.wait_for(&mut gnu, &mut neo, baseline, 5, Duration::from_secs(8));
454463

455464
let prompt_intact = |grid: &[String]| {
456465
grid.iter().any(|row| row.contains("Compile command:"))
@@ -500,12 +509,8 @@ fn grep_via_mx_lists_matching_file_lines() {
500509

501510
let ready = |grid: &[String]| {
502511
grid.iter().any(|row| row.contains("*grep*"))
503-
&& grid
504-
.iter()
505-
.any(|row| row.contains("grep-usage.txt:1:alpha needle one"))
506-
&& grid
507-
.iter()
508-
.any(|row| row.contains("grep-usage.txt:3:gamma needle two"))
512+
&& grid.iter().any(|row| row.contains("1:alpha needle one"))
513+
&& grid.iter().any(|row| row.contains("3:gamma needle two"))
509514
};
510515
gnu.read_until(Duration::from_secs(12), ready);
511516
neo.read_until(Duration::from_secs(14), ready);
@@ -532,6 +537,7 @@ fn grep_via_mx_lists_matching_file_lines() {
532537
#[test]
533538
fn grep_empty_prompt_multiple_del_keeps_prompt() {
534539
let (mut gnu, mut neo) = boot_pair("");
540+
let trace = CommandTrace::install(&mut gnu, &mut neo);
535541
open_home_file(
536542
&mut gnu,
537543
&mut neo,
@@ -547,7 +553,9 @@ fn grep_empty_prompt_multiple_del_keeps_prompt() {
547553
neo.read_until(Duration::from_secs(10), prompt_ready);
548554
read_both(&mut gnu, &mut neo, Duration::from_millis(300));
549555

556+
let baseline = trace.mark();
550557
send_both(&mut gnu, &mut neo, "C-a C-k DEL DEL DEL");
558+
trace.wait_for(&mut gnu, &mut neo, baseline, 5, Duration::from_secs(8));
551559

552560
let prompt_intact = |grid: &[String]| {
553561
grid.iter().any(|row| row.contains("Run grep (like this):"))
@@ -672,6 +680,7 @@ fn diff_buffer_with_file_via_mx_shows_unsaved_changes() {
672680
#[test]
673681
fn diff_buffer_with_file_empty_prompt_multiple_del_keeps_prompt() {
674682
let (mut gnu, mut neo) = boot_pair_editing_a_shared_file();
683+
let trace = CommandTrace::install(&mut gnu, &mut neo);
675684
let shared_path = write_shared_temp_file("diff-buffer-empty-del.txt", "alpha\nbeta\n");
676685
open_shared_file(&mut gnu, &mut neo, &shared_path, "C-x C-f");
677686

@@ -681,7 +690,9 @@ fn diff_buffer_with_file_empty_prompt_multiple_del_keeps_prompt() {
681690
neo.read_until(Duration::from_secs(8), prompt_ready);
682691
read_both(&mut gnu, &mut neo, Duration::from_millis(300));
683692

693+
let baseline = trace.mark();
684694
send_both(&mut gnu, &mut neo, "DEL DEL DEL");
695+
trace.wait_for(&mut gnu, &mut neo, baseline, 3, Duration::from_secs(8));
685696

686697
let prompt_intact = |grid: &[String]| {
687698
grid.iter().any(|row| row.contains("Buffer (default"))
@@ -759,6 +770,7 @@ fn shell_command_on_region_via_mbar_with_cat_preserves_text() {
759770
#[test]
760771
fn shell_command_on_region_empty_prompt_multiple_del_keeps_prompt() {
761772
let (mut gnu, mut neo) = boot_pair("");
773+
let trace = CommandTrace::install(&mut gnu, &mut neo);
762774

763775
open_home_file(
764776
&mut gnu,
@@ -777,7 +789,9 @@ fn shell_command_on_region_empty_prompt_multiple_del_keeps_prompt() {
777789
neo.read_until(Duration::from_secs(8), prompt_ready);
778790
read_both(&mut gnu, &mut neo, Duration::from_millis(300));
779791

792+
let baseline = trace.mark();
780793
send_both(&mut gnu, &mut neo, "DEL DEL DEL");
794+
trace.wait_for(&mut gnu, &mut neo, baseline, 3, Duration::from_secs(8));
781795

782796
let prompt_intact = |grid: &[String]| {
783797
grid.iter()

0 commit comments

Comments
 (0)