From 88b350b8294c8eb9965341e7bc1f92f6f665dd1d Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 16 Sep 2025 19:38:51 -0400 Subject: [PATCH 1/3] Move two test files that don't need to be isolated from tests_isolated to tests Also: - Remove unnecessary nested directory under tests_isolated for the one remaining test file there. This is the first step in fixing the tests so none of them need to be isolated. The tests in tests_isolated/test_commandset.py follow bad practices where classes are modified instead of instances of classes. --- tests/conftest.py | 69 +++++++++++++++++++ .../test_argparse_subcommands.py | 0 .../test_categories.py | 0 .../{test_commandset => }/conftest.py | 0 .../{test_commandset => }/test_commandset.py | 0 tests_isolated/test_commandset/__init__.py | 0 6 files changed, 69 insertions(+) rename {tests_isolated/test_commandset => tests}/test_argparse_subcommands.py (100%) rename {tests_isolated/test_commandset => tests}/test_categories.py (100%) rename tests_isolated/{test_commandset => }/conftest.py (100%) rename tests_isolated/{test_commandset => }/test_commandset.py (100%) delete mode 100644 tests_isolated/test_commandset/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py index 72e902602..7a9e49f74 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ from collections.abc import Callable from contextlib import redirect_stderr from typing import ( + TYPE_CHECKING, ParamSpec, TextIO, TypeVar, @@ -164,3 +165,71 @@ def find_subcommand(action: argparse.ArgumentParser, subcmd_names: list[str]) -> return find_subcommand(choice, subcmd_names) break raise ValueError(f"Could not find subcommand '{subcmd_names}'") + + +if TYPE_CHECKING: + _Base = cmd2.Cmd +else: + _Base = object + + +class ExternalTestMixin(_Base): + """A cmd2 plugin (mixin class) that exposes an interface to execute application commands from python""" + + def __init__(self, *args, **kwargs): + """ + + :type self: cmd2.Cmd + :param args: + :param kwargs: + """ + # code placed here runs before cmd2 initializes + super().__init__(*args, **kwargs) + assert isinstance(self, cmd2.Cmd) + # code placed here runs after cmd2 initializes + self._pybridge = cmd2.py_bridge.PyBridge(self) + + def app_cmd(self, command: str, echo: bool | None = None) -> cmd2.CommandResult: + """ + Run the application command + + :param command: The application command as it would be written on the cmd2 application prompt + :param echo: Flag whether the command's output should be echoed to stdout/stderr + :return: A CommandResult object that captures stdout, stderr, and the command's result object + """ + assert isinstance(self, cmd2.Cmd) + assert isinstance(self, ExternalTestMixin) + try: + self._in_py = True + + return self._pybridge(command, echo=echo) + + finally: + self._in_py = False + + def fixture_setup(self): + """ + Replicates the behavior of `cmdloop()` preparing the state of the application + :type self: cmd2.Cmd + """ + + for func in self._preloop_hooks: + func() + self.preloop() + + def fixture_teardown(self): + """ + Replicates the behavior of `cmdloop()` tearing down the application + + :type self: cmd2.Cmd + """ + for func in self._postloop_hooks: + func() + self.postloop() + + +class WithCommandSets(ExternalTestMixin, cmd2.Cmd): + """Class for testing custom help_* methods which override docstring help.""" + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) diff --git a/tests_isolated/test_commandset/test_argparse_subcommands.py b/tests/test_argparse_subcommands.py similarity index 100% rename from tests_isolated/test_commandset/test_argparse_subcommands.py rename to tests/test_argparse_subcommands.py diff --git a/tests_isolated/test_commandset/test_categories.py b/tests/test_categories.py similarity index 100% rename from tests_isolated/test_commandset/test_categories.py rename to tests/test_categories.py diff --git a/tests_isolated/test_commandset/conftest.py b/tests_isolated/conftest.py similarity index 100% rename from tests_isolated/test_commandset/conftest.py rename to tests_isolated/conftest.py diff --git a/tests_isolated/test_commandset/test_commandset.py b/tests_isolated/test_commandset.py similarity index 100% rename from tests_isolated/test_commandset/test_commandset.py rename to tests_isolated/test_commandset.py diff --git a/tests_isolated/test_commandset/__init__.py b/tests_isolated/test_commandset/__init__.py deleted file mode 100644 index e69de29bb..000000000 From 89b2aea19ac67685d04f346fc13f5d07a7da2128 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 16 Sep 2025 19:52:28 -0400 Subject: [PATCH 2/3] Improve documentation comments in ExternalTestMixin class in tests/conftest.py based on PR comments from gemini-cli --- tests/conftest.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7a9e49f74..f29a96556 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -177,15 +177,19 @@ class ExternalTestMixin(_Base): """A cmd2 plugin (mixin class) that exposes an interface to execute application commands from python""" def __init__(self, *args, **kwargs): - """ + """Initializes the ExternalTestMixin. + + This class is intended to be used in multiple inheritance alongside `cmd2.Cmd` for an application class. + When doing this multiple inheritance, it is imperative that this mixin class come first. :type self: cmd2.Cmd - :param args: - :param kwargs: + :param args: arguments to pass to the superclass + :param kwargs: keyword arguments to pass to the superclass """ # code placed here runs before cmd2 initializes super().__init__(*args, **kwargs) - assert isinstance(self, cmd2.Cmd) + if not isinstance(self, cmd2.Cmd): + raise TypeError('The ExternalTestMixin class is intended to be used in multiple inhertance with cmd2.Cmd') # code placed here runs after cmd2 initializes self._pybridge = cmd2.py_bridge.PyBridge(self) @@ -197,19 +201,19 @@ def app_cmd(self, command: str, echo: bool | None = None) -> cmd2.CommandResult: :param echo: Flag whether the command's output should be echoed to stdout/stderr :return: A CommandResult object that captures stdout, stderr, and the command's result object """ - assert isinstance(self, cmd2.Cmd) - assert isinstance(self, ExternalTestMixin) try: self._in_py = True - return self._pybridge(command, echo=echo) finally: self._in_py = False def fixture_setup(self): - """ - Replicates the behavior of `cmdloop()` preparing the state of the application + """Replicates the behavior of `cmdloop()` to prepare the application state for testing. + + This method runs all preloop hooks and the preloop method to ensure the + application is in the correct state before running a test. + :type self: cmd2.Cmd """ @@ -218,8 +222,10 @@ def fixture_setup(self): self.preloop() def fixture_teardown(self): - """ - Replicates the behavior of `cmdloop()` tearing down the application + """Replicates the behavior of `cmdloop()` to tear down the application after a test. + + This method runs all postloop hooks and the postloop method to clean up + the application state and ensure test isolation. :type self: cmd2.Cmd """ From 3194dbe0af3d7819d6545112e2592779476237fa Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Tue, 16 Sep 2025 19:53:24 -0400 Subject: [PATCH 3/3] Fix spelling error --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index f29a96556..b77f9659d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -189,7 +189,7 @@ def __init__(self, *args, **kwargs): # code placed here runs before cmd2 initializes super().__init__(*args, **kwargs) if not isinstance(self, cmd2.Cmd): - raise TypeError('The ExternalTestMixin class is intended to be used in multiple inhertance with cmd2.Cmd') + raise TypeError('The ExternalTestMixin class is intended to be used in multiple inheritance with cmd2.Cmd') # code placed here runs after cmd2 initializes self._pybridge = cmd2.py_bridge.PyBridge(self)