|
16 | 16 | from prompt_toolkit.shortcuts import PromptSession |
17 | 17 |
|
18 | 18 | import cmd2 |
| 19 | +from cmd2.plugin import CommandFinalizationData |
19 | 20 | from cmd2.reserved_toolbar import native_toolbar_container |
20 | 21 |
|
21 | 22 |
|
@@ -602,3 +603,76 @@ class StuckDisplay: |
602 | 603 | def complete_abandoned_shutdown(self) -> bool: |
603 | 604 | """Report that the teardown cannot be finished while the thread runs.""" |
604 | 605 | 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