1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
//! Every wait takes a per-call deadline, and the error reports the
//! deadline that actually applied.
use std::time::{Duration, Instant};
use termlens::{Error, Key, Terminal};
mod common;
/// The builder default is deliberately far too short for the app; only
/// the per-call override can see each wait through. Steps are documented
/// in `fixtures/emit/src/main.rs`.
fn slow_app(steps: &[&str]) -> Terminal {
common::spawn_emit(
Terminal::builder()
.size(80, 24)
.env_clear()
.timeout(Duration::from_millis(150)),
steps,
)
.expect("spawn")
}
#[test]
#[cfg_attr(
windows,
ignore = "ConPTY closes a DEC 2026 bracket before the content it wrapped, so no frame holds what was drawn (#149)"
)]
fn wait_frame_for_overrides_the_builder_default() -> termlens::Result<()> {
let mut t = slow_app(&[
"--sleep",
"1s",
"--raw",
r"\e[?2026hlate frame\e[?2026l",
"--wait",
]);
t.wait_frame_for(|s| s.contains("late frame"), Duration::from_secs(30))?;
t.send(Key::Enter)?;
assert!(t.wait_exit_for(Duration::from_secs(30))?.success());
Ok(())
}
#[test]
fn wait_idle_for_overrides_the_builder_default() -> termlens::Result<()> {
let mut t = slow_app(&["busy", "--wait"]);
// Let the child's first output land before measuring the silence.
// `wait_idle` counts quiet from the call, and a child that has not
// started writing yet is trivially quiet: the stress workflow saw this
// resolve after 200 ms of *nothing* on a loaded Windows runner (ConPTY
// holds the child until its startup handshake is answered), and the
// screen assertion below then failed on an empty grid — a race in this
// test, not in the wait. The per-call override is still what is under
// test: a 200 ms quiet period cannot be observed under the 150 ms
// builder deadline at all.
t.wait_until_for(|s| s.contains("busy"), Duration::from_secs(30))?;
let start = Instant::now();
t.wait_idle_for(Duration::from_millis(200), Duration::from_secs(30))?;
// The quiet window counts from the last byte, which landed a moment
// before `start`, so the elapsed time is a hair under 200 ms; what the
// test needs is that it is past the 150 ms builder deadline, under
// which the same call would have errored instead.
assert!(
start.elapsed() > Duration::from_millis(150),
"resolved inside the builder deadline, so the override did not apply: {:?}",
start.elapsed()
);
assert!(t.screen().contains("busy"), "{}", t.screen());
// (Asserting that the unqualified `wait_idle` still fails here would
// be wrong: the terminal has now been silent for longer than the
// quiet period, so it resolves immediately without consulting any
// deadline. `per_call_timeouts_report_their_own_deadline` covers the
// deadline behaviour against a still-chattering child.)
t.send(Key::Enter)?;
assert!(t.wait_exit_for(Duration::from_secs(30))?.success());
Ok(())
}
#[test]
fn wait_exit_for_overrides_the_builder_default() -> termlens::Result<()> {
let mut t = slow_app(&["--sleep", "1s", "--exit", "3"]);
let status = t.wait_exit_for(Duration::from_secs(30))?;
assert_eq!(status.code(), Some(3), "status: {status}");
Ok(())
}
/// A per-call timeout also cuts a generous default short, and the error
/// reports the deadline that actually applied — not the builder's.
#[test]
fn per_call_timeouts_report_their_own_deadline() {
let mut t = common::spawn_emit(
Terminal::builder()
.size(80, 24)
.env_clear()
.timeout(Duration::from_secs(30)),
&["--wait"],
)
.expect("spawn");
for (label, err) in [
(
"wait_frame_for",
t.wait_frame_for(|s| s.contains("never"), Duration::from_millis(120))
.unwrap_err(),
),
(
"wait_idle_for",
t.wait_idle_for(Duration::from_secs(5), Duration::from_millis(120))
.unwrap_err(),
),
(
"wait_exit_for",
t.wait_exit_for(Duration::from_millis(120)).unwrap_err(),
),
] {
match err {
Error::Timeout { timeout, .. } => assert_eq!(
timeout,
Duration::from_millis(120),
"{label} reported the wrong deadline"
),
other => panic!("{label}: expected a timeout, got {other}"),
}
}
}
/// The overrides must not cost wall-clock when they are not needed.
#[test]
fn a_short_per_call_timeout_fails_fast() {
let mut t = common::spawn_emit(
Terminal::builder()
.env_clear()
.timeout(Duration::from_secs(60)),
&["--wait"],
)
.expect("spawn");
let start = Instant::now();
let _ = t.wait_frame_for(|s| s.contains("never"), Duration::from_millis(100));
assert!(
start.elapsed() < Duration::from_secs(5),
"the per-call deadline did not cut the 60s default short: {:?}",
start.elapsed()
);
}