Skip to content

Commit 21a0041

Browse files
committed
Stage 3: cover the command loop's own lifetime
The reservation seen from outside: a real cmdloop, start to finish. It takes the rows, gives them back, and leaves the application rendering through its own backend again -- and a second loop in the same process does it all over. A finalization hook that raises does not cost the toolbar. Finalization runs at the end of every command, which is exactly when the toolbar has to still be there, and it is the site the design singles out for keeping the reservation while the display is quiesced.
1 parent c9a5abe commit 21a0041

1 file changed

Lines changed: 74 additions & 0 deletions

File tree

‎tests/test_reserved_lifecycle.py‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from prompt_toolkit.shortcuts import PromptSession
1717

1818
import cmd2
19+
from cmd2.plugin import CommandFinalizationData
1920
from cmd2.reserved_toolbar import native_toolbar_container
2021

2122

@@ -602,3 +603,76 @@ class StuckDisplay:
602603
def complete_abandoned_shutdown(self) -> bool:
603604
"""Report that the teardown cannot be finished while the thread runs."""
604605
return False
606+
607+
608+
class TestCommandLoop:
609+
"""The reservation's lifetime seen from the outside: a real cmdloop, start to finish."""
610+
611+
def run_loop(self, harness: Harness, *commands: str) -> None:
612+
"""Feed the loop some commands and let it exit."""
613+
harness.pipe.send_text("".join(f"{command}\n" for command in (*commands, "quit")))
614+
harness.app.cmdloop()
615+
616+
def test_a_loop_installs_and_releases_the_reservation(self) -> None:
617+
harness = Harness(mode="reserved")
618+
try:
619+
self.run_loop(harness)
620+
written = harness.written()
621+
assert "\x1b[1;23r" in written
622+
assert written.rindex("\x1b[r") > written.index("\x1b[1;23r")
623+
assert harness.app.reserved_toolbar is None
624+
finally:
625+
harness.close()
626+
627+
def test_the_terminal_is_left_as_it_was_found(self) -> None:
628+
"""Margins reset, and the application rendering through its own backend again."""
629+
harness = Harness(mode="reserved")
630+
try:
631+
app = harness.app.main_session.app
632+
backend = harness.backend
633+
self.run_loop(harness)
634+
assert app.output is backend
635+
assert app.renderer.output is backend
636+
assert harness.written().endswith("\x1b[r") or "\x1b[r" in harness.written()
637+
finally:
638+
harness.close()
639+
640+
def test_a_second_loop_reserves_again(self) -> None:
641+
"""Repeated loops in one process: each takes the rows and gives them back."""
642+
harness = Harness(mode="reserved")
643+
try:
644+
self.run_loop(harness)
645+
harness.clear()
646+
self.run_loop(harness)
647+
written = harness.written()
648+
assert "\x1b[1;23r" in written
649+
assert "\x1b[r" in written
650+
assert harness.app.reserved_toolbar is None
651+
finally:
652+
harness.close()
653+
654+
def test_a_failing_finalization_hook_keeps_the_reservation(self) -> None:
655+
"""Finalization runs at the end of every command; a broken hook is not a lost toolbar."""
656+
reserved_during: list[bool] = []
657+
658+
def failing_hook(data: CommandFinalizationData) -> CommandFinalizationData:
659+
toolbar = harness.app.reserved_toolbar
660+
reserved_during.append(toolbar is not None and toolbar.display.is_reserved)
661+
raise RuntimeError("hook failed")
662+
663+
harness = Harness(mode="reserved")
664+
try:
665+
harness.app.register_cmdfinalization_hook(failing_hook)
666+
self.run_loop(harness, "help")
667+
assert reserved_during
668+
assert all(reserved_during)
669+
finally:
670+
harness.close()
671+
672+
def test_a_legacy_loop_reserves_nothing(self) -> None:
673+
harness = Harness(mode="legacy")
674+
try:
675+
self.run_loop(harness)
676+
assert "\x1b[1;23r" not in harness.written()
677+
finally:
678+
harness.close()

0 commit comments

Comments
 (0)