@@ -8,68 +8,92 @@ pub use pty_terminal::{
88 terminal:: { ChildHandle , PtyWriter } ,
99} ;
1010
11- const MILESTONE_HYPERTEXT : char = '\u{200b}' ;
12-
1311/// Tracks the two independently delivered parts of each milestone.
1412///
15- /// A milestone starts with an OSC 8 hyperlink carrying its name and contains a
16- /// zero-width printable character. On `ConPTY`, the OSC control sequence can be
17- /// forwarded before earlier screen updates, while the printable character
18- /// follows those updates through the asynchronous rendering path. A milestone
19- /// is therefore complete only after both parts have arrived .
13+ /// A milestone starts with an OSC 8 hyperlink carrying its name and identifier,
14+ /// followed by the same identifier encoded with zero-width rendered characters.
15+ /// On `ConPTY`, the OSC control sequence can be forwarded before earlier screen
16+ /// updates, while the fence follows those updates through the asynchronous
17+ /// rendering path. A milestone is complete only after both matching parts arrive .
2018///
21- /// `ConPTY` may coalesce intermediate rendered frames, so the protocol permits
22- /// only one marker to await its fence at a time. The test harness must consume
23- /// that fence before sending input that can trigger another render.
19+ /// Self-identifying fences allow `ConPTY` to coalesce intermediate frames without
20+ /// making a later fence complete the wrong marker.
2421#[ derive( Default ) ]
2522struct MilestoneTracker {
26- /// Marker whose rendered zero-width anchor has not arrived yet.
27- awaiting_fence : Option < String > ,
23+ pending : VecDeque < pty_terminal_test_client:: DecodedMilestone > ,
2824 /// Marker names whose matching rendered anchors have arrived.
2925 completed : VecDeque < String > ,
30- /// Monotonic completion count used for silent flow-control waits.
31- completion_count : usize ,
26+ fence_decoder : RenderFenceDecoder ,
3227}
3328
3429impl MilestoneTracker {
35- fn take_completed ( & mut self , name : & str ) -> bool {
36- // Keep unrelated completed milestones available for later calls. A PTY
37- // read can contain more than the milestone currently being requested.
38- self . completed
39- . iter ( )
40- . position ( |completed| completed == name)
41- . and_then ( |index| self . completed . remove ( index) )
42- . is_some ( )
30+ fn pop_completed ( & mut self ) -> Option < String > {
31+ self . completed . pop_front ( )
4332 }
4433}
4534
4635impl vte:: Perform for MilestoneTracker {
4736 fn print ( & mut self , character : char ) {
48- // `print` is called only for rendered characters, not for bytes inside
49- // OSC metadata. With stop-and-wait flow control, the anchor can only
50- // belong to the single marker awaiting a fence.
51- if character == MILESTONE_HYPERTEXT
52- && let Some ( name) = self . awaiting_fence . take ( )
37+ if let Some ( id) = self . fence_decoder . advance ( character)
38+ && let Some ( index) = self . pending . iter ( ) . position ( |marker| marker. id == id)
5339 {
54- self . completed . push_back ( name) ;
55- self . completion_count += 1 ;
40+ // A later rendered fence proves that any older pending frames were
41+ // coalesced. Drop them together with the matching marker.
42+ let marker = self . pending . drain ( ..=index) . next_back ( ) . unwrap ( ) ;
43+ self . completed . push_back ( marker. name ) ;
5644 }
5745 }
5846
5947 fn osc_dispatch ( & mut self , params : & [ & [ u8 ] ] , _bell_terminated : bool ) {
6048 // The decoder accepts only milestone hyperlink opens. Ordinary OSC
6149 // sequences and the empty OSC 8 close sequence are ignored.
62- if let Some ( name) = pty_terminal_test_client:: decode_milestone_from_osc8_params ( params) {
63- assert ! (
64- self . awaiting_fence. is_none( ) ,
65- "milestone protocol violation: marker '{name}' arrived before the rendered fence for '{}'" ,
66- self . awaiting_fence. as_deref( ) . unwrap( )
67- ) ;
68- self . awaiting_fence = Some ( name) ;
50+ if let Some ( marker) = pty_terminal_test_client:: decode_milestone_from_osc8_params ( params) {
51+ if let Some ( index) = self . pending . iter ( ) . position ( |pending| pending. id == marker. id ) {
52+ self . pending . remove ( index) ;
53+ }
54+ self . pending . push_back ( marker) ;
6955 }
7056 }
7157}
7258
59+ #[ derive( Default ) ]
60+ struct RenderFenceDecoder {
61+ active : bool ,
62+ id : u32 ,
63+ digits : u8 ,
64+ }
65+
66+ impl RenderFenceDecoder {
67+ fn advance ( & mut self , character : char ) -> Option < u32 > {
68+ if pty_terminal_test_client:: is_milestone_render_boundary ( character) {
69+ if self . active && self . digits == pty_terminal_test_client:: MILESTONE_RENDER_DIGITS {
70+ let id = self . id ;
71+ // The closing boundary can also be the opening boundary of a
72+ // following fence if the previous close was coalesced away.
73+ self . id = 0 ;
74+ self . digits = 0 ;
75+ return Some ( id) ;
76+ }
77+ self . active = true ;
78+ self . id = 0 ;
79+ self . digits = 0 ;
80+ return None ;
81+ }
82+
83+ let Some ( digit) = pty_terminal_test_client:: decode_milestone_render_digit ( character) else {
84+ self . active = false ;
85+ return None ;
86+ } ;
87+ if !self . active || self . digits == pty_terminal_test_client:: MILESTONE_RENDER_DIGITS {
88+ self . active = false ;
89+ return None ;
90+ }
91+ self . id = ( self . id << 2 ) | u32:: from ( digit) ;
92+ self . digits += 1 ;
93+ None
94+ }
95+ }
96+
7397/// A test-oriented terminal that provides milestone-based synchronization.
7498///
7599/// Wraps a PTY terminal, splitting it into a [`PtyWriter`] for sending input
@@ -139,15 +163,19 @@ impl Reader {
139163 #[ must_use]
140164 pub fn screen_contents ( & self ) -> String {
141165 let mut contents = self . pty . screen_contents ( ) ;
142- contents. retain ( |ch| ch != MILESTONE_HYPERTEXT ) ;
166+ contents. retain ( |character| {
167+ !pty_terminal_test_client:: is_milestone_render_character ( character)
168+ } ) ;
143169 contents
144170 }
145171
146172 /// Returns the screen contents with inline ANSI SGR escape codes preserved.
147173 /// Useful for snapshot tests that need to assert colour or style attributes.
148174 #[ must_use]
149175 pub fn screen_contents_formatted ( & self ) -> Vec < u8 > {
150- self . pty . screen_contents_formatted ( )
176+ pty_terminal_test_client:: strip_milestone_render_characters (
177+ & self . pty . screen_contents_formatted ( ) ,
178+ )
151179 }
152180
153181 /// Reads from the PTY until a milestone with the given name is encountered.
@@ -171,35 +199,17 @@ impl Reader {
171199 let mut buf = [ 0u8 ; 4096 ] ;
172200
173201 loop {
174- if self . milestone_tracker . take_completed ( name) {
175- return self . screen_contents ( ) ;
202+ while let Some ( completed) = self . milestone_tracker . pop_completed ( ) {
203+ if completed == name {
204+ return self . screen_contents ( ) ;
205+ }
176206 }
177207
178208 let n = self . read ( & mut buf) . expect ( "PTY read failed" ) ;
179209 assert ! ( n > 0 , "EOF reached before milestone '{name}'" ) ;
180210 }
181211 }
182212
183- /// Waits until the next milestone marker and its rendered fence arrive.
184- ///
185- /// Unlike [`Self::expect_milestone`], this does not consume the completed
186- /// milestone. It is intended for stop-and-wait flow control when a test
187- /// sends several logical input events but only snapshots the final state.
188- ///
189- /// # Panics
190- ///
191- /// Panics if the child process exits before another milestone completes,
192- /// or if a PTY read fails.
193- pub fn wait_for_next_milestone ( & mut self ) {
194- let completion_count = self . milestone_tracker . completion_count ;
195- let mut buf = [ 0u8 ; 4096 ] ;
196-
197- while self . milestone_tracker . completion_count == completion_count {
198- let n = self . read ( & mut buf) . expect ( "PTY read failed" ) ;
199- assert ! ( n > 0 , "EOF reached before the next milestone" ) ;
200- }
201- }
202-
203213 /// Reads all remaining PTY output until the child exits, then returns the exit status.
204214 ///
205215 /// # Errors
@@ -220,16 +230,18 @@ impl Reader {
220230mod tests {
221231 use super :: * ;
222232
223- fn marker_without_fence ( name : & str ) -> Vec < u8 > {
233+ fn marker_without_fence ( name : & str ) -> ( Vec < u8 > , Vec < u8 > ) {
224234 // Model ConPTY's fast control path by delivering the complete OSC marker
225235 // before its printable anchor reaches the output pipe.
226236 let mut marker = pty_terminal_test_client:: encoded_milestone ( name) ;
227237 let index = marker
228- . windows ( pty_terminal_test_client:: MILESTONE_RENDER_FENCE . len ( ) )
229- . position ( |window| window == pty_terminal_test_client:: MILESTONE_RENDER_FENCE )
238+ . windows ( pty_terminal_test_client:: MILESTONE_RENDER_FENCE_START . len ( ) )
239+ . position ( |window| window == pty_terminal_test_client:: MILESTONE_RENDER_FENCE_START )
230240 . unwrap ( ) ;
231- marker. drain ( index..index + pty_terminal_test_client:: MILESTONE_RENDER_FENCE . len ( ) ) ;
232- marker
241+ let fence = marker
242+ . drain ( index..index + pty_terminal_test_client:: MILESTONE_RENDER_FENCE_LEN )
243+ . collect ( ) ;
244+ ( marker, fence)
233245 }
234246
235247 fn advance ( parser : & mut vte:: Parser , tracker : & mut MilestoneTracker , bytes : & [ u8 ] ) {
@@ -243,12 +255,13 @@ mod tests {
243255
244256 // Receiving the marker and subsequent printable output is insufficient:
245257 // only the protocol's rendered anchor establishes the screen barrier.
246- advance ( & mut parser, & mut tracker, & marker_without_fence ( "target" ) ) ;
258+ let ( marker, fence) = marker_without_fence ( "target" ) ;
259+ advance ( & mut parser, & mut tracker, & marker) ;
247260 advance ( & mut parser, & mut tracker, b"rendered output" ) ;
248- assert ! ( ! tracker. take_completed ( "target" ) ) ;
261+ assert ! ( tracker. pop_completed ( ) . is_none ( ) ) ;
249262
250- advance ( & mut parser, & mut tracker, pty_terminal_test_client :: MILESTONE_RENDER_FENCE ) ;
251- assert ! ( tracker. take_completed ( "target" ) ) ;
263+ advance ( & mut parser, & mut tracker, & fence ) ;
264+ assert_eq ! ( tracker. pop_completed ( ) . as_deref ( ) , Some ( "target" ) ) ;
252265 }
253266
254267 #[ test]
@@ -260,7 +273,11 @@ mod tests {
260273 let mut tracker = MilestoneTracker :: default ( ) ;
261274 advance ( & mut parser, & mut tracker, & marker[ ..split] ) ;
262275 advance ( & mut parser, & mut tracker, & marker[ split..] ) ;
263- assert ! ( tracker. take_completed( "target" ) , "failed at split {split}" ) ;
276+ assert_eq ! (
277+ tracker. pop_completed( ) . as_deref( ) ,
278+ Some ( "target" ) ,
279+ "failed at split {split}"
280+ ) ;
264281 }
265282 }
266283
@@ -270,19 +287,65 @@ mod tests {
270287 let mut tracker = MilestoneTracker :: default ( ) ;
271288
272289 advance ( & mut parser, & mut tracker, & pty_terminal_test_client:: encoded_milestone ( "first" ) ) ;
273- assert ! ( tracker. take_completed ( "first" ) ) ;
290+ assert_eq ! ( tracker. pop_completed ( ) . as_deref ( ) , Some ( "first" ) ) ;
274291 advance ( & mut parser, & mut tracker, & pty_terminal_test_client:: encoded_milestone ( "second" ) ) ;
275- assert ! ( tracker. take_completed( "second" ) ) ;
292+ assert_eq ! ( tracker. pop_completed( ) . as_deref( ) , Some ( "second" ) ) ;
293+ }
294+
295+ #[ test]
296+ fn coalesced_markers_complete_the_latest_state ( ) {
297+ let mut parser = vte:: Parser :: new ( ) ;
298+ let mut tracker = MilestoneTracker :: default ( ) ;
299+ let ( mut markers, _) = marker_without_fence ( "first" ) ;
300+ let ( second_marker, second_fence) = marker_without_fence ( "second" ) ;
301+ markers. extend ( second_marker) ;
302+
303+ advance ( & mut parser, & mut tracker, & markers) ;
304+ advance ( & mut parser, & mut tracker, & second_fence) ;
305+ assert_eq ! ( tracker. pop_completed( ) . as_deref( ) , Some ( "second" ) ) ;
306+ assert ! ( tracker. pop_completed( ) . is_none( ) ) ;
307+ assert ! ( tracker. pending. is_empty( ) ) ;
276308 }
277309
278310 #[ test]
279- #[ should_panic( expected = "milestone protocol violation" ) ]
280- fn second_marker_before_fence_is_a_protocol_violation ( ) {
311+ fn surviving_fences_complete_their_own_markers ( ) {
281312 let mut parser = vte:: Parser :: new ( ) ;
282313 let mut tracker = MilestoneTracker :: default ( ) ;
283- let mut markers = marker_without_fence ( "first" ) ;
284- markers. extend ( marker_without_fence ( "second" ) ) ;
314+ let ( mut markers, first_fence) = marker_without_fence ( "first" ) ;
315+ let ( second_marker, second_fence) = marker_without_fence ( "second" ) ;
316+ markers. extend ( second_marker) ;
285317
286318 advance ( & mut parser, & mut tracker, & markers) ;
319+ advance ( & mut parser, & mut tracker, & first_fence) ;
320+ advance ( & mut parser, & mut tracker, & second_fence) ;
321+ assert_eq ! ( tracker. pop_completed( ) . as_deref( ) , Some ( "first" ) ) ;
322+ assert_eq ! ( tracker. pop_completed( ) . as_deref( ) , Some ( "second" ) ) ;
323+ }
324+
325+ #[ test]
326+ fn a_missing_close_does_not_consume_the_next_fence ( ) {
327+ let mut parser = vte:: Parser :: new ( ) ;
328+ let mut tracker = MilestoneTracker :: default ( ) ;
329+ let ( mut markers, mut first_fence) = marker_without_fence ( "first" ) ;
330+ let ( second_marker, second_fence) = marker_without_fence ( "second" ) ;
331+ markers. extend ( second_marker) ;
332+ first_fence. truncate (
333+ first_fence. len ( ) - pty_terminal_test_client:: MILESTONE_RENDER_FENCE_START . len ( ) ,
334+ ) ;
335+
336+ advance ( & mut parser, & mut tracker, & markers) ;
337+ advance ( & mut parser, & mut tracker, & first_fence) ;
338+ advance ( & mut parser, & mut tracker, & second_fence) ;
339+ assert_eq ! ( tracker. pop_completed( ) . as_deref( ) , Some ( "first" ) ) ;
340+ assert_eq ! ( tracker. pop_completed( ) . as_deref( ) , Some ( "second" ) ) ;
341+ }
342+
343+ #[ test]
344+ fn screen_stripping_removes_reserved_protocol_characters ( ) {
345+ let standalone = "before\u{2060} \u{2061} after" ;
346+ assert_eq ! (
347+ pty_terminal_test_client:: strip_milestone_render_characters( standalone. as_bytes( ) ) ,
348+ b"beforeafter"
349+ ) ;
287350 }
288351}
0 commit comments