1010"""
1111
1212import io
13+ import threading
1314from concurrent .futures import Future
1415from typing import Any
1516
@@ -41,13 +42,20 @@ def isatty(self) -> bool:
4142class Harness :
4243 """A real application over a reserved terminal, with the stream it writes to."""
4344
44- def __init__ (self , rows : int = 24 , columns : int = 40 , reserved_rows : int = 1 , content : Any = "hello" ) -> None :
45+ def __init__ (
46+ self ,
47+ rows : int = 24 ,
48+ columns : int = 40 ,
49+ reserved_rows : int = 1 ,
50+ content : Any = "hello" ,
51+ lock : TerminalLock | None = None ,
52+ ) -> None :
4553 self .stream = TtyStringIO ()
4654 self .size = Size (rows = rows , columns = columns )
4755 self .backend = Vt100_Output (self .stream , lambda : self .size )
4856 self .display = TerminalDisplay (self .backend , reserved_rows = reserved_rows )
4957 assert self .display .acquire () is True
50- self .lock = TerminalLock ()
58+ self .lock = lock or TerminalLock ()
5159 self .app : Application [Any ] = Application (
5260 layout = Layout (Window (FormattedTextControl (content ))),
5361 output = self .display .output ,
@@ -208,7 +216,7 @@ def test_uncommitted_frame_metadata_is_not_dispatched(self) -> None:
208216 harness = Harness ()
209217 prepared = harness .prepare ()
210218 assert prepared is not None
211- harness .bridge .note_managed_write ()
219+ harness .bridge .note_managed_write (prompt_anchor = 1 )
212220 assert harness .bridge .can_dispatch_input is False
213221 assert harness .bridge .commit (prepared ) is False
214222 assert harness .bridge .can_dispatch_input is False
@@ -274,7 +282,7 @@ def test_discarded_frame_resynchronizes_terminal_modes(self) -> None:
274282 assert prepared is not None
275283 # Preparation advanced the flag even though the terminal saw nothing.
276284 assert harness .renderer ._bracketed_paste_enabled is True
277- harness .bridge .note_managed_write ()
285+ harness .bridge .note_managed_write (prompt_anchor = 1 )
278286 harness .bridge .commit (prepared )
279287 harness .clear ()
280288
@@ -291,7 +299,7 @@ def test_recovery_restores_the_baseline_only_after_a_full_frame(self) -> None:
291299 harness = Harness ()
292300 prepared = harness .prepare ()
293301 assert prepared is not None
294- harness .bridge .note_managed_write ()
302+ harness .bridge .note_managed_write (prompt_anchor = 1 )
295303 harness .bridge .commit (prepared )
296304 harness .resynchronize ()
297305 harness .clear ()
@@ -374,7 +382,7 @@ def test_repeated_invalidation_yields_to_managed_output(self) -> None:
374382 harness .bridge .set_redraw_scheduler (lambda : scheduled .append (1 ))
375383
376384 for _ in range (5 ):
377- harness .bridge .note_managed_write ()
385+ harness .bridge .note_managed_write (prompt_anchor = 1 )
378386
379387 assert len (scheduled ) == 1
380388 assert harness .bridge .redraw_pending is True
@@ -387,10 +395,10 @@ def test_a_redraw_is_requested_again_after_it_is_served(self) -> None:
387395 harness = Harness ()
388396 scheduled : list [int ] = []
389397 harness .bridge .set_redraw_scheduler (lambda : scheduled .append (1 ))
390- harness .bridge .note_managed_write ()
398+ harness .bridge .note_managed_write (prompt_anchor = 1 )
391399 harness .resynchronize ()
392400 harness .render ()
393- harness .bridge .note_managed_write ()
401+ harness .bridge .note_managed_write (prompt_anchor = 1 )
394402 assert len (scheduled ) == 2
395403
396404
@@ -660,3 +668,69 @@ def test_a_cursor_report_is_validated_against_the_screen_when_released(self) ->
660668 harness .bridge .request_cursor_position ()
661669 assert harness .bridge .report_cursor_row (24 ) is True
662670 assert harness .renderer ._min_available_height == 24 - 24 + 1
671+
672+
673+ class RetiringLock :
674+ """A lock that runs a callback at the moment it is handed over.
675+
676+ This stands in for another writer retiring the batch while a commit waits for the
677+ terminal. Driving that with two real threads cannot say *where* the second thread got to
678+ before the lock was released -- the interleaving the test is about is the one where the
679+ commit is already past its own checks -- so the handover itself is the seam to inject at.
680+ """
681+
682+ def __init__ (self ) -> None :
683+ self ._lock = threading .RLock ()
684+ self .on_acquire : Any = None
685+
686+ def acquire (self , * args : Any , ** kwargs : Any ) -> bool :
687+ acquired = self ._lock .acquire (* args , ** kwargs )
688+ if self .on_acquire is not None :
689+ callback , self .on_acquire = self .on_acquire , None
690+ callback ()
691+ return acquired
692+
693+ def release (self ) -> None :
694+ self ._lock .release ()
695+
696+
697+ class TestReviewRegressionsRoundTwo :
698+ def test_a_managed_write_without_an_origin_forgets_the_old_one (self ) -> None :
699+ """Review finding 1: the output moved the cursor, so the remembered row is stale."""
700+ harness = Harness ()
701+ harness .render ()
702+ assert harness .bridge .prompt_anchor == 1
703+ harness .bridge .note_managed_write ()
704+ assert harness .bridge .prompt_anchor is None
705+
706+ harness .clear ()
707+ harness .resynchronize ()
708+ written = harness .written ()
709+ assert "\x1b [1;1H" not in written
710+ assert "\x1b [6n" in written
711+ assert harness .bridge .needs_resynchronization is True
712+
713+ def test_a_frame_prepared_after_emission_stopped_is_not_published (self ) -> None :
714+ """Review finding 2: stopping is not the same state as owing a recovery."""
715+
716+ def content () -> str :
717+ harness .bridge .stop_reserved_emission (OSError ("terminal went away" ))
718+ return "hello"
719+
720+ harness = Harness (content = content )
721+ assert harness .prepare () is None
722+ assert harness .bridge .in_flight is None
723+ assert harness .bridge .reserved_emission_stopped is True
724+
725+ def test_a_frame_retired_while_the_commit_waits_is_not_emitted (self ) -> None :
726+ """Review finding 3: retirement only replaces an explicit guard if read under the lock."""
727+ handover = RetiringLock ()
728+ harness = Harness (lock = TerminalLock (lock = handover ))
729+ prepared = harness .prepare ()
730+ assert prepared is not None
731+ harness .clear ()
732+
733+ handover .on_acquire = lambda : harness .bridge .require_resynchronization ("another writer" )
734+ assert harness .bridge .commit (prepared ) is False
735+ assert harness .written () == ""
736+ assert harness .bridge .needs_resynchronization is True
0 commit comments