1- use std:: { collections :: VecDeque , io :: Read } ;
1+ use std:: io :: { BufReader , Read } ;
22
33pub use portable_pty:: CommandBuilder ;
44use pty_terminal:: terminal:: { PtyReader , Terminal } ;
@@ -10,58 +10,6 @@ pub use pty_terminal::{
1010
1111const MILESTONE_HYPERTEXT : char = '\u{200b}' ;
1212
13- /// Tracks the two independently delivered parts of each milestone.
14- ///
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.
20- ///
21- /// Several OSC markers can overtake their anchors. The two queues preserve the
22- /// protocol order so an earlier marker's delayed anchor cannot complete a later
23- /// marker by mistake.
24- #[ derive( Default ) ]
25- struct MilestoneTracker {
26- /// Marker names whose rendered zero-width anchors have not arrived yet.
27- awaiting_fence : VecDeque < String > ,
28- /// Marker names whose matching rendered anchors have arrived.
29- completed : VecDeque < String > ,
30- }
31-
32- impl MilestoneTracker {
33- fn take_completed ( & mut self , name : & str ) -> bool {
34- // Keep unrelated completed milestones available for later calls. A PTY
35- // read can contain more than the milestone currently being requested.
36- self . completed
37- . iter ( )
38- . position ( |completed| completed == name)
39- . and_then ( |index| self . completed . remove ( index) )
40- . is_some ( )
41- }
42- }
43-
44- impl vte:: Perform for MilestoneTracker {
45- fn print ( & mut self , character : char ) {
46- // `print` is called only for rendered characters, not for bytes inside
47- // OSC metadata. ConPTY preserves the order of these rendered anchors,
48- // so each anchor completes the oldest marker still awaiting one.
49- if character == MILESTONE_HYPERTEXT
50- && let Some ( name) = self . awaiting_fence . pop_front ( )
51- {
52- self . completed . push_back ( name) ;
53- }
54- }
55-
56- fn osc_dispatch ( & mut self , params : & [ & [ u8 ] ] , _bell_terminated : bool ) {
57- // The decoder accepts only milestone hyperlink opens. Ordinary OSC
58- // sequences and the empty OSC 8 close sequence are ignored.
59- if let Some ( name) = pty_terminal_test_client:: decode_milestone_from_osc8_params ( params) {
60- self . awaiting_fence . push_back ( name) ;
61- }
62- }
63- }
64-
6513/// A test-oriented terminal that provides milestone-based synchronization.
6614///
6715/// Wraps a PTY terminal, splitting it into a [`PtyWriter`] for sending input
@@ -75,17 +23,7 @@ pub struct TestTerminal {
7523
7624/// The read half of a test terminal, wrapping [`PtyReader`] with milestone support.
7725pub struct Reader {
78- /// Reads bytes and updates the terminal's primary `vt100` screen parser.
79- ///
80- /// This is deliberately not wrapped in `BufReader`: its read-ahead would
81- /// let the primary parser consume bytes the milestone parser has not seen.
82- pty : PtyReader ,
83- /// Observes the same byte stream to distinguish OSC markers from printable
84- /// anchors. `vt100::Callbacks` exposes unhandled OSC sequences but has no
85- /// callback for ordinary rendered characters, hence this small second parser.
86- milestone_parser : vte:: Parser ,
87- /// Persists protocol state across reads and `expect_milestone` calls.
88- milestone_tracker : MilestoneTracker ,
26+ pty : BufReader < PtyReader > ,
8927 child_handle : ChildHandle ,
9028}
9129
@@ -99,38 +37,17 @@ impl TestTerminal {
9937 let Terminal { pty_reader, pty_writer, child_handle, .. } = Terminal :: spawn ( size, cmd) ?;
10038 Ok ( Self {
10139 writer : pty_writer,
102- reader : Reader {
103- pty : pty_reader,
104- milestone_parser : vte:: Parser :: new ( ) ,
105- milestone_tracker : MilestoneTracker :: default ( ) ,
106- child_handle : child_handle. clone ( ) ,
107- } ,
40+ reader : Reader { pty : BufReader :: new ( pty_reader) , child_handle : child_handle. clone ( ) } ,
10841 child_handle,
10942 } )
11043 }
11144}
11245
11346impl Reader {
114- /// Reads once while keeping the screen parser and milestone parser in lockstep.
115- ///
116- /// All PTY draining, including shutdown, must go through this method. Reading
117- /// directly from `pty` would update the screen while silently skipping those
118- /// bytes in the milestone protocol state.
119- fn read ( & mut self , buf : & mut [ u8 ] ) -> std:: io:: Result < usize > {
120- let n = self . pty . read ( buf) ?;
121- self . milestone_parser . advance ( & mut self . milestone_tracker , & buf[ ..n] ) ;
122-
123- // `PtyReader`'s primary parser also records the OSC sequences. The
124- // dedicated tracker above owns milestone handling, so discard this
125- // duplicate copy rather than letting it grow for the lifetime of a test.
126- drop ( self . pty . take_unhandled_osc_sequences ( ) ) ;
127- Ok ( n)
128- }
129-
13047 /// Returns terminal screen contents with milestone hyperlink text removed.
13148 #[ must_use]
13249 pub fn screen_contents ( & self ) -> String {
133- let mut contents = self . pty . screen_contents ( ) ;
50+ let mut contents = self . pty . get_ref ( ) . screen_contents ( ) ;
13451 contents. retain ( |ch| ch != MILESTONE_HYPERTEXT ) ;
13552 contents
13653 }
@@ -139,20 +56,18 @@ impl Reader {
13956 /// Useful for snapshot tests that need to assert colour or style attributes.
14057 #[ must_use]
14158 pub fn screen_contents_formatted ( & self ) -> Vec < u8 > {
142- self . pty . screen_contents_formatted ( )
59+ self . pty . get_ref ( ) . screen_contents_formatted ( )
14360 }
14461
14562 /// Reads from the PTY until a milestone with the given name is encountered.
14663 ///
14764 /// Returns the terminal screen contents at the moment the milestone is detected.
14865 ///
14966 /// Milestones use a uniform protocol across platforms: the milestone name
150- /// is encoded in an OSC 8 hyperlink URI. A zero-width hyperlink anchor follows
151- /// each marker through the rendered output path. The reader waits for both the
152- /// marker and its corresponding anchor before returning, then strips the anchor
153- /// from the returned screen contents. Marker and anchor parsing is incremental,
154- /// so either sequence may be split across PTY reads or share a read with other
155- /// milestones.
67+ /// is encoded in an OSC 8 hyperlink URI. We parse unhandled OSC sequences
68+ /// from the VT parser state (instead of raw byte matching), then decode the
69+ /// milestone URI payload. The zero-width milestone hyperlink anchor is
70+ /// stripped from returned screen contents.
15671 ///
15772 /// # Panics
15873 ///
@@ -163,11 +78,20 @@ impl Reader {
16378 let mut buf = [ 0u8 ; 4096 ] ;
16479
16580 loop {
166- if self . milestone_tracker . take_completed ( name) {
81+ let found = self
82+ . pty
83+ . get_ref ( )
84+ . take_unhandled_osc_sequences ( )
85+ . into_iter ( )
86+ . filter_map ( |params| {
87+ pty_terminal_test_client:: decode_milestone_from_osc8_params ( & params)
88+ } )
89+ . any ( |decoded| decoded == name) ;
90+ if found {
16791 return self . screen_contents ( ) ;
16892 }
16993
170- let n = self . read ( & mut buf) . expect ( "PTY read failed" ) ;
94+ let n = self . pty . read ( & mut buf) . expect ( "PTY read failed" ) ;
17195 assert ! ( n > 0 , "EOF reached before milestone '{name}'" ) ;
17296 }
17397 }
@@ -182,75 +106,8 @@ impl Reader {
182106 ///
183107 /// Panics if reading from the PTY fails.
184108 pub fn wait_for_exit ( & mut self ) -> anyhow:: Result < ExitStatus > {
185- let mut buf = [ 0u8 ; 4096 ] ;
186- while self . read ( & mut buf ) . expect ( "PTY read failed" ) > 0 { }
109+ let mut discard = Vec :: new ( ) ;
110+ self . pty . read_to_end ( & mut discard ) . expect ( "PTY read_to_end failed" ) ;
187111 self . child_handle . wait ( )
188112 }
189113}
190-
191- #[ cfg( test) ]
192- mod tests {
193- use super :: * ;
194-
195- fn marker_without_fence ( name : & str ) -> Vec < u8 > {
196- // Model ConPTY's fast control path by delivering the complete OSC marker
197- // before its printable anchor reaches the output pipe.
198- let mut marker = pty_terminal_test_client:: encoded_milestone ( name) ;
199- let index = marker
200- . windows ( pty_terminal_test_client:: MILESTONE_RENDER_FENCE . len ( ) )
201- . position ( |window| window == pty_terminal_test_client:: MILESTONE_RENDER_FENCE )
202- . unwrap ( ) ;
203- marker. drain ( index..index + pty_terminal_test_client:: MILESTONE_RENDER_FENCE . len ( ) ) ;
204- marker
205- }
206-
207- fn advance ( parser : & mut vte:: Parser , tracker : & mut MilestoneTracker , bytes : & [ u8 ] ) {
208- parser. advance ( tracker, bytes) ;
209- }
210-
211- #[ test]
212- fn milestone_waits_for_rendered_fence ( ) {
213- let mut parser = vte:: Parser :: new ( ) ;
214- let mut tracker = MilestoneTracker :: default ( ) ;
215-
216- // Receiving the marker and subsequent printable output is insufficient:
217- // only the protocol's rendered anchor establishes the screen barrier.
218- advance ( & mut parser, & mut tracker, & marker_without_fence ( "target" ) ) ;
219- advance ( & mut parser, & mut tracker, b"rendered output" ) ;
220- assert ! ( !tracker. take_completed( "target" ) ) ;
221-
222- advance ( & mut parser, & mut tracker, pty_terminal_test_client:: MILESTONE_RENDER_FENCE ) ;
223- assert ! ( tracker. take_completed( "target" ) ) ;
224- }
225-
226- #[ test]
227- fn milestone_parses_across_every_chunk_boundary ( ) {
228- let marker = pty_terminal_test_client:: encoded_milestone ( "target" ) ;
229-
230- for split in 0 ..=marker. len ( ) {
231- let mut parser = vte:: Parser :: new ( ) ;
232- let mut tracker = MilestoneTracker :: default ( ) ;
233- advance ( & mut parser, & mut tracker, & marker[ ..split] ) ;
234- advance ( & mut parser, & mut tracker, & marker[ split..] ) ;
235- assert ! ( tracker. take_completed( "target" ) , "failed at split {split}" ) ;
236- }
237- }
238-
239- #[ test]
240- fn rendered_fences_complete_overtaken_markers_in_order ( ) {
241- let mut parser = vte:: Parser :: new ( ) ;
242- let mut tracker = MilestoneTracker :: default ( ) ;
243- let mut markers = marker_without_fence ( "first" ) ;
244- markers. extend ( marker_without_fence ( "second" ) ) ;
245-
246- // Both controls overtake rendering. The first anchor must still complete
247- // `first`, never whichever marker the caller happens to be waiting for.
248- advance ( & mut parser, & mut tracker, & markers) ;
249- advance ( & mut parser, & mut tracker, pty_terminal_test_client:: MILESTONE_RENDER_FENCE ) ;
250- assert ! ( tracker. take_completed( "first" ) ) ;
251- assert ! ( !tracker. take_completed( "second" ) ) ;
252-
253- advance ( & mut parser, & mut tracker, pty_terminal_test_client:: MILESTONE_RENDER_FENCE ) ;
254- assert ! ( tracker. take_completed( "second" ) ) ;
255- }
256- }
0 commit comments