From 20b638c4210681410dce6eda671514ac32583bd9 Mon Sep 17 00:00:00 2001 From: kolo Date: Fri, 5 Dec 2025 15:54:03 +0300 Subject: [PATCH 1/8] refactor tests --- justfile | 4 + mock/local_test.py | 17 ++- pyproject.toml | 8 ++ src/argenta/app/models.py | 11 +- src/argenta/command/models.py | 6 +- src/argenta/router/entity.py | 64 ++++----- tests/unit_tests/test_app.py | 157 ++++++++++++--------- tests/unit_tests/test_dividing_line.py | 45 ++++-- tests/unit_tests/test_router.py | 181 +++++++++++++------------ 9 files changed, 267 insertions(+), 226 deletions(-) diff --git a/justfile b/justfile index 1649082..b13b6a7 100644 --- a/justfile +++ b/justfile @@ -13,6 +13,10 @@ tests: tests-cov: python -m pytest --cov=argenta tests +# Запустить тесты с отчетом о покрытии с html репортом +tests-cov-html: + python -m pytest --cov=argenta tests --cov-report=html + # Отформатировать код (Ruff + isort) format: python -m ruff format ./src diff --git a/mock/local_test.py b/mock/local_test.py index 285a5ec..bfed62e 100644 --- a/mock/local_test.py +++ b/mock/local_test.py @@ -1,2 +1,15 @@ -import sys -print(sys.version_info >= (3, 13)) \ No newline at end of file +from argenta import App, Router, Command, Response + + +app = App(override_system_messages=True) +router = Router() + +@router.command(Command('test', aliases={'alias', 'primer'})) +def handler(res: Response): + pass + +@router.command(Command('test2', aliases={'alias', 'primer'})) +def handler2(res: Response): + pass + +print(router.aliases) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index be694df..eb84794 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,14 @@ line-length=90 [tool.pyright] typeCheckingMode = "strict" +[[tool.pyright.executionEnvironments]] +root = "tests/" +reportPrivateUsage = false +reportUnusedFunction = false + +[tool.coverage.run] +branch = true + [tool.mypy] disable_error_code = "import-untyped" diff --git a/src/argenta/app/models.py b/src/argenta/app/models.py index 328a87d..562a9a4 100644 --- a/src/argenta/app/models.py +++ b/src/argenta/app/models.py @@ -323,7 +323,7 @@ def _pre_cycle_setup(self) -> None: for router_entity in self.registered_routers: router_triggers = router_entity.triggers router_aliases = router_entity.aliases - combined = router_triggers + router_aliases + combined = router_triggers | router_aliases for trigger in combined: self._matching_default_triggers_with_routers[trigger] = router_entity @@ -331,15 +331,6 @@ def _pre_cycle_setup(self) -> None: self._autocompleter.initial_setup(list(self._current_matching_triggers_with_routers.keys())) - seen = {} - for item in list(self._current_matching_triggers_with_routers.keys()): - if item in seen: - Console().print( - f"\n[b red]WARNING:[/b red] Overlapping trigger or alias: [b blue]{item}[/b blue]" - ) - else: - seen[item] = True - if not self._override_system_messages: self._setup_default_view() diff --git a/src/argenta/command/models.py b/src/argenta/command/models.py index 1553b35..6290447 100644 --- a/src/argenta/command/models.py +++ b/src/argenta/command/models.py @@ -17,7 +17,7 @@ MIN_FLAG_PREFIX: str = "-" PREFIX_TYPE = Literal["-", "--", "---"] DEFAULT_WITHOUT_FLAGS: Flags = Flags() -DEFAULT_WITHOUT_ALIASES: list[Never] = [] +DEFAULT_WITHOUT_ALIASES: set[Never] = set() DEFAULT_WITHOUT_INPUT_FLAGS: InputFlags = InputFlags() @@ -29,7 +29,7 @@ def __init__( *, description: str = "Some useful command", flags: Flag | Flags = DEFAULT_WITHOUT_FLAGS, - aliases: list[str] | list[Never] = DEFAULT_WITHOUT_ALIASES, + aliases: set[str] | set[Never] = DEFAULT_WITHOUT_ALIASES, ): """ Public. The command that can and should be registered in the Router @@ -41,7 +41,7 @@ def __init__( self.registered_flags: Flags = flags if isinstance(flags, Flags) else Flags([flags]) self.trigger: str = trigger self.description: str = description - self.aliases: list[str] | list[Never] = aliases + self.aliases: set[str] | set[Never] = aliases def validate_input_flag(self, flag: InputFlag) -> ValidationStatus: """ diff --git a/src/argenta/router/entity.py b/src/argenta/router/entity.py index cb5d5dd..7c3934b 100644 --- a/src/argenta/router/entity.py +++ b/src/argenta/router/entity.py @@ -41,6 +41,9 @@ def __init__( self.command_handlers: CommandHandlers = CommandHandlers() self.command_register_ignore: bool = False + + self.aliases: set[str] = set() + self.triggers: set[str] = set() def command(self, command: Command | str) -> Callable[[HandlerFunc], HandlerFunc]: """ @@ -53,7 +56,13 @@ def command(self, command: Command | str) -> Callable[[HandlerFunc], HandlerFunc else: redefined_command = command - _validate_command(redefined_command) + self._validate_command(redefined_command) + + if overlapping := (self.aliases | self.triggers) & command.aliases: + Console().print(f"\n[b red]WARNING:[/b red] Overlapping trigger or alias: [b blue]{overlapping}[/b blue]") + + self.aliases.update(command.aliases) + self.triggers.add(command.trigger) def decorator(func: HandlerFunc) -> HandlerFunc: _validate_func_args(func) @@ -61,6 +70,20 @@ def decorator(func: HandlerFunc) -> HandlerFunc: return func return decorator + + def _validate_command(self, command: Command) -> None: + """ + Private. Validates the command registered in handler + :param command: validated command + :return: None if command is valid else raise exception + """ + command_name: str = command.trigger + if command_name.find(" ") != -1: + raise TriggerContainSpacesException() + flags: Flags = command.registered_flags + flags_name: list[str] = [flag.string_entity.lower() for flag in flags] + if len(set(flags_name)) < len(flags_name): + raise RepeatedFlagNameException() def finds_appropriate_handler(self, input_command: InputCommand) -> None: """ @@ -105,29 +128,6 @@ def process_input_command(self, input_command_flags: InputFlags, command_handler response = Response(ResponseStatus.ALL_FLAGS_VALID) command_handler.handling(response) - @property - def triggers(self) -> list[str]: - """ - Public. Gets registered triggers - :return: registered in router triggers as list[str] - """ - all_triggers: list[str] = [] - for command_handler in self.command_handlers: - all_triggers.append(command_handler.handled_command.trigger) - return all_triggers - - @property - def aliases(self) -> list[str]: - """ - Public. Gets registered aliases - :return: registered in router aliases as list[str] - """ - all_aliases: list[str] = [] - for command_handler in self.command_handlers: - if command_handler.handled_command.aliases: - all_aliases.extend(command_handler.handled_command.aliases) - return all_aliases - class CommandDecorator: def __init__(self, router_instance: Router, command: Command): @@ -188,18 +188,4 @@ def _validate_func_args(func: Callable[..., None]) -> None: + f" [i]but[/i] [bold blue]{response_arg_annotation}[/bold blue] [i]is specified[/i]", highlight=False, ) - - -def _validate_command(command: Command) -> None: - """ - Private. Validates the command registered in handler - :param command: validated command - :return: None if command is valid else raise exception - """ - command_name: str = command.trigger - if command_name.find(" ") != -1: - raise TriggerContainSpacesException() - flags: Flags = command.registered_flags - flags_name: list[str] = [flag.string_entity.lower() for flag in flags] - if len(set(flags_name)) < len(flags_name): - raise RepeatedFlagNameException() + \ No newline at end of file diff --git a/tests/unit_tests/test_app.py b/tests/unit_tests/test_app.py index a30dacf..bf382f8 100644 --- a/tests/unit_tests/test_app.py +++ b/tests/unit_tests/test_app.py @@ -1,74 +1,97 @@ -import unittest +from pytest import CaptureFixture from argenta.app import App +from argenta.response import Response from argenta.command.models import Command, InputCommand from argenta.router import Router -class MyTestCase(unittest.TestCase): - def test_is_exit_command1(self): - app = App() - self.assertEqual(app._is_exit_command(InputCommand('q')), True) - - def test_is_exit_command5(self): - app = App() - self.assertEqual(app._is_exit_command(InputCommand('Q')), True) - - def test_is_exit_command2(self): - app = App(ignore_command_register=False) - self.assertEqual(app._is_exit_command(InputCommand('q')), False) - - def test_is_exit_command3(self): - app = App(exit_command=Command('quit')) - self.assertEqual(app._is_exit_command(InputCommand('quit')), True) - - def test_is_exit_command4(self): - app = App(exit_command=Command('quit')) - self.assertEqual(app._is_exit_command(InputCommand('qUIt')), True) - - def test_is_exit_command6(self): - app = App(ignore_command_register=False, - exit_command=Command('quit')) - self.assertEqual(app._is_exit_command(InputCommand('qUIt')), False) - - def test_is_unknown_command1(self): - app = App() - app.set_unknown_command_handler(lambda command: None) - app._current_matching_triggers_with_routers = {'fr': Router(), 'tr': Router(), 'de': Router()} - self.assertEqual(app._is_unknown_command(InputCommand('fr')), False) - - def test_is_unknown_command2(self): - app = App() - app.set_unknown_command_handler(lambda command: None) - app._current_matching_triggers_with_routers = {'fr': Router(), 'tr': Router(), 'de': Router()} - self.assertEqual(app._is_unknown_command(InputCommand('cr')), True) - - def test_is_unknown_command3(self): - app = App(ignore_command_register=False) - app.set_unknown_command_handler(lambda command: None) - app._current_matching_triggers_with_routers = {'Pr': Router(), 'tW': Router(), 'deQW': Router()} - self.assertEqual(app._is_unknown_command(InputCommand('pr')), True) - - def test_is_unknown_command4(self): - app = App(ignore_command_register=False) - app.set_unknown_command_handler(lambda command: None) - app._current_matching_triggers_with_routers = {'Pr': Router(), 'tW': Router(), 'deQW': Router()} - self.assertEqual(app._is_unknown_command(InputCommand('tW')), False) - - - - - - - - - - - - - - - - - +def test_is_exit_command1(): + app = App() + assert app._is_exit_command(InputCommand('q')) is True + + +def test_is_exit_command5(): + app = App() + assert app._is_exit_command(InputCommand('Q')) is True + + +def test_is_exit_command2(): + app = App(ignore_command_register=False) + assert app._is_exit_command(InputCommand('q')) is False + + +def test_is_exit_command3(): + app = App(exit_command=Command('quit')) + assert app._is_exit_command(InputCommand('quit')) is True + + +def test_is_exit_command4(): + app = App(exit_command=Command('quit')) + assert app._is_exit_command(InputCommand('qUIt')) is True + + +def test_is_exit_command6(): + app = App(ignore_command_register=False, + exit_command=Command('quit')) + assert app._is_exit_command(InputCommand('qUIt')) is False + + +def test_is_unknown_command1(): + app = App() + app.set_unknown_command_handler(lambda command: None) + app._current_matching_triggers_with_routers = {'fr': Router(), 'tr': Router(), 'de': Router()} + assert app._is_unknown_command(InputCommand('fr')) is False + + +def test_is_unknown_command2(): + app = App() + app.set_unknown_command_handler(lambda command: None) + app._current_matching_triggers_with_routers = {'fr': Router(), 'tr': Router(), 'de': Router()} + assert app._is_unknown_command(InputCommand('cr')) is True + +def test_is_unknown_command3(): + app = App(ignore_command_register=False) + app.set_unknown_command_handler(lambda command: None) + app._current_matching_triggers_with_routers = {'Pr': Router(), 'tW': Router(), 'deQW': Router()} + assert app._is_unknown_command(InputCommand('pr')) is True + + +def test_is_unknown_command4(): + app = App(ignore_command_register=False) + app.set_unknown_command_handler(lambda command: None) + app._current_matching_triggers_with_routers = {'Pr': Router(), 'tW': Router(), 'deQW': Router()} + assert app._is_unknown_command(InputCommand('tW')) is False + +def test_add_messages_on_startup(): + app = App() + app.add_message_on_startup('Some message') + assert app._messages_on_startup == ['Some message'] + +def test_include_routers(): + app = App() + router = Router() + router2 = Router() + app.include_routers(router, router2) + + assert app.registered_routers.registered_routers == [router, router2] + +def test_overlapping_aliases(capsys: CaptureFixture[str]): + app = App(override_system_messages=True) + router = Router() + + @router.command(Command('test', aliases=['alias'])) + def handler(res: Response): + pass + + @router.command(Command('test2', aliases=['alias'])) + def handler2(res: Response): + pass + + app.include_routers(router) + app._pre_cycle_setup() + + captured = capsys.readouterr() + + assert "Overlapping" in captured.out diff --git a/tests/unit_tests/test_dividing_line.py b/tests/unit_tests/test_dividing_line.py index 2496d8d..151fd2a 100644 --- a/tests/unit_tests/test_dividing_line.py +++ b/tests/unit_tests/test_dividing_line.py @@ -1,21 +1,36 @@ -import unittest - from argenta.app.dividing_line import DynamicDividingLine, StaticDividingLine -class TestDividingLine(unittest.TestCase): - def test_get_static_dividing_line_full_line(self): - line = StaticDividingLine('-') - self.assertEqual(line.get_full_static_line(is_override=True).count('-'), 25) +def test_get_static_dividing_line_full_line(): + line = StaticDividingLine('-') + assert line.get_full_static_line(is_override=True).count('-') == 25 + +def test_get_static_dividing_line2_full_line(): + line = StaticDividingLine('-', length=5) + assert line.get_full_static_line(is_override=False) == '\n[dim]-----[/dim]\n' + +def test_get_dividing_line_unit_part(): + line = StaticDividingLine('') + assert line.get_unit_part() == ' ' + +def test_get_dividing_line2_unit_part(): + line = StaticDividingLine('+-0987654321!@#$%^&*()_') + assert line.get_unit_part() == '+' + +def test_get_dynamic_dividing_line_full_line(): + line = DynamicDividingLine() + assert line.get_full_dynamic_line(length=20, is_override=True).count('-') == 20 + +def test_get_dynamic_dividing_line2_full_line(): + line = DynamicDividingLine() + assert line.get_full_dynamic_line(length=5, is_override=False) == '\n[dim]-----[/dim]\n' + +def test_get_dynamic_dividing_line_unit_part(): + line = DynamicDividingLine('') + assert line.get_unit_part() == ' ' - def test_get_dynamic_dividing_line_full_line(self): - line = DynamicDividingLine() - self.assertEqual(line.get_full_dynamic_line(length=20, is_override=True).count('-'), 20) +def test_get_dynamic_dividing_line2_unit_part(): + line = DynamicDividingLine('45n352834528&^%@&*T$G') + assert line.get_unit_part() == '4' - def test_get_dividing_line_unit_part(self): - line = StaticDividingLine('') - self.assertEqual(line.get_unit_part(), ' ') - def test_get_dividing_line2_unit_part(self): - line = StaticDividingLine('+-0987654321!@#$%^&*()_') - self.assertEqual(line.get_unit_part(), '+') diff --git a/tests/unit_tests/test_router.py b/tests/unit_tests/test_router.py index 95ea8e9..073edea 100644 --- a/tests/unit_tests/test_router.py +++ b/tests/unit_tests/test_router.py @@ -1,5 +1,5 @@ import re -import unittest +import pytest from argenta.command import Command from argenta.command.flag import Flag, InputFlag @@ -7,97 +7,98 @@ from argenta.command.flag.models import PossibleValues, ValidationStatus from argenta.response.entity import Response from argenta.router import Router -from argenta.router.entity import _structuring_input_flags, _validate_command, _validate_func_args # pyright: ignore[reportPrivateUsage] +from argenta.router.entity import _structuring_input_flags, _validate_func_args # pyright: ignore[reportPrivateUsage] from argenta.router.exceptions import (RepeatedFlagNameException, RequiredArgumentNotPassedException, TriggerContainSpacesException) -class TestRouter(unittest.TestCase): - def test_register_command_with_spaces_in_trigger(self): - with self.assertRaises(TriggerContainSpacesException): - _validate_command(Command(trigger='command with spaces')) - - def test_register_command_with_repeated_flags(self): - with self.assertRaises(RepeatedFlagNameException): - _validate_command(Command(trigger='command', flags=Flags([Flag('test'), Flag('test')]))) - - def test_structuring_input_flags1(self): - cmd = Command('cmd') - input_flags = InputFlags([InputFlag('ssh', input_value=None, status=None)]) - self.assertEqual(_structuring_input_flags(cmd, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value=None, status=ValidationStatus.UNDEFINED)])) - - def test_structuring_input_flags2(self): - cmd = Command('cmd') - input_flags = InputFlags([InputFlag('ssh', input_value='some', status=None)]) - self.assertEqual(_structuring_input_flags(cmd, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value='some', status=ValidationStatus.UNDEFINED)])) - - def test_structuring_input_flags3(self): - cmd = Command('cmd', flags=Flag('port')) - input_flags = InputFlags([InputFlag('ssh', input_value='some2', status=None)]) - self.assertEqual(_structuring_input_flags(cmd, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value='some2', status=ValidationStatus.UNDEFINED)])) - - def test_structuring_input_flags4(self): - command = Command('cmd', flags=Flag('ssh', possible_values=PossibleValues.NEITHER)) - input_flags = InputFlags([InputFlag('ssh', input_value='some3', status=None)]) - self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value='some3', status=ValidationStatus.INVALID)])) - - def test_structuring_input_flags5(self): - command = Command('cmd', flags=Flag('ssh', possible_values=re.compile(r'some[1-5]$'))) - input_flags = InputFlags([InputFlag('ssh', input_value='some40', status=None)]) - self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value='some40', status=ValidationStatus.INVALID)])) - - def test_structuring_input_flags6(self): - command = Command('cmd', flags=Flag('ssh', possible_values=['example'])) - input_flags = InputFlags([InputFlag('ssh', input_value='example2', status=None)]) - self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value='example2', status=ValidationStatus.INVALID)])) - - def test_structuring_input_flags7(self): - command = Command('cmd', flags=Flag('port')) - input_flags = InputFlags([InputFlag('port', input_value='some2', status=None)]) - self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('port', input_value='some2', status=ValidationStatus.VALID)])) - - def test_structuring_input_flags8(self): - command = Command('cmd', flags=Flag('port', possible_values=['some2', 'some3'])) - input_flags = InputFlags([InputFlag('port', input_value='some2', status=None)]) - self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('port', input_value='some2', status=ValidationStatus.VALID)])) - - def test_structuring_input_flags9(self): - command = Command('cmd', flags=Flag('ssh', possible_values=re.compile(r'more[1-5]$'))) - input_flags = InputFlags([InputFlag('ssh', input_value='more5', status=None)]) - self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value='more5', status=ValidationStatus.VALID)])) - - def test_structuring_input_flags10(self): - command = Command('cmd', flags=Flag('ssh', possible_values=PossibleValues.NEITHER)) - input_flags = InputFlags([InputFlag('ssh', input_value=None, status=None)]) - self.assertEqual(_structuring_input_flags(command, input_flags).input_flags, InputFlags([InputFlag('ssh', input_value=None, status=ValidationStatus.VALID)])) - - def test_validate_incorrect_func_args1(self): - def handler(): - pass - with self.assertRaises(RequiredArgumentNotPassedException): - _validate_func_args(handler) # pyright: ignore[reportArgumentType] - - def test_get_router_aliases(self): - router = Router() - @router.command(Command('some', aliases=['test', 'case'])) - def handler(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - pass - self.assertListEqual(router.aliases, ['test', 'case']) - - def test_get_router_aliases2(self): - router = Router() - @router.command(Command('some', aliases=['test', 'case'])) - def handler(response: Response): # pyright: ignore[reportUnusedFunction] - pass - @router.command(Command('ext', aliases=['more', 'foo'])) - def handler2(response: Response): # pyright: ignore[reportUnusedFunction] - pass - self.assertListEqual(router.aliases, ['test', 'case', 'more', 'foo']) - - def test_get_router_aliases3(self): - router = Router() - @router.command(Command('some')) - def handler(response: Response): # pyright: ignore[reportUnusedFunction] - pass - self.assertListEqual(router.aliases, []) +def test_register_command_with_spaces_in_trigger(): + router = Router() + with pytest.raises(TriggerContainSpacesException): + router._validate_command(Command(trigger='command with spaces')) + +def test_register_command_with_repeated_flags(): + router = Router() + with pytest.raises(RepeatedFlagNameException): + router._validate_command(Command(trigger='command', flags=Flags([Flag('test'), Flag('test')]))) + +def test_structuring_input_flags1(): + cmd = Command('cmd') + input_flags = InputFlags([InputFlag('ssh', input_value='', status=None)]) + assert _structuring_input_flags(cmd, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='', status=ValidationStatus.UNDEFINED)]) + +def test_structuring_input_flags2(): + cmd = Command('cmd') + input_flags = InputFlags([InputFlag('ssh', input_value='some', status=None)]) + assert _structuring_input_flags(cmd, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some', status=ValidationStatus.UNDEFINED)]) + +def test_structuring_input_flags3(): + cmd = Command('cmd', flags=Flag('port')) + input_flags = InputFlags([InputFlag('ssh', input_value='some2', status=None)]) + assert _structuring_input_flags(cmd, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some2', status=ValidationStatus.UNDEFINED)]) + +def test_structuring_input_flags4(): + command = Command('cmd', flags=Flag('ssh', possible_values=PossibleValues.NEITHER)) + input_flags = InputFlags([InputFlag('ssh', input_value='some3', status=None)]) + assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some3', status=ValidationStatus.INVALID)]) + +def test_structuring_input_flags5(): + command = Command('cmd', flags=Flag('ssh', possible_values=re.compile(r'some[1-5]$'))) + input_flags = InputFlags([InputFlag('ssh', input_value='some40', status=None)]) + assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some40', status=ValidationStatus.INVALID)]) + +def test_structuring_input_flags6(): + command = Command('cmd', flags=Flag('ssh', possible_values=['example'])) + input_flags = InputFlags([InputFlag('ssh', input_value='example2', status=None)]) + assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='example2', status=ValidationStatus.INVALID)]) + +def test_structuring_input_flags7(): + command = Command('cmd', flags=Flag('port')) + input_flags = InputFlags([InputFlag('port', input_value='some2', status=None)]) + assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('port', input_value='some2', status=ValidationStatus.VALID)]) + +def test_structuring_input_flags8(): + command = Command('cmd', flags=Flag('port', possible_values=['some2', 'some3'])) + input_flags = InputFlags([InputFlag('port', input_value='some2', status=None)]) + assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('port', input_value='some2', status=ValidationStatus.VALID)]) + +def test_structuring_input_flags9(): + command = Command('cmd', flags=Flag('ssh', possible_values=re.compile(r'more[1-5]$'))) + input_flags = InputFlags([InputFlag('ssh', input_value='more5', status=None)]) + assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='more5', status=ValidationStatus.VALID)]) + +def test_structuring_input_flags10(): + command = Command('cmd', flags=Flag('ssh', possible_values=PossibleValues.NEITHER)) + input_flags = InputFlags([InputFlag('ssh', input_value='', status=None)]) + assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='', status=ValidationStatus.VALID)]) + +def test_validate_incorrect_func_args1(): + def handler(): + pass + with pytest.raises(RequiredArgumentNotPassedException): + _validate_func_args(handler) # pyright: ignore[reportArgumentType] + +def test_get_router_aliases(): + router = Router() + @router.command(Command('some', aliases={'test', 'case'})) + def handler(response: Response) -> None: + pass + assert router.aliases == {'test', 'case'} + +def test_get_router_aliases2(): + router = Router() + @router.command(Command('some', aliases={'test', 'case'})) + def handler(response: Response): + pass + @router.command(Command('ext', aliases={'more', 'foo'})) + def handler2(response: Response): + pass + assert router.aliases == {'test', 'case', 'more', 'foo'} + +def test_get_router_aliases3(): + router = Router() + @router.command(Command('some')) + def handler(response: Response): + pass + assert router.aliases == set() From 913e7f16caef06b3dba63339fa8b50d3a571c062 Mon Sep 17 00:00:00 2001 From: kolo Date: Fri, 5 Dec 2025 16:30:56 +0300 Subject: [PATCH 2/8] refactor tests --- docs/root/api/app/index.rst | 2 +- src/argenta/app/models.py | 2 +- src/argenta/router/entity.py | 6 +- tests/unit_tests/test_app.py | 191 +++++++++++++++++++++++++++++++---- 4 files changed, 174 insertions(+), 27 deletions(-) diff --git a/docs/root/api/app/index.rst b/docs/root/api/app/index.rst index 1fa883d..f099c18 100644 --- a/docs/root/api/app/index.rst +++ b/docs/root/api/app/index.rst @@ -30,7 +30,7 @@ App system_router_title: str | None = "System points:", ignore_command_register: bool = True, dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE, - repeat_command_groups_printing: bool = True, + repeat_command_groups_printing: bool = False, override_system_messages: bool = False, autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER, print_func: Printer = DEFAULT_PRINT_FUNC) -> None diff --git a/src/argenta/app/models.py b/src/argenta/app/models.py index 562a9a4..c1d76a8 100644 --- a/src/argenta/app/models.py +++ b/src/argenta/app/models.py @@ -363,7 +363,7 @@ def __init__( system_router_title: str | None = "System points:", ignore_command_register: bool = True, dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE, - repeat_command_groups_printing: bool = True, + repeat_command_groups_printing: bool = False, override_system_messages: bool = False, autocompleter: AutoCompleter = DEFAULT_AUTOCOMPLETER, print_func: Printer = DEFAULT_PRINT_FUNC, diff --git a/src/argenta/router/entity.py b/src/argenta/router/entity.py index 7c3934b..972bf1d 100644 --- a/src/argenta/router/entity.py +++ b/src/argenta/router/entity.py @@ -58,11 +58,11 @@ def command(self, command: Command | str) -> Callable[[HandlerFunc], HandlerFunc self._validate_command(redefined_command) - if overlapping := (self.aliases | self.triggers) & command.aliases: + if overlapping := (self.aliases | self.triggers) & redefined_command.aliases: Console().print(f"\n[b red]WARNING:[/b red] Overlapping trigger or alias: [b blue]{overlapping}[/b blue]") - self.aliases.update(command.aliases) - self.triggers.add(command.trigger) + self.aliases.update(redefined_command.aliases) + self.triggers.add(redefined_command.trigger) def decorator(func: HandlerFunc) -> HandlerFunc: _validate_func_args(func) diff --git a/tests/unit_tests/test_app.py b/tests/unit_tests/test_app.py index bf382f8..a6249ec 100644 --- a/tests/unit_tests/test_app.py +++ b/tests/unit_tests/test_app.py @@ -1,14 +1,17 @@ +from argenta.app.dividing_line import DynamicDividingLine, StaticDividingLine +from argenta.app.protocols import DescriptionMessageGenerator, NonStandardBehaviorHandler from pytest import CaptureFixture from argenta.app import App from argenta.response import Response from argenta.command.models import Command, InputCommand from argenta.router import Router +import pytest def test_is_exit_command1(): app = App() - assert app._is_exit_command(InputCommand('q')) is True + assert app._is_exit_command(InputCommand('q')) is True def test_is_exit_command5(): @@ -18,80 +21,224 @@ def test_is_exit_command5(): def test_is_exit_command2(): app = App(ignore_command_register=False) - assert app._is_exit_command(InputCommand('q')) is False + assert app._is_exit_command(InputCommand('q')) is False def test_is_exit_command3(): app = App(exit_command=Command('quit')) - assert app._is_exit_command(InputCommand('quit')) is True + assert app._is_exit_command(InputCommand('quit')) is True def test_is_exit_command4(): app = App(exit_command=Command('quit')) - assert app._is_exit_command(InputCommand('qUIt')) is True + assert app._is_exit_command(InputCommand('qUIt')) is True def test_is_exit_command6(): app = App(ignore_command_register=False, exit_command=Command('quit')) - assert app._is_exit_command(InputCommand('qUIt')) is False + assert app._is_exit_command(InputCommand('qUIt')) is False def test_is_unknown_command1(): app = App() app.set_unknown_command_handler(lambda command: None) - app._current_matching_triggers_with_routers = {'fr': Router(), 'tr': Router(), 'de': Router()} - assert app._is_unknown_command(InputCommand('fr')) is False + app._current_matching_triggers_with_routers = {'fr': Router(), 'tr': Router(), 'de': Router()} + assert app._is_unknown_command(InputCommand('fr')) is False def test_is_unknown_command2(): app = App() app.set_unknown_command_handler(lambda command: None) - app._current_matching_triggers_with_routers = {'fr': Router(), 'tr': Router(), 'de': Router()} - assert app._is_unknown_command(InputCommand('cr')) is True + app._current_matching_triggers_with_routers = {'fr': Router(), 'tr': Router(), 'de': Router()} + assert app._is_unknown_command(InputCommand('cr')) is True def test_is_unknown_command3(): app = App(ignore_command_register=False) app.set_unknown_command_handler(lambda command: None) - app._current_matching_triggers_with_routers = {'Pr': Router(), 'tW': Router(), 'deQW': Router()} - assert app._is_unknown_command(InputCommand('pr')) is True + app._current_matching_triggers_with_routers = {'Pr': Router(), 'tW': Router(), 'deQW': Router()} + assert app._is_unknown_command(InputCommand('pr')) is True def test_is_unknown_command4(): app = App(ignore_command_register=False) app.set_unknown_command_handler(lambda command: None) - app._current_matching_triggers_with_routers = {'Pr': Router(), 'tW': Router(), 'deQW': Router()} - assert app._is_unknown_command(InputCommand('tW')) is False - + app._current_matching_triggers_with_routers = {'Pr': Router(), 'tW': Router(), 'deQW': Router()} + assert app._is_unknown_command(InputCommand('tW')) is False + def test_add_messages_on_startup(): app = App() app.add_message_on_startup('Some message') assert app._messages_on_startup == ['Some message'] - + def test_include_routers(): app = App() router = Router() router2 = Router() app.include_routers(router, router2) - + assert app.registered_routers.registered_routers == [router, router2] - + def test_overlapping_aliases(capsys: CaptureFixture[str]): app = App(override_system_messages=True) router = Router() - - @router.command(Command('test', aliases=['alias'])) + + @router.command(Command('test', aliases={'alias'})) def handler(res: Response): pass - - @router.command(Command('test2', aliases=['alias'])) + + @router.command(Command('test2', aliases={'alias'})) def handler2(res: Response): pass app.include_routers(router) app._pre_cycle_setup() + + captured = capsys.readouterr() + + assert "Overlapping" in captured.out + +def test_print_framed_text(capsys: CaptureFixture[str]): + app = App( + override_system_messages=True, + dividing_line=StaticDividingLine(length=5) + ) + app._print_framed_text('test') + + captured = capsys.readouterr() + + assert '\n-----\n\ntest\n\n-----\n' in captured.out +def test_print_framed_text2(capsys: CaptureFixture[str]): + app = App( + override_system_messages=True, + dividing_line=DynamicDividingLine() + ) + app._print_framed_text('test as test as test') + captured = capsys.readouterr() + + assert '\n' + '-'*20 + '\n\ntest as test as test\n\n' + '-'*20 + '\n' in captured.out - assert "Overlapping" in captured.out +def test_print_framed_text3(capsys: CaptureFixture[str]): + app = App( + override_system_messages=True, + dividing_line=DynamicDividingLine() + ) + app._print_framed_text('some long test') + captured = capsys.readouterr() + + assert '\n--------------\n\nsome long test\n\n--------------\n' in captured.out + +def test_print_framed_text4(capsys: CaptureFixture[str]): + class OtherDividingLine: + pass + + app = App( + override_system_messages=True, + dividing_line=OtherDividingLine() # pyright: ignore[reportArgumentType] + ) + + with pytest.raises(NotImplementedError): + app._print_framed_text('some long test') + +def test_most_similiar_command(): + app = App(override_system_messages=True) + router = Router() + + @router.command(Command('port', aliases={'p'})) + def handler(res: Response): + pass + + @router.command(Command('host', aliases={'h'})) + def handler2(res: Response): + pass + + app.include_routers(router) + app._pre_cycle_setup() + + assert app._most_similar_command('por') == 'port' + +def test_most_similiar_command2(): + app = App(override_system_messages=True) + router = Router() + + @router.command(Command('command')) + def handler(res: Response): + pass + + @router.command(Command('command_other')) + def handler2(res: Response): + pass + + app.include_routers(router) + app._pre_cycle_setup() + + assert app._most_similar_command('com') == 'command' + +def test_most_similiar_command3(): + app = App(override_system_messages=True) + router = Router() + + @router.command(Command('command')) + def handler(res: Response): + pass + + @router.command(Command('command_other')) + def handler2(res: Response): + pass + + app.include_routers(router) + app._pre_cycle_setup() + + assert app._most_similar_command('command_') == 'command_other' + +def test_most_similiar_command4(): + app = App(override_system_messages=True) + router = Router() + + @router.command(Command('command')) + def handler(res: Response): + pass + + @router.command(Command('command_other')) + def handler2(res: Response): + pass + + app.include_routers(router) + app._pre_cycle_setup() + + assert app._most_similar_command('nonexists') is None + +def test_most_similiar_command3(): + app = App(override_system_messages=True) + router = Router() + + @router.command(Command('command', aliases={'other_name'})) + def handler(res: Response): + pass + + @router.command(Command('command_other', aliases={'more_name'})) + def handler2(res: Response): + pass + + app.include_routers(router) + app._pre_cycle_setup() + + assert app._most_similar_command('othe') == 'other_name' + +def test_app_set_description_message_gen(): + app = App() + descr_gen: DescriptionMessageGenerator = lambda command, description: print(command + '-+-' + description) + app.set_description_message_pattern(descr_gen) + + assert app._description_message_gen is descr_gen + +def test_app_set_exit_handler(): + app = App() + handler: NonStandardBehaviorHandler[Response] = lambda response: print('goodbye') + app.set_exit_command_handler(handler) + + assert app._exit_command_handler is handler + From 2423f570006f3129a715cc481172727d1d7dba64 Mon Sep 17 00:00:00 2001 From: kolo Date: Fri, 5 Dec 2025 22:57:19 +0300 Subject: [PATCH 3/8] new tests, upgrade tests coverage --- mock/local_test.py | 18 ++---- pyproject.toml | 4 ++ src/argenta/app/models.py | 61 +++++++++---------- src/argenta/app/protocols.py | 7 ++- src/argenta/app/registered_routers/entity.py | 3 - src/argenta/router/entity.py | 4 +- .../test_system_handling_normal_behavior.py | 7 ++- tests/unit_tests/test_app.py | 58 +++++++++++++++++- 8 files changed, 104 insertions(+), 58 deletions(-) diff --git a/mock/local_test.py b/mock/local_test.py index bfed62e..e526fdf 100644 --- a/mock/local_test.py +++ b/mock/local_test.py @@ -1,15 +1,5 @@ -from argenta import App, Router, Command, Response +from argenta import App - -app = App(override_system_messages=True) -router = Router() - -@router.command(Command('test', aliases={'alias', 'primer'})) -def handler(res: Response): - pass - -@router.command(Command('test2', aliases={'alias', 'primer'})) -def handler2(res: Response): - pass - -print(router.aliases) \ No newline at end of file +app = App() +app._setup_default_view() +app._empty_input_command_handler() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index eb84794..6e1f23e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,10 @@ reportUnusedFunction = false [tool.coverage.run] branch = true +omit = [ + "src/argenta/app/protocols.py", + "src/argenta/command/exceptions.py" +] [tool.mypy] disable_error_code = "import-untyped" diff --git a/src/argenta/app/models.py b/src/argenta/app/models.py index c1d76a8..af50cb1 100644 --- a/src/argenta/app/models.py +++ b/src/argenta/app/models.py @@ -19,7 +19,6 @@ ) from argenta.app.registered_routers.entity import RegisteredRouters from argenta.command.exceptions import ( - EmptyInputCommandException, InputCommandException, RepeatedInputFlagsException, UnprocessedInputFlagException, @@ -40,7 +39,7 @@ def __init__( initial_message: str, farewell_message: str, exit_command: Command, - system_router_title: str | None, + system_router_title: str, ignore_command_register: bool, dividing_line: StaticDividingLine | DynamicDividingLine, repeat_command_groups_printing: bool, @@ -51,7 +50,7 @@ def __init__( self._prompt: str = prompt self._print_func: Printer = print_func self._exit_command: Command = exit_command - self._system_router_title: str | None = system_router_title + self._system_router_title: str = system_router_title self._dividing_line: StaticDividingLine | DynamicDividingLine = dividing_line self._ignore_command_register: bool = ignore_command_register self._repeat_command_groups_printing_description: bool = repeat_command_groups_printing @@ -144,8 +143,7 @@ def _print_command_group_description(self) -> None: :return: None """ for registered_router in self.registered_routers: - if registered_router.title: - self._print_func(registered_router.title) + self._print_func(registered_router.title) for command_handler in registered_router.command_handlers: handled_command = command_handler.handled_command self._print_func( @@ -239,7 +237,7 @@ def _error_handler(self, error: InputCommandException, raw_command: str) -> None self._incorrect_input_syntax_handler(raw_command) elif isinstance(error, RepeatedInputFlagsException): self._repeated_input_flags_handler(raw_command) - elif isinstance(error, EmptyInputCommandException): + else: self._empty_input_command_handler() def _setup_system_router(self) -> None: @@ -253,9 +251,8 @@ def _setup_system_router(self) -> None: def _(response: Response) -> None: self._exit_command_handler(response) - if system_router not in self.registered_routers.registered_routers: - system_router.command_register_ignore = self._ignore_command_register - self.registered_routers.add_registered_router(system_router) + system_router.command_register_ignore = self._ignore_command_register + self.registered_routers.add_registered_router(system_router) def _most_similar_command(self, unknown_command: str) -> str | None: all_commands = list(self._current_matching_triggers_with_routers.keys()) @@ -342,6 +339,28 @@ def _pre_cycle_setup(self) -> None: print("\n") if not self._repeat_command_groups_printing_description: self._print_command_group_description() + + def _process_exist_and_valid_command(self, input_command: InputCommand): + processing_router = self._current_matching_triggers_with_routers[input_command.trigger.lower()] + + if processing_router.disable_redirect_stdout: + dividing_line_unit_part: str = self._dividing_line.get_unit_part() + self._print_func( + StaticDividingLine(dividing_line_unit_part).get_full_static_line( + is_override=self._override_system_messages + ) + ) + processing_router.finds_appropriate_handler(input_command) + self._print_func( + StaticDividingLine(dividing_line_unit_part).get_full_static_line( + is_override=self._override_system_messages + ) + ) + else: + with redirect_stdout(io.StringIO()) as stdout: + processing_router.finds_appropriate_handler(input_command) + stdout_result: str = stdout.getvalue() + self._print_framed_text(stdout_result) AVAILABLE_DIVIDING_LINES: TypeAlias = StaticDividingLine | DynamicDividingLine @@ -360,7 +379,7 @@ def __init__( initial_message: str = "Argenta\n", farewell_message: str = "\nSee you\n", exit_command: Command = DEFAULT_EXIT_COMMAND, - system_router_title: str | None = "System points:", + system_router_title: str = "System points:", ignore_command_register: bool = True, dividing_line: AVAILABLE_DIVIDING_LINES = DEFAULT_DIVIDING_LINE, repeat_command_groups_printing: bool = False, @@ -433,27 +452,7 @@ def run_polling(self) -> None: self._print_framed_text(stdout_res) continue - processing_router = self._current_matching_triggers_with_routers[input_command.trigger.lower()] - - if processing_router.disable_redirect_stdout: - dividing_line_unit_part: str = self._dividing_line.get_unit_part() - self._print_func( - StaticDividingLine(dividing_line_unit_part).get_full_static_line( - is_override=self._override_system_messages - ) - ) - processing_router.finds_appropriate_handler(input_command) - self._print_func( - StaticDividingLine(dividing_line_unit_part).get_full_static_line( - is_override=self._override_system_messages - ) - ) - else: - with redirect_stdout(io.StringIO()) as stdout: - processing_router.finds_appropriate_handler(input_command) - stdout_result: str = stdout.getvalue() - if stdout_result: - self._print_framed_text(stdout_result) + self._process_exist_and_valid_command(input_command) def include_router(self, router: Router) -> None: """ diff --git a/src/argenta/app/protocols.py b/src/argenta/app/protocols.py index 8300be5..530b520 100644 --- a/src/argenta/app/protocols.py +++ b/src/argenta/app/protocols.py @@ -2,11 +2,12 @@ from typing import Protocol, TypeVar + T = TypeVar("T", contravariant=True) # noqa: WPS111 class NonStandardBehaviorHandler(Protocol[T]): - def __call__(self, __param: T) -> None: + def __call__(self, _param: T, /) -> None: raise NotImplementedError @@ -16,10 +17,10 @@ def __call__(self) -> None: class Printer(Protocol): - def __call__(self, __text: str) -> None: + def __call__(self, _text: str, /) -> None: raise NotImplementedError class DescriptionMessageGenerator(Protocol): - def __call__(self, __first_param: str, __second_param: str) -> str: + def __call__(self, _command: str, _description: str, /) -> str: raise NotImplementedError diff --git a/src/argenta/app/registered_routers/entity.py b/src/argenta/app/registered_routers/entity.py index 2212389..366676e 100644 --- a/src/argenta/app/registered_routers/entity.py +++ b/src/argenta/app/registered_routers/entity.py @@ -24,6 +24,3 @@ def add_registered_router(self, router: Router, /) -> None: def __iter__(self) -> Iterator[Router]: return iter(self.registered_routers) - - def __next__(self) -> Router: - return next(iter(self.registered_routers)) diff --git a/src/argenta/router/entity.py b/src/argenta/router/entity.py index 972bf1d..fa9a5ea 100644 --- a/src/argenta/router/entity.py +++ b/src/argenta/router/entity.py @@ -22,7 +22,7 @@ class Router: def __init__( self, - title: str | None = "Default title", + title: str = "Default title", *, disable_redirect_stdout: bool = False, ): @@ -36,7 +36,7 @@ def __init__( which is ambiguous behavior and can lead to unexpected work :return: None """ - self.title: str | None = title + self.title: str = title self.disable_redirect_stdout: bool = disable_redirect_stdout self.command_handlers: CommandHandlers = CommandHandlers() diff --git a/tests/system_tests/test_system_handling_normal_behavior.py b/tests/system_tests/test_system_handling_normal_behavior.py index 3ebc80d..92ade73 100644 --- a/tests/system_tests/test_system_handling_normal_behavior.py +++ b/tests/system_tests/test_system_handling_normal_behavior.py @@ -99,8 +99,11 @@ def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] if valid_flag and valid_flag.status == ValidationStatus.VALID: print(f'flag value for {valid_flag.name} flag : {valid_flag.input_value}') - app = App(override_system_messages=True, - print_func=print) + app = App( + override_system_messages=True, + repeat_command_groups_printing=True, + print_func=print + ) app.include_router(router) orchestrator.start_polling(app) diff --git a/tests/unit_tests/test_app.py b/tests/unit_tests/test_app.py index a6249ec..7e88545 100644 --- a/tests/unit_tests/test_app.py +++ b/tests/unit_tests/test_app.py @@ -131,7 +131,7 @@ def test_print_framed_text3(capsys: CaptureFixture[str]): assert '\n--------------\n\nsome long test\n\n--------------\n' in captured.out -def test_print_framed_text4(capsys: CaptureFixture[str]): +def test_print_framed_text4(): class OtherDividingLine: pass @@ -211,7 +211,7 @@ def handler2(res: Response): assert app._most_similar_command('nonexists') is None -def test_most_similiar_command3(): +def test_most_similiar_command5(): app = App(override_system_messages=True) router = Router() @@ -230,7 +230,7 @@ def handler2(res: Response): def test_app_set_description_message_gen(): app = App() - descr_gen: DescriptionMessageGenerator = lambda command, description: print(command + '-+-' + description) + descr_gen: DescriptionMessageGenerator = lambda command, description: command + '-+-' + description app.set_description_message_pattern(descr_gen) assert app._description_message_gen is descr_gen @@ -242,3 +242,55 @@ def test_app_set_exit_handler(): assert app._exit_command_handler is handler +def test_app_is_exit_command(): + app = App(exit_command=Command('q', aliases={'exit'})) + assert app._is_exit_command(InputCommand('exit')) is True + +def test_app_is_exit_command2(): + app = App(exit_command=Command('q', aliases={'exit'}), ignore_command_register=False) + assert app._is_exit_command(InputCommand('exit')) is True + +def test_app_is_exit_command3(): + app = App(exit_command=Command('q', aliases={'exit'})) + assert app._is_exit_command(InputCommand('quit')) is False + +def test_app_is_exit_command4(): + app = App(exit_command=Command('q', aliases={'exit'}), ignore_command_register=False) + assert app._is_exit_command(InputCommand('Exit')) is False + +def test_app_setup_default_view(): + app = App(prompt='>>') + app._setup_default_view() + + assert app._prompt == '[italic dim bold]>>' + +def test_app_setup_default_view2(): + app = App() + app._setup_default_view() + assert app._unknown_command_handler(InputCommand('nonexists')) is None + +def test_app_pre_cycle_setup(capsys: CaptureFixture[str]): + app = App() + app.add_message_on_startup('some message') + app._pre_cycle_setup() + stdout = capsys.readouterr() + + assert 'some message' in stdout.out + +def test_app_with_router_with_disable_redirect_stdout(capsys: CaptureFixture[str]): + app = App(repeat_command_groups_printing=True) + router = Router(disable_redirect_stdout=True) + + @router.command('command') + def handler(res: Response): + print("Hello!") + + app.include_router(router) + + app._pre_cycle_setup() + app._process_exist_and_valid_command(InputCommand('command')) + + stdout = capsys.readouterr() + + assert 'Hello!' in stdout.out + From a2ef2652eddfdb451bd40fe32def13f0833c2c32 Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 6 Dec 2025 09:26:53 +0300 Subject: [PATCH 4/8] refactor tests and add new --- tests/unit_tests/test_argparser.py | 482 ++++++++++++------------- tests/unit_tests/test_autocompleter.py | 359 +++++++++--------- tests/unit_tests/test_command.py | 92 +++-- tests/unit_tests/test_flag.py | 153 ++++---- tests/unit_tests/test_response.py | 193 +++++----- 5 files changed, 628 insertions(+), 651 deletions(-) diff --git a/tests/unit_tests/test_argparser.py b/tests/unit_tests/test_argparser.py index f02beb2..8157991 100644 --- a/tests/unit_tests/test_argparser.py +++ b/tests/unit_tests/test_argparser.py @@ -1,249 +1,247 @@ -from argparse import Namespace import sys +from argparse import Namespace +from typing import TYPE_CHECKING from unittest.mock import call import pytest - -from argenta.orchestrator.argparser.arguments.models import (BaseArgument, - BooleanArgument, - InputArgument, - ValueArgument) +from pytest_mock import MockerFixture + +from argenta.orchestrator.argparser.arguments.models import ( + BaseArgument, + BooleanArgument, + InputArgument, + ValueArgument, +) from argenta.orchestrator.argparser.entity import ArgParser, ArgSpace +if TYPE_CHECKING: + from pytest_mock.plugin import MockType + + +def test_value_argument_creation() -> None: + arg: ValueArgument = ValueArgument( + name="test_arg", + prefix="--", + help="A test argument.", + possible_values=["one", "two"], + default="one", + is_required=True, + is_deprecated=False, + ) + assert arg.name == "test_arg" + assert arg.prefix == "--" + assert arg.help == "A test argument." + assert arg.possible_values == ["one", "two"] + assert arg.default == "one" + assert arg.is_required is True + assert arg.is_deprecated is False + assert arg.action == "store" + assert arg.string_entity == "--test_arg" + + +def test_boolean_argument_creation() -> None: + arg: BooleanArgument = BooleanArgument( + name="verbose", prefix="-", help="Enable verbose mode.", is_deprecated=True + ) + assert arg.name == "verbose" + assert arg.prefix == "-" + assert arg.help == "Enable verbose mode." + assert arg.is_deprecated is True + assert arg.action == "store_true" + assert arg.string_entity == "-verbose" + + +def test_input_argument_creation() -> None: + arg: InputArgument = InputArgument( + name="file", value="/path/to/file", founder_class=ValueArgument + ) + assert arg.name == "file" + assert arg.value == "/path/to/file" + assert arg.founder_class is ValueArgument + + +@pytest.fixture +def mock_arguments() -> list[InputArgument]: + return [ + InputArgument(name="arg1", value="val1", founder_class=ValueArgument), + InputArgument(name="arg2", value=True, founder_class=BooleanArgument), + InputArgument(name="arg3", value="val3", founder_class=ValueArgument), + ] + + +@pytest.fixture +def arg_space(mock_arguments: list[InputArgument]) -> ArgSpace: + return ArgSpace(all_arguments=mock_arguments) + + +def test_argspace_initialization(arg_space: ArgSpace, mock_arguments: list[InputArgument]) -> None: + assert len(arg_space.all_arguments) == 3 + assert arg_space.all_arguments == mock_arguments + -class TestArgumentCreation: - """Tests for the creation and attribute validation of argument model classes.""" - - def test_value_argument_creation(self): - """Ensures ValueArgument instances are created with correct attributes.""" - arg = ValueArgument( - name="test_arg", - prefix="--", - help="A test argument.", - possible_values=["one", "two"], - default="one", - is_required=True, - is_deprecated=False, - ) - assert arg.name == "test_arg" - assert arg.prefix == "--" - assert arg.help == "A test argument." - assert arg.possible_values == ["one", "two"] - assert arg.default == "one" - assert arg.is_required is True - assert arg.is_deprecated is False - assert arg.action == "store" - assert arg.string_entity == "--test_arg" - - def test_boolean_argument_creation(self): - """Ensures BooleanArgument instances are created with correct attributes.""" - arg = BooleanArgument( - name="verbose", prefix="-", help="Enable verbose mode.", is_deprecated=True - ) - assert arg.name == "verbose" - assert arg.prefix == "-" - assert arg.help == "Enable verbose mode." - assert arg.is_deprecated is True - assert arg.action == "store_true" - assert arg.string_entity == "-verbose" - - def test_input_argument_creation(self): - """Ensures InputArgument instances are created with correct attributes.""" - arg = InputArgument( - name="file", value="/path/to/file", founder_class=ValueArgument - ) - assert arg.name == "file" - assert arg.value == "/path/to/file" - assert arg.founder_class is ValueArgument - - -class TestArgSpace: - """Tests for the ArgSpace class, which holds parsed argument values.""" - - @pytest.fixture - def mock_arguments(self) -> list[InputArgument]: - """Provides a list of mock InputArgument objects for testing.""" - return [ - InputArgument(name="arg1", value="val1", founder_class=ValueArgument), - InputArgument(name="arg2", value=True, founder_class=BooleanArgument), - InputArgument(name="arg3", value="val3", founder_class=ValueArgument), - ] - - @pytest.fixture - def arg_space(self, mock_arguments: list[InputArgument]) -> ArgSpace: - """Provides a pre-populated ArgSpace instance.""" - return ArgSpace(all_arguments=mock_arguments) - - def test_initialization(self, arg_space: ArgSpace, mock_arguments: list[InputArgument]): - """Tests if ArgSpace is initialized correctly with a list of arguments.""" - assert len(arg_space.all_arguments) == 3 - assert arg_space.all_arguments == mock_arguments - - def test_get_by_name(self, arg_space: ArgSpace, mock_arguments: list[InputArgument]): - """Tests retrieving an argument by its name.""" - found_arg = arg_space.get_by_name("arg1") - assert found_arg is not None - assert found_arg == mock_arguments[0] - - def test_get_by_name_not_found(self, arg_space: ArgSpace): - """Tests that get_by_name returns None for a non-existent argument.""" - found_arg = arg_space.get_by_name("non_existent_arg") - assert found_arg is None - - def test_get_by_type(self, arg_space: ArgSpace, mock_arguments: list[InputArgument]): - """Tests retrieving arguments based on their founder class type.""" - value_args = arg_space.get_by_type(ValueArgument) - assert len(value_args) == 2 - assert mock_arguments[0] in value_args - assert mock_arguments[2] in value_args - - bool_args = arg_space.get_by_type(BooleanArgument) - assert len(bool_args) == 1 - assert mock_arguments[1] in bool_args - - def test_get_by_type_not_found(self, arg_space: ArgSpace): - """Tests that get_by_type returns an empty list for an unused argument type.""" - class OtherArgument(BaseArgument): - pass - other_args = arg_space.get_by_type(OtherArgument) - assert other_args == [] - - def test_from_namespace(self): - """Tests the class method for creating an ArgSpace from an argparse.Namespace.""" - namespace = Namespace(config="config.json", debug=True, verbose=False) - processed_args = [ - ValueArgument(name="config", prefix="--"), - BooleanArgument(name="debug", prefix="-"), - BooleanArgument(name="verbose", prefix="-"), - ] - - arg_space = ArgSpace.from_namespace(namespace, processed_args) - assert len(arg_space.all_arguments) == 3 - - config_arg = arg_space.get_by_name('config') - debug_arg = arg_space.get_by_name('debug') - - assert config_arg is not None - assert config_arg.value == "config.json" - assert config_arg.founder_class is ValueArgument - - assert debug_arg is not None - assert debug_arg.value is True - assert debug_arg.founder_class is BooleanArgument - - -class TestArgParser: - """Tests for the ArgParser class, which orchestrates argument parsing.""" - - @pytest.fixture - def value_arg(self) -> ValueArgument: - """Provides a sample ValueArgument.""" - return ValueArgument(name="config", help="Path to config file", default="dev.json", is_required=False, possible_values=["dev.json", "prod.json"]) - - @pytest.fixture - def bool_arg(self) -> BooleanArgument: - """Provides a sample BooleanArgument.""" - return BooleanArgument(name="debug", help="Enable debug mode") - - @pytest.fixture - def processed_args(self, value_arg: ValueArgument, bool_arg: BooleanArgument) -> list: - """Provides a list of processed arguments.""" - return [value_arg, bool_arg] - - def test_initialization(self, processed_args: list): - """Tests that the ArgParser constructor correctly assigns attributes.""" - parser = ArgParser( - processed_args=processed_args, - name="TestApp", - description="A test application.", - epilog="Test epilog.", - ) - assert parser.name == "TestApp" - assert parser.description == "A test application." - assert parser.epilog == "Test epilog." - assert parser.processed_args == processed_args - assert isinstance(parser.parsed_argspace, ArgSpace) - assert parser.parsed_argspace.all_arguments == [] - - @pytest.mark.skipif(sys.version_info < (3, 13), reason="requires python3.13 or higher") - def test_register_args(self, mocker, value_arg: ValueArgument, bool_arg: BooleanArgument): - """Tests that arguments are correctly registered with the underlying ArgumentParser.""" - mock_add_argument = mocker.patch("argparse.ArgumentParser.add_argument") - - parser = ArgParser(processed_args=[value_arg, bool_arg]) - - expected_calls = [ - # Call for the ValueArgument - call( - value_arg.string_entity, - action=value_arg.action, - help=value_arg.help, - default=value_arg.default, - choices=value_arg.possible_values, - required=value_arg.is_required, - deprecated=value_arg.is_deprecated - ), - # Call for the BooleanArgument - call( - bool_arg.string_entity, - action=bool_arg.action, - help=bool_arg.help, - deprecated=bool_arg.is_deprecated - ) - ] - mock_add_argument.assert_has_calls(expected_calls, any_order=True) - - @pytest.mark.skipif(sys.version_info > (3, 12), reason='for more latest python version has been other test') - def test_register_args(self, mocker, value_arg: ValueArgument, bool_arg: BooleanArgument): - """Tests that arguments are correctly registered with the underlying ArgumentParser.""" - mock_add_argument = mocker.patch("argparse.ArgumentParser.add_argument") - - parser = ArgParser(processed_args=[value_arg, bool_arg]) - - expected_calls = [ - # Call for the ValueArgument - call( - value_arg.string_entity, - action=value_arg.action, - help=value_arg.help, - default=value_arg.default, - choices=value_arg.possible_values, - required=value_arg.is_required - ), - # Call for the BooleanArgument - call( - bool_arg.string_entity, - action=bool_arg.action, - help=bool_arg.help - ) - ] - mock_add_argument.assert_has_calls(expected_calls, any_order=True) - - def test_parse_args_populates_argspace(self, mocker, processed_args: list[ValueArgument | BooleanArgument]): - """Tests that _parse_args correctly calls the parser and populates the ArgSpace.""" - # 1. Mock the return value of the internal argparse instance - mock_namespace = Namespace(config='config.json', debug=True) - mocker.patch( - 'argparse.ArgumentParser.parse_args', - return_value=mock_namespace - ) - - # 2. Initialize the parser and call the method under test - parser = ArgParser(processed_args=processed_args) - parser._parse_args() # Test the private method that contains the logic - - # 3. Assert the results - arg_space = parser.parsed_argspace - assert isinstance(arg_space, ArgSpace) - assert len(arg_space.all_arguments) == 2 - - config_arg = arg_space.get_by_name('config') - debug_arg = arg_space.get_by_name('debug') - - assert config_arg is not None - assert config_arg.value == 'config.json' - assert config_arg.founder_class is ValueArgument - - assert debug_arg is not None - assert debug_arg.value is True - assert debug_arg.founder_class is BooleanArgument +def test_argspace_get_by_name(arg_space: ArgSpace, mock_arguments: list[InputArgument]) -> None: + found_arg: InputArgument | None = arg_space.get_by_name("arg1") + assert found_arg is not None + assert found_arg == mock_arguments[0] + + +def test_argspace_get_by_name_not_found(arg_space: ArgSpace) -> None: + found_arg: InputArgument | None = arg_space.get_by_name("non_existent_arg") + assert found_arg is None + + +def test_argspace_get_by_type(arg_space: ArgSpace, mock_arguments: list[InputArgument]) -> None: + value_args: list[InputArgument] = arg_space.get_by_type(ValueArgument) + assert len(value_args) == 2 + assert mock_arguments[0] in value_args + assert mock_arguments[2] in value_args + + bool_args: list[InputArgument] = arg_space.get_by_type(BooleanArgument) + assert len(bool_args) == 1 + assert mock_arguments[1] in bool_args + + +def test_argspace_get_by_type_not_found(arg_space: ArgSpace) -> None: + class OtherArgument(BaseArgument): + pass + + other_args: list[InputArgument] = arg_space.get_by_type(OtherArgument) # pyright: ignore[reportAssignmentType] + assert other_args == [] + + +def test_argspace_from_namespace() -> None: + namespace: Namespace = Namespace(config="config.json", debug=True, verbose=False) + processed_args: list[ValueArgument | BooleanArgument] = [ + ValueArgument(name="config", prefix="--"), + BooleanArgument(name="debug", prefix="-"), + BooleanArgument(name="verbose", prefix="-"), + ] + + arg_space: ArgSpace = ArgSpace.from_namespace(namespace, processed_args) + assert len(arg_space.all_arguments) == 3 + + config_arg: InputArgument | None = arg_space.get_by_name('config') + debug_arg: InputArgument | None = arg_space.get_by_name('debug') + + assert config_arg is not None + assert config_arg.value == "config.json" + assert config_arg.founder_class is ValueArgument + + assert debug_arg is not None + assert debug_arg.value is True + assert debug_arg.founder_class is BooleanArgument + + +@pytest.fixture +def value_arg() -> ValueArgument: + return ValueArgument( + name="config", + help="Path to config file", + default="dev.json", + is_required=False, + possible_values=["dev.json", "prod.json"], + ) + + +@pytest.fixture +def bool_arg() -> BooleanArgument: + return BooleanArgument(name="debug", help="Enable debug mode") + + +@pytest.fixture +def processed_args(value_arg: ValueArgument, bool_arg: BooleanArgument) -> list[ValueArgument | BooleanArgument]: + return [value_arg, bool_arg] + + +def test_argparser_initialization(processed_args: list[ValueArgument | BooleanArgument]) -> None: + parser: ArgParser = ArgParser( + processed_args=processed_args, + name="TestApp", + description="A test application.", + epilog="Test epilog.", + ) + assert parser.name == "TestApp" + assert parser.description == "A test application." + assert parser.epilog == "Test epilog." + assert parser.processed_args == processed_args + assert isinstance(parser.parsed_argspace, ArgSpace) + assert parser.parsed_argspace.all_arguments == [] + + +@pytest.mark.skipif(sys.version_info < (3, 13), reason="requires python3.13 or higher") +def test_argparser_register_args_py313( + mocker: MockerFixture, value_arg: ValueArgument, bool_arg: BooleanArgument +) -> None: + mock_add_argument: MockType = mocker.patch("argparse.ArgumentParser.add_argument") + + parser: ArgParser = ArgParser(processed_args=[value_arg, bool_arg]) # pyright: ignore[reportUnusedVariable] + + expected_calls: list[call] = [ + call( + value_arg.string_entity, + action=value_arg.action, + help=value_arg.help, + default=value_arg.default, + choices=value_arg.possible_values, + required=value_arg.is_required, + deprecated=value_arg.is_deprecated, + ), + call( + bool_arg.string_entity, + action=bool_arg.action, + help=bool_arg.help, + deprecated=bool_arg.is_deprecated, + ), + ] + mock_add_argument.assert_has_calls(expected_calls, any_order=True) + + +@pytest.mark.skipif(sys.version_info > (3, 12), reason="for more latest python version has been other test") +def test_argparser_register_args_py312( + mocker: MockerFixture, value_arg: ValueArgument, bool_arg: BooleanArgument +) -> None: + mock_add_argument: MockType = mocker.patch("argparse.ArgumentParser.add_argument") + + parser: ArgParser = ArgParser(processed_args=[value_arg, bool_arg]) + + expected_calls: list[call] = [ + call( + value_arg.string_entity, + action=value_arg.action, + help=value_arg.help, + default=value_arg.default, + choices=value_arg.possible_values, + required=value_arg.is_required, + ), + call( + bool_arg.string_entity, + action=bool_arg.action, + help=bool_arg.help, + ), + ] + mock_add_argument.assert_has_calls(expected_calls, any_order=True) + + +def test_argparser_parse_args_populates_argspace( + mocker: MockerFixture, processed_args: list[ValueArgument | BooleanArgument] +) -> None: + mock_namespace: Namespace = Namespace(config='config.json', debug=True) + mocker.patch('argparse.ArgumentParser.parse_args', return_value=mock_namespace) + + parser: ArgParser = ArgParser(processed_args=processed_args) + parser._parse_args() + + arg_space: ArgSpace = parser.parsed_argspace + assert isinstance(arg_space, ArgSpace) + assert len(arg_space.all_arguments) == 2 + + config_arg: InputArgument | None = arg_space.get_by_name('config') + debug_arg: InputArgument | None = arg_space.get_by_name('debug') + + assert config_arg is not None + assert config_arg.value == 'config.json' + assert config_arg.founder_class is ValueArgument + + assert debug_arg is not None + assert debug_arg.value is True + assert debug_arg.founder_class is BooleanArgument diff --git a/tests/unit_tests/test_autocompleter.py b/tests/unit_tests/test_autocompleter.py index aab1a66..37ecb70 100644 --- a/tests/unit_tests/test_autocompleter.py +++ b/tests/unit_tests/test_autocompleter.py @@ -1,198 +1,173 @@ import os -from unittest.mock import MagicMock, call, patch import pytest +from pyfakefs.fake_filesystem import FakeFilesystem +from pytest_mock import MockerFixture +from pytest_mock.plugin import MockType + +from argenta.app.autocompleter.entity import ( + AutoCompleter, + _get_history_items, + _is_command_exist, +) -# Since readline is not available on all platforms (e.g., Windows) for testing, -# it is mocked for all tests. -readline_mock = MagicMock() -# We patch the module where it's imported, not where it's defined. @pytest.fixture -def mock_readline(): - """Fixture to provide a mock of the `readline` module.""" - with patch('argenta.app.autocompleter.entity.readline', readline_mock) as mock: - # This nested state simulates readline's internal history list. - _history = [] - - def add_history(item: str) -> None: - _history.append(item) - - def get_history_item(index: int) -> str | None: - # readline history is 1-based. - if 1 <= index <= len(_history): - return _history[index - 1] - return None - - def get_current_history_length() -> int: - return len(_history) - - def clear_history() -> None: - _history.clear() - - # Reset all mocks and the internal history before each test. - mock.reset_mock() - clear_history() - - # Apply side effects to mock functions to simulate real behavior. - mock.add_history.side_effect = add_history - mock.get_history_item.side_effect = get_history_item - mock.get_current_history_length.side_effect = get_current_history_length - - # Provide a default return value for functions that are read from. - mock.get_completer_delims.return_value = " " - - yield mock - -# We import the class under test after setting up the patch context if needed, -# or ensure patches target the correct import location. -from argenta.app.autocompleter.entity import (AutoCompleter, - _get_history_items, - _is_command_exist) - - -class TestAutoCompleter: - """Test suite for the AutoCompleter class.""" - HISTORY_FILE = "test_history.txt" - COMMANDS = ["start", "stop", "status"] - - def test_initialization(self): - """Tests that the constructor correctly assigns attributes.""" - completer = AutoCompleter(history_filename=self.HISTORY_FILE, autocomplete_button="tab") - assert completer.history_filename == self.HISTORY_FILE - assert completer.autocomplete_button == "tab" - - def test_initial_setup_if_history_file_does_not_exist(self, fs, mock_readline): - """Tests initial setup creates history from commands when the history file is absent.""" - # Ensure the file does not exist in the fake filesystem. - if os.path.exists(self.HISTORY_FILE): - os.remove(self.HISTORY_FILE) - - completer = AutoCompleter(history_filename=self.HISTORY_FILE) - completer.initial_setup(self.COMMANDS) - - mock_readline.read_history_file.assert_not_called() - expected_calls = [call(cmd) for cmd in self.COMMANDS] - mock_readline.add_history.assert_has_calls(expected_calls, any_order=True) - assert mock_readline.add_history.call_count == len(self.COMMANDS) - - mock_readline.set_completer.assert_called_with(completer._complete) - mock_readline.parse_and_bind.assert_called_with("tab: complete") - - def test_initial_setup_if_history_file_exists(self, fs, mock_readline): - """Tests initial setup reads from an existing history file.""" - fs.create_file(self.HISTORY_FILE, contents="previous_command\n") - - completer = AutoCompleter(history_filename=self.HISTORY_FILE) - completer.initial_setup(self.COMMANDS) - - mock_readline.read_history_file.assert_called_once_with(self.HISTORY_FILE) - mock_readline.add_history.assert_not_called() - mock_readline.set_completer.assert_called_once() - mock_readline.parse_and_bind.assert_called_once() - - def test_initial_setup_with_no_history_filename(self, mock_readline): - """Tests initial setup when no history filename is provided.""" - completer = AutoCompleter(history_filename=None) - completer.initial_setup(self.COMMANDS) - - mock_readline.read_history_file.assert_not_called() - expected_calls = [call(cmd) for cmd in self.COMMANDS] - mock_readline.add_history.assert_has_calls(expected_calls, any_order=True) - - def test_exit_setup_writes_and_filters_history(self, fs, mock_readline): - """Tests that exit_setup writes a filtered and unique history to the file.""" - # 1. Populate the mock readline history. - mock_readline.add_history.side_effect(None) # Temporarily disable side effect to just record calls - mock_readline.add_history("start server") - mock_readline.add_history("stop client") - mock_readline.add_history("invalid command") - mock_readline.add_history("start server") # Add a duplicate. - - # 2. Simulate the state of the history file after readline.write_history_file would have run. - raw_history_content = "\n".join(["start server", "stop client", "invalid command", "start server"]) - fs.create_file(self.HISTORY_FILE, contents=raw_history_content) - - # 3. Call the method under test. - completer = AutoCompleter(history_filename=self.HISTORY_FILE) - completer.exit_setup(all_commands=["start", "stop"], ignore_command_register=False) - - # 4. Assert that readline's write function was called. - mock_readline.write_history_file.assert_called_once_with(self.HISTORY_FILE) - - # 5. Assert the file was correctly re-written with filtered and unique content. - with open(self.HISTORY_FILE, "r") as f: - content = f.read() - lines = sorted(content.strip().split("\n")) - assert lines == ["start server", "stop client"] - - def test_exit_setup_with_no_history_filename(self, mock_readline): - """Tests that exit_setup does nothing if no filename is provided.""" - completer = AutoCompleter(history_filename=None) - completer.exit_setup(all_commands=self.COMMANDS, ignore_command_register=False) - mock_readline.write_history_file.assert_not_called() - - def test_complete_with_no_matches(self, mock_readline): - """Tests the _complete method when there are no matching history items.""" - for cmd in ["start", "stop"]: - mock_readline.add_history(cmd) - - completer = AutoCompleter() - assert completer._complete("run", 0) is None - assert completer._complete("run", 1) is None - - def test_complete_with_one_match(self, mock_readline): - """Tests the _complete method when there is exactly one match.""" - mock_readline.add_history("start server") - mock_readline.add_history("stop server") - - completer = AutoCompleter() - assert completer._complete("start", 0) == "start server" - assert completer._complete("start", 1) is None # Subsequent states yield no matches - - def test_complete_with_multiple_matches(self, mock_readline): - """Tests _complete with multiple matches that share a common prefix.""" - mock_readline.add_history("status client") - mock_readline.add_history("status server") - mock_readline.add_history("stop") - - completer = AutoCompleter() - - # On state 0, it should insert the common prefix via readline and return None. - result = completer._complete("stat", 0) - assert result is None - mock_readline.insert_text.assert_called_once_with("us ") # Completes "stat" to "status " - mock_readline.redisplay.assert_called_once() - - # On subsequent states, it should do nothing. - mock_readline.reset_mock() - result_state_1 = completer._complete("stat", 1) - assert result_state_1 is None - mock_readline.insert_text.assert_not_called() - - -class TestHelperFunctions: - """Test suite for helper functions in the autocompleter module.""" - - def test_is_command_exist(self): - """Tests the _is_command_exist helper function.""" - existing = ["start", "stop", "status"] - - # Case-sensitive check - assert _is_command_exist("start", existing, ignore_command_register=False) is True - assert _is_command_exist("START", existing, ignore_command_register=False) is False - assert _is_command_exist("unknown", existing, ignore_command_register=False) is False - - # Case-insensitive check - assert _is_command_exist("start", existing, ignore_command_register=True) is True - assert _is_command_exist("START", existing, ignore_command_register=True) is True - assert _is_command_exist("unknown", existing, ignore_command_register=True) is False - - def test_get_history_items(self, mock_readline): - """Tests the _get_history_items helper function.""" - assert _get_history_items() == [] - - mock_readline.add_history("first item") - mock_readline.add_history("second item") - - assert _get_history_items() == ["first item", "second item"] +def mock_readline(mocker: MockerFixture) -> MockType: + _history: list[str] = [] + + def add_history(item: str) -> None: + _history.append(item) + + def get_history_item(index: int) -> str | None: + if 1 <= index <= len(_history): + return _history[index - 1] + return None + + def get_current_history_length() -> int: + return len(_history) + + def clear_history() -> None: + _history.clear() + + mock: MockType = mocker.MagicMock() + mocker.patch('argenta.app.autocompleter.entity.readline', mock) + + mock.reset_mock() + clear_history() + + mock.add_history.side_effect = add_history + mock.get_history_item.side_effect = get_history_item + mock.get_current_history_length.side_effect = get_current_history_length + mock.get_completer_delims.return_value = " " + + return mock + + +HISTORY_FILE: str = "test_history.txt" +COMMANDS: list[str] = ["start", "stop", "status"] + + +def test_initialization() -> None: + completer: AutoCompleter = AutoCompleter(history_filename=HISTORY_FILE, autocomplete_button="tab") + assert completer.history_filename == HISTORY_FILE + assert completer.autocomplete_button == "tab" + + +def test_initial_setup_if_history_file_does_not_exist(fs: FakeFilesystem, mock_readline: MockType) -> None: + if os.path.exists(HISTORY_FILE): + os.remove(HISTORY_FILE) + + completer: AutoCompleter = AutoCompleter(history_filename=HISTORY_FILE) + completer.initial_setup(COMMANDS) + + mock_readline.read_history_file.assert_not_called() + assert mock_readline.add_history.call_count == len(COMMANDS) + + mock_readline.set_completer.assert_called_with(completer._complete) + mock_readline.parse_and_bind.assert_called_with("tab: complete") + + +def test_initial_setup_if_history_file_exists(fs: FakeFilesystem, mock_readline: MockType) -> None: + fs.create_file(HISTORY_FILE, contents="previous_command\n") + + completer: AutoCompleter = AutoCompleter(history_filename=HISTORY_FILE) + completer.initial_setup(COMMANDS) + + mock_readline.read_history_file.assert_called_once_with(HISTORY_FILE) + mock_readline.add_history.assert_not_called() + mock_readline.set_completer.assert_called_once() + mock_readline.parse_and_bind.assert_called_once() + + +def test_initial_setup_with_no_history_filename(mock_readline: MockType) -> None: + completer: AutoCompleter = AutoCompleter(history_filename=None) + completer.initial_setup(COMMANDS) + + mock_readline.read_history_file.assert_not_called() + assert mock_readline.add_history.call_count == len(COMMANDS) + + +def test_exit_setup_writes_and_filters_history(fs: FakeFilesystem, mock_readline: MockType) -> None: + mock_readline.add_history.side_effect = None + mock_readline.add_history("start server") + mock_readline.add_history("stop client") + mock_readline.add_history("invalid command") + mock_readline.add_history("start server") + + raw_history_content: str = "\n".join(["start server", "stop client", "invalid command", "start server"]) + fs.create_file(HISTORY_FILE, contents=raw_history_content) + + completer: AutoCompleter = AutoCompleter(history_filename=HISTORY_FILE) + completer.exit_setup(all_commands=["start", "stop"], ignore_command_register=False) + + mock_readline.write_history_file.assert_called_once_with(HISTORY_FILE) + + with open(HISTORY_FILE) as f: + content: str = f.read() + lines: list[str] = sorted(content.strip().split("\n")) + assert lines == ["start server", "stop client"] + + +def test_exit_setup_with_no_history_filename(mock_readline: MockType) -> None: + completer: AutoCompleter = AutoCompleter(history_filename=None) + completer.exit_setup(all_commands=COMMANDS, ignore_command_register=False) + mock_readline.write_history_file.assert_not_called() + + +def test_complete_with_no_matches(mock_readline: MockType) -> None: + cmd: str + for cmd in ["start", "stop"]: + mock_readline.add_history(cmd) + + completer: AutoCompleter = AutoCompleter() + assert completer._complete("run", 0) is None + assert completer._complete("run", 1) is None + + +def test_complete_with_one_match(mock_readline: MockType) -> None: + mock_readline.add_history("start server") + mock_readline.add_history("stop server") + + completer: AutoCompleter = AutoCompleter() + assert completer._complete("start", 0) == "start server" + assert completer._complete("start", 1) is None + + +def test_complete_with_multiple_matches(mock_readline: MockType) -> None: + mock_readline.add_history("status client") + mock_readline.add_history("status server") + mock_readline.add_history("stop") + + completer: AutoCompleter = AutoCompleter() + + result: str | None = completer._complete("stat", 0) + assert result is None + mock_readline.insert_text.assert_called_once_with("us ") + mock_readline.redisplay.assert_called_once() + + mock_readline.reset_mock() + result_state_1: str | None = completer._complete("stat", 1) + assert result_state_1 is None + mock_readline.insert_text.assert_not_called() + + +def test_is_command_exist() -> None: + existing: list[str] = ["start", "stop", "status"] + + assert _is_command_exist("start", existing, ignore_command_register=False) is True + assert _is_command_exist("START", existing, ignore_command_register=False) is False + assert _is_command_exist("unknown", existing, ignore_command_register=False) is False + + assert _is_command_exist("start", existing, ignore_command_register=True) is True + assert _is_command_exist("START", existing, ignore_command_register=True) is True + assert _is_command_exist("unknown", existing, ignore_command_register=True) is False + + +def test_get_history_items(mock_readline: MockType) -> None: + assert _get_history_items() == [] + + mock_readline.add_history("first item") + mock_readline.add_history("second item") + + assert _get_history_items() == ["first item", "second item"] diff --git a/tests/unit_tests/test_command.py b/tests/unit_tests/test_command.py index fbd5bab..5873e30 100644 --- a/tests/unit_tests/test_command.py +++ b/tests/unit_tests/test_command.py @@ -1,5 +1,6 @@ import re -import unittest + +import pytest from argenta.command.exceptions import (EmptyInputCommandException, RepeatedInputFlagsException, @@ -10,54 +11,65 @@ from argenta.command.models import Command, InputCommand -class TestInputCommand(unittest.TestCase): - def test_parse_correct_raw_command(self): - self.assertEqual(InputCommand.parse('ssh --host 192.168.0.3').trigger, 'ssh') +def test_parse_correct_raw_command(): + assert InputCommand.parse('ssh --host 192.168.0.3').trigger == 'ssh' + + +def test_parse_raw_command_without_flag_name_with_value(): + with pytest.raises(UnprocessedInputFlagException): + InputCommand.parse('ssh 192.168.0.3') + + +def test_parse_raw_command_with_repeated_flag_name(): + with pytest.raises(RepeatedInputFlagsException): + InputCommand.parse('ssh --host 192.168.0.3 --host 172.198.0.43') + + +def test_parse_empty_raw_command(): + with pytest.raises(EmptyInputCommandException): + InputCommand.parse('') + + +def test_validate_invalid_input_flag1(): + command = Command('some', flags=Flag('test')) + assert command.validate_input_flag(InputFlag('test', input_value='', status=None)) == ValidationStatus.INVALID + + +def test_validate_valid_input_flag2(): + command = Command('some', flags=Flags([Flag('test'), Flag('more')])) + assert command.validate_input_flag(InputFlag('more', input_value='random-value', status=None)) == ValidationStatus.VALID + + +def test_validate_undefined_input_flag1(): + command = Command('some', flags=Flag('test')) + assert command.validate_input_flag(InputFlag('more', input_value='', status=None)) == ValidationStatus.UNDEFINED - def test_parse_raw_command_without_flag_name_with_value(self): - with self.assertRaises(UnprocessedInputFlagException): - InputCommand.parse('ssh 192.168.0.3') - def test_parse_raw_command_with_repeated_flag_name(self): - with self.assertRaises(RepeatedInputFlagsException): - InputCommand.parse('ssh --host 192.168.0.3 --host 172.198.0.43') +def test_validate_undefined_input_flag2(): + command = Command('some', flags=Flags([Flag('test'), Flag('more')])) + assert command.validate_input_flag(InputFlag('case', input_value='', status=None)) == ValidationStatus.UNDEFINED - def test_parse_empty_raw_command(self): - with self.assertRaises(EmptyInputCommandException): - InputCommand.parse('') - def test_validate_invalid_input_flag1(self): - command = Command('some', flags=Flag('test')) - self.assertEqual(command.validate_input_flag(InputFlag('test', input_value='', status=None)), ValidationStatus.INVALID) +def test_validate_undefined_input_flag3(): + command = Command('some') + assert command.validate_input_flag(InputFlag('case', input_value='', status=None)) == ValidationStatus.UNDEFINED - def test_validate_valid_input_flag2(self): - command = Command('some', flags=Flags([Flag('test'), Flag('more')])) - self.assertEqual(command.validate_input_flag(InputFlag('more', input_value='random-value', status=None)), ValidationStatus.VALID) - def test_validate_undefined_input_flag1(self): - command = Command('some', flags=Flag('test')) - self.assertEqual(command.validate_input_flag(InputFlag('more', input_value='', status=None)), ValidationStatus.UNDEFINED) +def test_invalid_input_flag1(): + command = Command('some', flags=Flag('test', possible_values=PossibleValues.NEITHER)) + assert command.validate_input_flag(InputFlag('test', input_value='example', status=None)) == ValidationStatus.INVALID - def test_validate_undefined_input_flag2(self): - command = Command('some', flags=Flags([Flag('test'), Flag('more')])) - self.assertEqual(command.validate_input_flag(InputFlag('case', input_value='', status=None)), ValidationStatus.UNDEFINED) - def test_validate_undefined_input_flag3(self): - command = Command('some') - self.assertEqual(command.validate_input_flag(InputFlag('case', input_value='', status=None)), ValidationStatus.UNDEFINED) +def test_invalid_input_flag2(): + command = Command('some', flags=Flag('test', possible_values=['some', 'case'])) + assert command.validate_input_flag(InputFlag('test', input_value='slay', status=None)) == ValidationStatus.INVALID - def test_invalid_input_flag1(self): - command = Command('some', flags=Flag('test', possible_values=PossibleValues.NEITHER)) - self.assertEqual(command.validate_input_flag(InputFlag('test', input_value='example', status=None)), ValidationStatus.INVALID) - def test_invalid_input_flag2(self): - command = Command('some', flags=Flag('test', possible_values=['some', 'case'])) - self.assertEqual(command.validate_input_flag(InputFlag('test', input_value='slay', status=None)), ValidationStatus.INVALID) +def test_invalid_input_flag3(): + command = Command('some', flags=Flag('test', possible_values=re.compile(r'^ex\d{, 2}op$'))) + assert command.validate_input_flag(InputFlag('test', input_value='example', status=None)) == ValidationStatus.INVALID - def test_invalid_input_flag3(self): - command = Command('some', flags=Flag('test', possible_values=re.compile(r'^ex\d{, 2}op$'))) - self.assertEqual(command.validate_input_flag(InputFlag('test', input_value='example', status=None)), ValidationStatus.INVALID) - def test_isinstance_parse_correct_raw_command(self): - cmd = InputCommand.parse('ssh --host 192.168.0.3') - self.assertIsInstance(cmd, InputCommand) +def test_isinstance_parse_correct_raw_command(): + cmd = InputCommand.parse('ssh --host 192.168.0.3') + assert isinstance(cmd, InputCommand) diff --git a/tests/unit_tests/test_flag.py b/tests/unit_tests/test_flag.py index d85a446..854a9f4 100644 --- a/tests/unit_tests/test_flag.py +++ b/tests/unit_tests/test_flag.py @@ -1,129 +1,116 @@ import re -import unittest from argenta.command.flag import Flag, InputFlag, PossibleValues from argenta.command.flag.flags import Flags, InputFlags -class TestFlag(unittest.TestCase): - def test_get_string_entity(self): - self.assertEqual(Flag(name='test').string_entity, - '--test') +def test_get_string_entity(): + assert Flag(name='test').string_entity == '--test' - def test_get_string_entity2(self): - self.assertEqual(Flag(name='test', - prefix='---').string_entity, - '---test') - def test_get_flag_name(self): - self.assertEqual(Flag(name='test').name, - 'test') +def test_get_string_entity2(): + assert Flag(name='test', prefix='---').string_entity == '---test' - def test_get_flag_prefix(self): - self.assertEqual(Flag(name='test').prefix, - '--') - def test_get_flag_prefix2(self): - self.assertEqual(Flag(name='test', - prefix='--').prefix, - '--') +def test_get_flag_name(): + assert Flag(name='test').name == 'test' - def test_get_flag_value_without_set(self): - self.assertEqual(InputFlag(name='test', input_value='', status=None).input_value, - '') - def test_get_flag_value_with_set(self): - flag = InputFlag(name='test', input_value='example', status=None) - self.assertEqual(flag.input_value, 'example') +def test_get_flag_prefix(): + assert Flag(name='test').prefix == '--' - def test_validate_incorrect_flag_value_with_list_of_possible_flag_values(self): - flag = Flag(name='test', possible_values=['1', '2', '3']) - self.assertEqual(flag.validate_input_flag_value('bad value'), False) - def test_validate_correct_flag_value_with_list_of_possible_flag_values(self): - flag = Flag(name='test', possible_values=['1', '2', '3']) - self.assertEqual(flag.validate_input_flag_value('1'), True) +def test_get_flag_prefix2(): + assert Flag(name='test', prefix='--').prefix == '--' - def test_validate_incorrect_flag_value_with_pattern_of_possible_flag_values(self): - flag = Flag(name='test', possible_values=re.compile(r'192.168.\d+.\d+')) - self.assertEqual(flag.validate_input_flag_value('152.123.9.8'), False) - def test_validate_correct_flag_value_with_pattern_of_possible_flag_values(self): - flag = Flag(name='test', possible_values=re.compile(r'192.168.\d+.\d+')) - self.assertEqual(flag.validate_input_flag_value('192.168.9.8'), True) +def test_get_flag_value_without_set(): + assert InputFlag(name='test', input_value='', status=None).input_value == '' - def test_validate_correct_empty_flag_value_without_possible_flag_values(self): - flag = Flag(name='test', possible_values=PossibleValues.NEITHER) - self.assertEqual(flag.validate_input_flag_value(''), True) - def test_validate_correct_empty_flag_value_with_possible_flag_values(self): - flag = Flag(name='test', possible_values=PossibleValues.NEITHER) - self.assertEqual(flag.validate_input_flag_value(''), True) +def test_get_flag_value_with_set(): + flag = InputFlag(name='test', input_value='example', status=None) + assert flag.input_value == 'example' - def test_validate_incorrect_random_flag_value_without_possible_flag_values(self): - flag = Flag(name='test', possible_values=PossibleValues.NEITHER) - self.assertEqual(flag.validate_input_flag_value('random value'), False) - def test_validate_correct_random_flag_value_with_possible_flag_values(self): - flag = Flag(name='test', possible_values=PossibleValues.ALL) - self.assertEqual(flag.validate_input_flag_value('random value'), True) +def test_validate_incorrect_flag_value_with_list_of_possible_flag_values(): + flag = Flag(name='test', possible_values=['1', '2', '3']) + assert flag.validate_input_flag_value('bad value') is False - def test_get_input_flag1(self): - flag = InputFlag(name='test', input_value='', status=None) - input_flags = InputFlags([flag]) - self.assertEqual(input_flags.get_flag_by_name('test'), flag) - def test_get_input_flag2(self): - flag = InputFlag(name='test', input_value='', status=None) - flag2 = InputFlag(name='some', input_value='', status=None) - input_flags = InputFlags([flag, flag2]) - self.assertEqual(input_flags.get_flag_by_name('some'), flag2) +def test_validate_correct_flag_value_with_list_of_possible_flag_values(): + flag = Flag(name='test', possible_values=['1', '2', '3']) + assert flag.validate_input_flag_value('1') is True - def test_get_undefined_input_flag(self): - flag = InputFlag(name='test', input_value='', status=None) - flag2 = InputFlag(name='some', input_value='', status=None) - input_flags = InputFlags([flag, flag2]) - self.assertEqual(input_flags.get_flag_by_name('case'), None) - def test_get_flags(self): - flags = Flags() - list_of_flags = [ - Flag('test1'), - Flag('test2'), - Flag('test3'), - ] - flags.add_flags(list_of_flags) - self.assertEqual(flags.flags, - list_of_flags) - - def test_add_flag(self): - flags = Flags() - flags.add_flag(Flag('test')) - self.assertEqual(len(flags.flags), 1) - - def test_add_flags(self): - flags = Flags() - flags.add_flags([Flag('test'), Flag('test2')]) - self.assertEqual(len(flags.flags), 2) +def test_validate_incorrect_flag_value_with_pattern_of_possible_flag_values(): + flag = Flag(name='test', possible_values=re.compile(r'192.168.\d+.\d+')) + assert flag.validate_input_flag_value('152.123.9.8') is False +def test_validate_correct_flag_value_with_pattern_of_possible_flag_values(): + flag = Flag(name='test', possible_values=re.compile(r'192.168.\d+.\d+')) + assert flag.validate_input_flag_value('192.168.9.8') is True +def test_validate_correct_empty_flag_value_without_possible_flag_values(): + flag = Flag(name='test', possible_values=PossibleValues.NEITHER) + assert flag.validate_input_flag_value('') is True +def test_validate_correct_empty_flag_value_with_possible_flag_values(): + flag = Flag(name='test', possible_values=PossibleValues.NEITHER) + assert flag.validate_input_flag_value('') is True +def test_validate_incorrect_random_flag_value_without_possible_flag_values(): + flag = Flag(name='test', possible_values=PossibleValues.NEITHER) + assert flag.validate_input_flag_value('random value') is False +def test_validate_correct_random_flag_value_with_possible_flag_values(): + flag = Flag(name='test', possible_values=PossibleValues.ALL) + assert flag.validate_input_flag_value('random value') is True +def test_get_input_flag1(): + flag = InputFlag(name='test', input_value='', status=None) + input_flags = InputFlags([flag]) + assert input_flags.get_flag_by_name('test') == flag +def test_get_input_flag2(): + flag = InputFlag(name='test', input_value='', status=None) + flag2 = InputFlag(name='some', input_value='', status=None) + input_flags = InputFlags([flag, flag2]) + assert input_flags.get_flag_by_name('some') == flag2 +def test_get_undefined_input_flag(): + flag = InputFlag(name='test', input_value='', status=None) + flag2 = InputFlag(name='some', input_value='', status=None) + input_flags = InputFlags([flag, flag2]) + assert input_flags.get_flag_by_name('case') is None +def test_get_flags(): + flags = Flags() + list_of_flags = [ + Flag('test1'), + Flag('test2'), + Flag('test3'), + ] + flags.add_flags(list_of_flags) + assert flags.flags == list_of_flags +def test_add_flag(): + flags = Flags() + flags.add_flag(Flag('test')) + assert len(flags.flags) == 1 +def test_add_flags(): + flags = Flags() + flags.add_flags([Flag('test'), Flag('test2')]) + assert len(flags.flags) == 2 diff --git a/tests/unit_tests/test_response.py b/tests/unit_tests/test_response.py index 6a1d020..2110b2d 100644 --- a/tests/unit_tests/test_response.py +++ b/tests/unit_tests/test_response.py @@ -1,6 +1,7 @@ -import unittest from datetime import date, datetime +import pytest + from argenta.data_bridge import DataBridge from argenta.command.flag.models import InputFlag from argenta.command.flag.flags.models import InputFlags @@ -8,96 +9,100 @@ from argenta.response.status import ResponseStatus -class TestDataBridge(unittest.TestCase): - def setUp(self): - """Create a new DataBridge instance for each test""" - self.data_bridge = DataBridge() - - def test_update_data_basic(self): - """Test basic data update functionality""" - test_data = {"key1": "value1", "key2": "value2"} - self.data_bridge.update(test_data) - self.assertEqual(self.data_bridge.get_all(), test_data) - - def test_update_data_with_datetime(self): - """Test updating data with datetime objects""" - test_datetime = datetime(2024, 1, 15, 10, 30, 45) - test_data = {"created_at": test_datetime, "name": "test"} - self.data_bridge.update(test_data) - - result = self.data_bridge.get_all() - self.assertEqual(result["created_at"], test_datetime) - self.assertEqual(result["name"], "test") - - def test_update_data_multiple_calls(self): - """Test multiple update calls merge data""" - first_data = {"key1": "value1"} - second_data = {"key2": "value2"} - self.data_bridge.update(first_data) - self.data_bridge.update(second_data) - self.assertEqual(len(self.data_bridge.get_all()), 2) - - def test_get_data_empty(self): - """Test get_all returns empty dict when no data""" - self.assertEqual(self.data_bridge.get_all(), {}) - - def test_clear_data(self): - """Test clear_all removes all data""" - self.data_bridge.update({"key": "value"}) - self.assertNotEqual(self.data_bridge.get_all(), {}) - self.data_bridge.clear_all() - self.assertEqual(self.data_bridge.get_all(), {}) - - def test_delete_from_data(self): - """Test delete_by_key removes specific key""" - test_data = {"key1": "value1", "key2": "value2"} - self.data_bridge.update(test_data) - self.data_bridge.delete_by_key("key1") - result = self.data_bridge.get_all() - self.assertNotIn("key1", result) - self.assertIn("key2", result) - - def test_delete_from_data_nonexistent_key(self): - """Test delete_by_key with nonexistent key raises KeyError""" - with self.assertRaises(KeyError): - self.data_bridge.delete_by_key("nonexistent_key") - - def test_get_by_key(self): - """Test get_by_key retrieves correct value""" - test_data = {"key1": "value1", "key2": date(2024, 1, 1)} - self.data_bridge.update(test_data) - self.assertEqual(self.data_bridge.get_by_key("key1"), "value1") - self.assertEqual(self.data_bridge.get_by_key("key2"), date(2024, 1, 1)) - self.assertIsNone(self.data_bridge.get_by_key("nonexistent")) - - -class TestResponse(unittest.TestCase): - def test_response_initialization_basic(self): - """Test basic Response initialization""" - response = Response(ResponseStatus.ALL_FLAGS_VALID) - self.assertEqual(response.status, ResponseStatus.ALL_FLAGS_VALID) - self.assertEqual(response.input_flags, EMPTY_INPUT_FLAGS) - - def test_response_initialization_with_flags(self): - """Test Response initialization with input flags""" - input_flags = InputFlags([InputFlag('test', input_value='value', status=None)]) - response = Response(ResponseStatus.INVALID_VALUE_FLAGS, input_flags) - self.assertEqual(response.status, ResponseStatus.INVALID_VALUE_FLAGS) - self.assertEqual(response.input_flags, input_flags) - - def test_response_status_types(self): - """Test Response with different status types""" - statuses = [ - ResponseStatus.ALL_FLAGS_VALID, - ResponseStatus.UNDEFINED_FLAGS, - ResponseStatus.INVALID_VALUE_FLAGS, - ResponseStatus.UNDEFINED_AND_INVALID_FLAGS - ] - for status in statuses: - with self.subTest(status=status): - response = Response(status) - self.assertEqual(response.status, status) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file +@pytest.fixture +def data_bridge(): + """Create a new DataBridge instance for each test""" + return DataBridge() + + +def test_update_data_basic(data_bridge: DataBridge): + """Test basic data update functionality""" + test_data = {"key1": "value1", "key2": "value2"} + data_bridge.update(test_data) + assert data_bridge.get_all() == test_data + + +def test_update_data_with_datetime(data_bridge: DataBridge): + """Test updating data with datetime objects""" + test_datetime = datetime(2024, 1, 15, 10, 30, 45) + test_data = {"created_at": test_datetime, "name": "test"} + data_bridge.update(test_data) + + result = data_bridge.get_all() + assert result["created_at"] == test_datetime + assert result["name"] == "test" + + +def test_update_data_multiple_calls(data_bridge: DataBridge): + """Test multiple update calls merge data""" + first_data = {"key1": "value1"} + second_data = {"key2": "value2"} + data_bridge.update(first_data) + data_bridge.update(second_data) + assert len(data_bridge.get_all()) == 2 + + +def test_get_data_empty(data_bridge: DataBridge): + """Test get_all returns empty dict when no data""" + assert data_bridge.get_all() == {} + + +def test_clear_data(data_bridge: DataBridge): + """Test clear_all removes all data""" + data_bridge.update({"key": "value"}) + assert data_bridge.get_all() != {} + data_bridge.clear_all() + assert data_bridge.get_all() == {} + + +def test_delete_from_data(data_bridge: DataBridge): + """Test delete_by_key removes specific key""" + test_data = {"key1": "value1", "key2": "value2"} + data_bridge.update(test_data) + data_bridge.delete_by_key("key1") + result = data_bridge.get_all() + assert "key1" not in result + assert "key2" in result + + +def test_delete_from_data_nonexistent_key(data_bridge: DataBridge): + """Test delete_by_key with nonexistent key raises KeyError""" + with pytest.raises(KeyError): + data_bridge.delete_by_key("nonexistent_key") + + +def test_get_by_key(data_bridge: DataBridge): + """Test get_by_key retrieves correct value""" + test_data = {"key1": "value1", "key2": date(2024, 1, 1)} + data_bridge.update(test_data) + assert data_bridge.get_by_key("key1") == "value1" + assert data_bridge.get_by_key("key2") == date(2024, 1, 1) + assert data_bridge.get_by_key("nonexistent") is None + + +def test_response_initialization_basic(): + """Test basic Response initialization""" + response = Response(ResponseStatus.ALL_FLAGS_VALID) + assert response.status == ResponseStatus.ALL_FLAGS_VALID + assert response.input_flags == EMPTY_INPUT_FLAGS + + +def test_response_initialization_with_flags(): + """Test Response initialization with input flags""" + input_flags = InputFlags([InputFlag('test', input_value='value', status=None)]) + response = Response(ResponseStatus.INVALID_VALUE_FLAGS, input_flags) + assert response.status == ResponseStatus.INVALID_VALUE_FLAGS + assert response.input_flags == input_flags + + +def test_response_status_types(): + """Test Response with different status types""" + statuses = [ + ResponseStatus.ALL_FLAGS_VALID, + ResponseStatus.UNDEFINED_FLAGS, + ResponseStatus.INVALID_VALUE_FLAGS, + ResponseStatus.UNDEFINED_AND_INVALID_FLAGS + ] + for status in statuses: + response = Response(status) + assert response.status == status From 1d2ab6f6bb8d567e15677ff1bf37bdeb359059af Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 6 Dec 2025 11:55:50 +0300 Subject: [PATCH 5/8] refactor tests and add new --- mock/local_test.py | 20 +++- pyproject.toml | 3 +- src/argenta/command/flag/flags/models.py | 7 +- src/argenta/command/flag/models.py | 9 +- src/argenta/command/models.py | 3 - src/argenta/di/integration.py | 6 +- src/argenta/metrics/main.py | 2 +- src/argenta/orchestrator/argparser/entity.py | 2 +- src/argenta/orchestrator/entity.py | 2 +- src/argenta/response/entity.py | 4 +- src/argenta/router/command_handler/entity.py | 3 - src/argenta/router/exceptions.py | 2 +- tests/unit_tests/test_argparser.py | 8 ++ tests/unit_tests/test_command.py | 12 ++ tests/unit_tests/test_di.py | 81 ++++++++++++++ tests/unit_tests/test_flag.py | 111 +++++++++++++++++++ tests/unit_tests/test_router.py | 50 ++++++++- 17 files changed, 294 insertions(+), 31 deletions(-) create mode 100644 tests/unit_tests/test_di.py diff --git a/mock/local_test.py b/mock/local_test.py index e526fdf..7061395 100644 --- a/mock/local_test.py +++ b/mock/local_test.py @@ -1,5 +1,19 @@ -from argenta import App +from argenta import App, DataBridge, Response, Router +from argenta.di import FromDishka +from argenta.di.integration import setup_dishka, _auto_inject_handlers +from argenta.di.providers import SystemProvider +from dishka import make_container + +container = make_container() + +Response.patch_by_container(container) app = App() -app._setup_default_view() -app._empty_input_command_handler() \ No newline at end of file +router = Router() + +@router.command('command') +def handler(res: Response, data_bridge: FromDishka[DataBridge]): + print(data_bridge) + +_auto_inject_handlers(app) +_auto_inject_handlers(app) diff --git a/pyproject.toml b/pyproject.toml index 6e1f23e..c742baa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,8 @@ reportUnusedFunction = false branch = true omit = [ "src/argenta/app/protocols.py", - "src/argenta/command/exceptions.py" + "src/argenta/command/exceptions.py", + "src/argenta/metrics/*" ] [tool.mypy] diff --git a/src/argenta/command/flag/flags/models.py b/src/argenta/command/flag/flags/models.py index 4edb692..18ab340 100644 --- a/src/argenta/command/flag/flags/models.py +++ b/src/argenta/command/flag/flags/models.py @@ -39,9 +39,6 @@ def __len__(self) -> int: def __iter__(self) -> Iterator[FlagType]: return iter(self.flags) - def __next__(self) -> FlagType: - return next(iter(self)) - def __getitem__(self, flag_index: int) -> FlagType: return self.flags[flag_index] @@ -61,7 +58,7 @@ def get_flag_by_name(self, name: str) -> Flag | None: @override def __eq__(self, other: object) -> bool: if not isinstance(other, Flags): - return NotImplemented + return False if len(self.flags) != len(other.flags): return False @@ -91,7 +88,7 @@ def get_flag_by_name(self, name: str) -> InputFlag | None: @override def __eq__(self, other: object) -> bool: if not isinstance(other, InputFlags): - raise NotImplementedError + return False if len(self.flags) != len(other.flags): return False diff --git a/src/argenta/command/flag/models.py b/src/argenta/command/flag/models.py index 86be447..45630c7 100644 --- a/src/argenta/command/flag/models.py +++ b/src/argenta/command/flag/models.py @@ -53,10 +53,7 @@ def validate_input_flag_value(self, input_flag_value: str) -> bool: if isinstance(self.possible_values, Pattern): return bool(self.possible_values.match(input_flag_value)) - if isinstance(self.possible_values, list): - return input_flag_value in self.possible_values - - return False + return input_flag_value in self.possible_values @property def string_entity(self) -> str: @@ -88,9 +85,9 @@ def __init__( self, name: str, *, - prefix: PREFIX_TYPE = "--", input_value: str, - status: ValidationStatus | None, + prefix: PREFIX_TYPE = "--", + status: ValidationStatus | None = None, ): """ Public. The entity of the flag of the entered command diff --git a/src/argenta/command/models.py b/src/argenta/command/models.py index 6290447..858b174 100644 --- a/src/argenta/command/models.py +++ b/src/argenta/command/models.py @@ -104,9 +104,6 @@ def parse(cls, raw_command: str) -> Self: else: raise UnprocessedInputFlagException - if not name: - raise UnprocessedInputFlagException - if i + 1 < len(tokens) and not tokens[i + 1].startswith("-"): input_value = tokens[i + 1] i += 2 diff --git a/src/argenta/di/integration.py b/src/argenta/di/integration.py index 4c73a3a..f78dd1b 100644 --- a/src/argenta/di/integration.py +++ b/src/argenta/di/integration.py @@ -20,16 +20,16 @@ def inject(func: Callable[..., T]) -> Callable[..., T]: def setup_dishka(app: App, container: Container, *, auto_inject: bool = False) -> None: + Response.patch_by_container(container) if auto_inject: _auto_inject_handlers(app) - Response.patch_by_container(container) def _get_container_from_response(args: tuple[Any, ...], kwargs: dict[str, Any]) -> Container: for arg in args: if isinstance(arg, Response): - if hasattr(arg, "_dishka_container"): - return arg._dishka_container # pyright: ignore[reportPrivateUsage] + if hasattr(arg, "__dishka_container__"): + return arg.__dishka_container__ # pyright: ignore[reportPrivateUsage] break raise RuntimeError("dishka container not found in Response") diff --git a/src/argenta/metrics/main.py b/src/argenta/metrics/main.py index d87c945..12861f7 100644 --- a/src/argenta/metrics/main.py +++ b/src/argenta/metrics/main.py @@ -9,7 +9,7 @@ from argenta import App -def get_time_of_pre_cycle_setup(app: App) -> float: +def get_time_of_pre_cycle_setup(app: App) -> float: """ Public. Return time of pre cycle setup :param app: app instance for testing time of pre cycle setup diff --git a/src/argenta/orchestrator/argparser/entity.py b/src/argenta/orchestrator/argparser/entity.py index 42baaff..47fb358 100644 --- a/src/argenta/orchestrator/argparser/entity.py +++ b/src/argenta/orchestrator/argparser/entity.py @@ -94,7 +94,7 @@ def _parse_args(self) -> None: namespace=self._core.parse_args(), processed_args=self.processed_args ) - def _register_args(self, processed_args: list[ValueArgument | BooleanArgument]) -> None: + def _register_args(self, processed_args: list[ValueArgument | BooleanArgument]) -> None: # pragma: no cover if sys.version_info >= (3, 13): for arg in processed_args: if isinstance(arg, BooleanArgument): diff --git a/src/argenta/orchestrator/entity.py b/src/argenta/orchestrator/entity.py index e4646a1..17fb243 100644 --- a/src/argenta/orchestrator/entity.py +++ b/src/argenta/orchestrator/entity.py @@ -26,7 +26,7 @@ def __init__( self._custom_providers: list[Provider] = custom_providers self._auto_inject_handlers: bool = auto_inject_handlers - self._arg_parser._parse_args() + self._arg_parser._parse_args() # pyright: ignore[reportPrivateUsage] def start_polling(self, app: App) -> None: """ diff --git a/src/argenta/response/entity.py b/src/argenta/response/entity.py index 516e80d..985e864 100644 --- a/src/argenta/response/entity.py +++ b/src/argenta/response/entity.py @@ -9,7 +9,7 @@ class Response: - _dishka_container: Container + __dishka_container__: Container def __init__( self, @@ -26,4 +26,4 @@ def __init__( @classmethod def patch_by_container(cls, container: Container) -> None: - cls._dishka_container = container + cls.__dishka_container__ = container diff --git a/src/argenta/router/command_handler/entity.py b/src/argenta/router/command_handler/entity.py index 300c458..b95afd6 100644 --- a/src/argenta/router/command_handler/entity.py +++ b/src/argenta/router/command_handler/entity.py @@ -44,6 +44,3 @@ def add_handler(self, command_handler: CommandHandler) -> None: def __iter__(self) -> Iterator[CommandHandler]: return iter(self.command_handlers) - - def __next__(self) -> CommandHandler: - return next(iter(self.command_handlers)) diff --git a/src/argenta/router/exceptions.py b/src/argenta/router/exceptions.py index 2c40df0..6754a37 100644 --- a/src/argenta/router/exceptions.py +++ b/src/argenta/router/exceptions.py @@ -20,7 +20,7 @@ class RequiredArgumentNotPassedException(Exception): @override def __str__(self) -> str: - return "Required argument not passed" + return "Required argument with type Response not passed" class TriggerContainSpacesException(Exception): diff --git a/tests/unit_tests/test_argparser.py b/tests/unit_tests/test_argparser.py index 8157991..8b6b07d 100644 --- a/tests/unit_tests/test_argparser.py +++ b/tests/unit_tests/test_argparser.py @@ -245,3 +245,11 @@ def test_argparser_parse_args_populates_argspace( assert debug_arg is not None assert debug_arg.value is True assert debug_arg.founder_class is BooleanArgument + +def test_str_input_argument(): + arg = InputArgument('host', value='192.168.0.0', founder_class=ValueArgument) + assert str(arg) == 'InputArgument(host=192.168.0.0)' + +def test_repr_input_argument(): + arg = InputArgument('host', value='192.168.0.0', founder_class=ValueArgument) + assert repr(arg) == "InputArgument" diff --git a/tests/unit_tests/test_command.py b/tests/unit_tests/test_command.py index 5873e30..4a97dcc 100644 --- a/tests/unit_tests/test_command.py +++ b/tests/unit_tests/test_command.py @@ -23,6 +23,18 @@ def test_parse_raw_command_without_flag_name_with_value(): def test_parse_raw_command_with_repeated_flag_name(): with pytest.raises(RepeatedInputFlagsException): InputCommand.parse('ssh --host 192.168.0.3 --host 172.198.0.43') + + +def test_parse_raw_command_with_triple_prefix(): + assert InputCommand.parse( + 'ssh ---host 192.168.0.0' + ).input_flags.get_flag_by_name('host') == \ + InputFlag('host', input_value='192.168.0.0', prefix='---') + + +def test_parse_raw_command_with_unprocessed_entity(): + with pytest.raises(UnprocessedInputFlagException): + InputCommand.parse('ssh --host 192.168.0.3 9977') def test_parse_empty_raw_command(): diff --git a/tests/unit_tests/test_di.py b/tests/unit_tests/test_di.py new file mode 100644 index 0000000..54f6ac2 --- /dev/null +++ b/tests/unit_tests/test_di.py @@ -0,0 +1,81 @@ +from typing import Generator +from argenta import App, DataBridge, Router +from argenta.di.providers import SystemProvider +from argenta.orchestrator.argparser import ArgParser, ArgSpace +from argenta.response import ResponseStatus +from dishka import Container, make_container +import pytest +from argenta.response.entity import Response +from argenta.di.integration import FromDishka, _get_container_from_response, setup_dishka, _auto_inject_handlers + + +@pytest.fixture +def argparser() -> ArgParser: + return ArgParser(processed_args=[]) + +@pytest.fixture +def container(argparser: ArgParser) -> Generator[Container]: + container = make_container(SystemProvider(), context={ArgParser: argparser}) + yield container + container.close() + + +def test_get_container_from_response(container: Container): + Response.patch_by_container(container) + response = Response(ResponseStatus.ALL_FLAGS_VALID) + assert _get_container_from_response((response,), {}) == container + +def test_get_container_from_response4(container: Container): + Response.patch_by_container(container) + response = Response(ResponseStatus.ALL_FLAGS_VALID) + assert _get_container_from_response((object(), response,), {}) == container + +def test_get_container_from_response2(container: Container): + delattr(Response, '__dishka_container__') + response = Response(ResponseStatus.ALL_FLAGS_VALID) + with pytest.raises(RuntimeError): + _get_container_from_response((response,), {}) + +def test_get_container_from_response3(container: Container): + Response.patch_by_container(container) + with pytest.raises(RuntimeError): + assert _get_container_from_response((), {}) == container + +def test_setup_dishka(container: Container): + app = App() + router = Router() + + @router.command('command') + def handler(res: Response, data_bridge: FromDishka[DataBridge]): + print(data_bridge) + + app.include_router(router) + + assert setup_dishka(app, container, auto_inject=True) is None + +def test_setup_dishka2(container: Container): + app = App() + assert setup_dishka(app, container, auto_inject=False) is None + +def test_auto_inject_handlers(container: Container): + Response.patch_by_container(container) + + app = App() + router = Router() + + @router.command('command') + def handler(res: Response, data_bridge: FromDishka[DataBridge]): + print(data_bridge) + + app.include_router(router) + + _auto_inject_handlers(app) + _auto_inject_handlers(app) # check idempotency + +def test_get_from_container(container: Container): + assert isinstance(container.get(ArgSpace), ArgSpace) + +def test_get_from_container2(container: Container): + assert isinstance(container.get(DataBridge), DataBridge) + + \ No newline at end of file diff --git a/tests/unit_tests/test_flag.py b/tests/unit_tests/test_flag.py index 854a9f4..2e077d4 100644 --- a/tests/unit_tests/test_flag.py +++ b/tests/unit_tests/test_flag.py @@ -1,7 +1,9 @@ import re +from sys import flags from argenta.command.flag import Flag, InputFlag, PossibleValues from argenta.command.flag.flags import Flags, InputFlags +import pytest def test_get_string_entity(): @@ -114,3 +116,112 @@ def test_add_flags(): flags = Flags() flags.add_flags([Flag('test'), Flag('test2')]) assert len(flags.flags) == 2 + +def test_eq_flags(): + flags = Flags([Flag('some')]) + flags2 = Flags([Flag('some')]) + assert flags == flags2 + +def test_contains_flags(): + flags = Flags([Flag('some')]) + flag = Flag('some') + assert flag in flags + +def test_eq_flags2(): + flags = Flags([Flag('some')]) + flags2 = Flags([Flag('other')]) + assert flags != flags2 + +def test_eq_flags3(): + flags = Flags([Flag('some')]) + flags2 = Flags([Flag('some'), Flag('other')]) + assert flags != flags2 + +def test_eq_flags4(): + flags = Flags([Flag('some')]) + not_flags = object() + assert flags != not_flags + +def test_contains_flags2(): + flags = Flags([Flag('some')]) + flag = Flag('nonexists') + assert flag not in flags + +def test_contains_flags3(): + flags = Flags([Flag('some')]) + not_flag = object + with pytest.raises(TypeError): + not_flag in flags # pyright: ignore[reportUnusedExpression] + +def test_get_flag_by_name(): + flags = Flags([Flag('some')]) + assert flags.get_flag_by_name('some') == Flag('some') + +def test_eq_input_flags3(): + flags = InputFlags([InputFlag('some', input_value='')]) + flags2 = InputFlags([ + InputFlag('some', input_value=''), + InputFlag('some2', input_value='') + ]) + assert flags != flags2 + +def test_eq_input_flags4(): + flags = InputFlags([InputFlag('some', input_value='')]) + not_flags = object() + assert flags != not_flags + +def test_contains_input_flags2(): + flags = InputFlags([InputFlag('some', input_value='')]) + flag = InputFlag('nonexists', input_value='') + assert flag not in flags + +def test_contains_input_flags3(): + flags = InputFlags([InputFlag('some', input_value='')]) + not_flag = object + with pytest.raises(TypeError): + not_flag in flags # pyright: ignore[reportUnusedExpression] + +def test_len_flags(): + flags = Flags([Flag('one'), Flag('two')]) + assert len(flags) == 2 + +def test_bool_flags(): + flags = Flags([Flag('one'), Flag('two')]) + assert bool(flags) + +def test_bool_flags2(): + flags = Flags([]) + assert not bool(flags) + +def test_getitem_flags(): + flags = Flags([Flag('one'), Flag('two')]) + assert flags[1] == Flag('two') + +def test_str_flag(): + flag = Flag('two') + assert str(flag) == '--two' + +def test_repr_flag(): + flag = Flag('two') + assert repr(flag) == 'Flag' + +def test_eq_flag(): + flag = Flag('two') + not_flag = object() + with pytest.raises(NotImplementedError): + flag == not_flag # pyright: ignore[reportUnusedExpression] + +def test_str_input_flag(): + flag = InputFlag('two', input_value='value') + assert str(flag) == '--two value' + +def test_repr_input_flag(): + flag = InputFlag('two', input_value='some_value') + assert repr(flag) == 'InputFlag' + +def test_eq_input_flag(): + flag = InputFlag('two', input_value='') + not_flag = object() + with pytest.raises(NotImplementedError): + flag == not_flag # pyright: ignore[reportUnusedExpression] + \ No newline at end of file diff --git a/tests/unit_tests/test_router.py b/tests/unit_tests/test_router.py index 073edea..506baf4 100644 --- a/tests/unit_tests/test_router.py +++ b/tests/unit_tests/test_router.py @@ -1,7 +1,7 @@ import re import pytest -from argenta.command import Command +from argenta.command import Command, InputCommand from argenta.command.flag import Flag, InputFlag from argenta.command.flag.flags import Flags, InputFlags from argenta.command.flag.models import PossibleValues, ValidationStatus @@ -102,3 +102,51 @@ def test_get_router_aliases3(): def handler(response: Response): pass assert router.aliases == set() + +def test_find_appropiate_handler(capsys: pytest.CaptureFixture[str]): + router = Router() + + @router.command(Command('hello', aliases={'hi'})) + def handler(res: Response): + print("Hello World!") + + router.finds_appropriate_handler(InputCommand('hi')) + + output = capsys.readouterr() + + assert "Hello World!" in output.out + +def test_find_appropiate_handler2(capsys: CaptureFixture[str]): + router = Router() + + @router.command(Command('hello', flags=Flag('flag'), aliases={'hi'})) + def handler(res: Response): + print("Hello World!") + + router.finds_appropriate_handler(InputCommand('hi')) + + output = capsys.readouterr() + + assert "Hello World!" in output.out + +def test_wrong_typehint(capsys: pytest.CaptureFixture[str]): + class NotResponse: pass + + def func(response: NotResponse): pass + + _validate_func_args(func) + + output = capsys.readouterr() + + assert "WARNING" in output.out + +def test_missing_typehint(capsys: pytest.CaptureFixture[str]): + def func(response): pass # pyright: ignore[reportMissingParameterType, reportUnknownParameterType] + + _validate_func_args(func) # pyright: ignore[reportUnknownArgumentType] + + output = capsys.readouterr() + + assert output.out == '' + + From dee328525d1c90fc68c1595db7163666402b6cfa Mon Sep 17 00:00:00 2001 From: kolo Date: Sat, 6 Dec 2025 20:01:47 +0300 Subject: [PATCH 6/8] refactor system tests --- .gitignore | 2 + pyproject.toml | 2 +- src/argenta/router/entity.py | 11 - ...t_system_handling_non_standard_behavior.py | 375 ++++++++--------- .../test_system_handling_normal_behavior.py | 381 +++++++++--------- tests/unit_tests/test_flag.py | 1 - tests/unit_tests/test_router.py | 44 +- 7 files changed, 409 insertions(+), 407 deletions(-) diff --git a/.gitignore b/.gitignore index 897b850..a71cd19 100644 --- a/.gitignore +++ b/.gitignore @@ -318,3 +318,5 @@ http-client.private.env.json # Apifox Helper cache .idea/.cache/.Apifox_Helper .idea/ApifoxUploaderProjectSetting.xml + +.zed \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c742baa..eb066f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ reportUnusedFunction = false branch = true omit = [ "src/argenta/app/protocols.py", - "src/argenta/command/exceptions.py", + "src/argenta/*/exceptions.py", "src/argenta/metrics/*" ] diff --git a/src/argenta/router/entity.py b/src/argenta/router/entity.py index fa9a5ea..b267a4f 100644 --- a/src/argenta/router/entity.py +++ b/src/argenta/router/entity.py @@ -129,17 +129,6 @@ def process_input_command(self, input_command_flags: InputFlags, command_handler command_handler.handling(response) -class CommandDecorator: - def __init__(self, router_instance: Router, command: Command): - self.router: Router = router_instance - self.command: Command = command - - def __call__(self, handler_func: Callable[..., None]) -> Callable[..., None]: - _validate_func_args(handler_func) - self.router.command_handlers.add_handler(CommandHandler(handler_func, self.command)) - return handler_func - - def _structuring_input_flags(handled_command: Command, input_flags: InputFlags) -> Response: """ Private. Validates flags of input command diff --git a/tests/system_tests/test_system_handling_non_standard_behavior.py b/tests/system_tests/test_system_handling_non_standard_behavior.py index 47375f1..e952df0 100644 --- a/tests/system_tests/test_system_handling_non_standard_behavior.py +++ b/tests/system_tests/test_system_handling_non_standard_behavior.py @@ -1,10 +1,8 @@ -import io import re import sys -from unittest import TestCase -from unittest.mock import MagicMock, patch +from collections.abc import Iterator -import _io +import pytest from argenta import App, Orchestrator, Router from argenta.command import Command, PredefinedFlags @@ -13,252 +11,255 @@ from argenta.response import Response -class PatchedArgvTestCase(TestCase): - def setUp(self): - super().setUp() - self.patcher = patch.object(sys, 'argv', ['program.py']) - self.mock_argv = self.patcher.start() - self.addCleanup(self.patcher.stop) +@pytest.fixture(autouse=True) +def patch_argv(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(sys, 'argv', ['program.py']) -class TestSystemHandlerNormalWork(PatchedArgvTestCase): - @patch("builtins.input", side_effect=["help", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_incorrect_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() +def _mock_input(inputs: Iterator[str]) -> str: + return next(inputs) - @router.command(Command('test')) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print('test command') - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) - orchestrator.start_polling(app) +# ============================================================================ +# Tests for empty input handling +# ============================================================================ - output = mock_stdout.getvalue() - self.assertIn("\nUnknown command: help\n", output) +def test_empty_input_triggers_empty_command_handler(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() + @router.command(Command('test')) + def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('test command') - @patch("builtins.input", side_effect=["TeSt", "Q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_incorrect_command2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + app.set_empty_command_handler(lambda: print('Empty input command')) + orchestrator.start_polling(app) - @router.command(Command('test')) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print('test command') + output = capsys.readouterr().out - app = App(ignore_command_register=False, - override_system_messages=True, - print_func=print) - app.include_router(router) - app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) - orchestrator.start_polling(app) + assert "\nEmpty input command\n" in output - output = mock_stdout.getvalue() - self.assertIn('\nUnknown command: TeSt\n', output) +# ============================================================================ +# Tests for unknown command handling +# ============================================================================ - @patch("builtins.input", side_effect=["test --help", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command_with_unregistered_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() +def test_unknown_command_triggers_unknown_command_handler(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["help", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() - @router.command(Command('test')) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - undefined_flag = response.input_flags.get_flag_by_name('help') - if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED: - print(f'test command with undefined flag: {undefined_flag.string_entity}') + @router.command(Command('test')) + def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('test command') - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - orchestrator.start_polling(app) + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) + orchestrator.start_polling(app) - output = mock_stdout.getvalue() + output = capsys.readouterr().out - self.assertIn('\ntest command with undefined flag: --help\n', output) + assert "\nUnknown command: help\n" in output - @patch("builtins.input", side_effect=["test --port 22", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command_with_unregistered_flag2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() +def test_case_sensitive_command_triggers_unknown_command_handler(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["TeSt", "Q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() - @router.command(Command('test')) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - undefined_flag = response.input_flags.get_flag_by_name("port") - if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED: - print(f'test command with undefined flag with value: {undefined_flag.string_entity} {undefined_flag.input_value}') - else: - raise + @router.command(Command('test')) + def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('test command') - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - orchestrator.start_polling(app) + app = App(ignore_command_register=False, override_system_messages=True, print_func=print) + app.include_router(router) + app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) + orchestrator.start_polling(app) - output = mock_stdout.getvalue() + output = capsys.readouterr().out - self.assertIn('\ntest command with undefined flag with value: --port 22\n', output) + assert '\nUnknown command: TeSt\n' in output - @patch("builtins.input", side_effect=["test --host 192.168.32.1 --port 132", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command_with_one_correct_flag_an_one_incorrect_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() - flags = Flags([PredefinedFlags.HOST]) +def test_mixed_valid_and_unknown_commands_handled_correctly(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test", "some", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() - @router.command(Command('test', flags=flags)) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - undefined_flag = response.input_flags.get_flag_by_name("port") - if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED: - print(f'connecting to host with flag: {undefined_flag.string_entity} {undefined_flag.input_value}') + @router.command(Command('test')) + def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('test command') - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - orchestrator.start_polling(app) + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) + orchestrator.start_polling(app) - output = mock_stdout.getvalue() + output = capsys.readouterr().out - self.assertIn('\nconnecting to host with flag: --port 132\n', output) + assert re.search(r'\ntest command\n(.|\n)*\nUnknown command: some', output) - @patch("builtins.input", side_effect=["test", "some", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_one_correct_command_and_one_incorrect_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() +def test_multiple_commands_with_unknown_command_in_between(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test", "some", "more", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() - @router.command(Command('test')) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print(f'test command') + @router.command(Command('test')) + def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('test command') - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) - orchestrator.start_polling(app) + @router.command(Command('more')) + def test1(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('more command') - output = mock_stdout.getvalue() + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) + orchestrator.start_polling(app) - self.assertRegex(output, re.compile(r'\ntest command\n(.|\n)*\nUnknown command: some')) + output = capsys.readouterr().out + assert re.search(r'\ntest command\n(.|\n)*\nUnknown command: some\n(.|\n)*\nmore command', output) - @patch("builtins.input", side_effect=["test", "some", "more", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_two_correct_commands_and_one_incorrect_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() - @router.command(Command('test')) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print(f'test command') +# ============================================================================ +# Tests for unregistered flag handling +# ============================================================================ - @router.command(Command('more')) - def test1(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print(f'more command') - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - app.set_unknown_command_handler(lambda command: print(f'Unknown command: {command.trigger}')) - orchestrator.start_polling(app) +def test_unregistered_flag_without_value_is_accessible(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test --help", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() - output = mock_stdout.getvalue() + @router.command(Command('test')) + def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] + undefined_flag = response.input_flags.get_flag_by_name('help') + if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED: + print(f'test command with undefined flag: {undefined_flag.string_entity}') - self.assertRegex(output, re.compile(r'\ntest command\n(.|\n)*\nUnknown command: some\n(.|\n)*\nmore command')) + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + orchestrator.start_polling(app) + output = capsys.readouterr().out - @patch("builtins.input", side_effect=["test 535 --port", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command_with_incorrect_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() + assert '\ntest command with undefined flag: --help\n' in output - @router.command(Command('test')) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print(f'test command') - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - app.set_incorrect_input_syntax_handler(lambda command: print(f'Incorrect flag syntax: "{command}"')) - orchestrator.start_polling(app) +def test_unregistered_flag_with_value_is_accessible(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test --port 22", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() - output = mock_stdout.getvalue() + @router.command(Command('test')) + def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] + undefined_flag = response.input_flags.get_flag_by_name("port") + if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED: + print(f'test command with undefined flag with value: {undefined_flag.string_entity} {undefined_flag.input_value}') + else: + raise - self.assertIn("\nIncorrect flag syntax: \"test 535 --port\"\n", output) + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + orchestrator.start_polling(app) + output = capsys.readouterr().out - @patch("builtins.input", side_effect=["", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_empty_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() + assert '\ntest command with undefined flag with value: --port 22\n' in output - @router.command(Command('test')) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print(f'test command') - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - app.set_empty_command_handler(lambda: print('Empty input command')) - orchestrator.start_polling(app) +def test_registered_and_unregistered_flags_coexist(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test --host 192.168.32.1 --port 132", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() + flags = Flags([PredefinedFlags.HOST]) - output = mock_stdout.getvalue() + @router.command(Command('test', flags=flags)) + def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] + undefined_flag = response.input_flags.get_flag_by_name("port") + if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED: + print(f'connecting to host with flag: {undefined_flag.string_entity} {undefined_flag.input_value}') - self.assertIn("\nEmpty input command\n", output) + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + orchestrator.start_polling(app) + output = capsys.readouterr().out - @patch("builtins.input", side_effect=["test --port 22 --port 33", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command_with_repeated_flags(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() + assert '\nconnecting to host with flag: --port 132\n' in output - @router.command(Command('test', flags=PredefinedFlags.PORT)) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print('test command') - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"')) - orchestrator.start_polling(app) +# ============================================================================ +# Tests for incorrect flag syntax handling +# ============================================================================ - output = mock_stdout.getvalue() - self.assertIn('Repeated input flags: "test --port 22 --port 33"', output) +def test_flag_without_value_triggers_incorrect_syntax_handler(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test 535 --port", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() - @patch("builtins.input", side_effect=["test --help", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command_with_unregistered_flag3(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() + @router.command(Command('test')) + def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('test command') - @router.command(Command('test')) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - undefined_flag = response.input_flags.get_flag_by_name('help') - if undefined_flag and undefined_flag.status == ValidationStatus.UNDEFINED: - print(f'test command with undefined flag: {undefined_flag.string_entity}') + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + app.set_incorrect_input_syntax_handler(lambda command: print(f'Incorrect flag syntax: "{command}"')) + orchestrator.start_polling(app) - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - orchestrator.start_polling(app) + output = capsys.readouterr().out - output = mock_stdout.getvalue() + assert "\nIncorrect flag syntax: \"test 535 --port\"\n" in output - self.assertIn('\ntest command with undefined flag: --help\n', output) + +# ============================================================================ +# Tests for repeated flag handling +# ============================================================================ + + +def test_repeated_flags_trigger_repeated_flags_handler(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test --port 22 --port 33", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() + + @router.command(Command('test', flags=PredefinedFlags.PORT)) + def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('test command') + + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + app.set_repeated_input_flags_handler(lambda command: print(f'Repeated input flags: "{command}"')) + orchestrator.start_polling(app) + + output = capsys.readouterr().out + + assert 'Repeated input flags: "test --port 22 --port 33"' in output diff --git a/tests/system_tests/test_system_handling_normal_behavior.py b/tests/system_tests/test_system_handling_normal_behavior.py index 92ade73..77e88c6 100644 --- a/tests/system_tests/test_system_handling_normal_behavior.py +++ b/tests/system_tests/test_system_handling_normal_behavior.py @@ -1,10 +1,8 @@ -import io import re import sys -from unittest import TestCase -from unittest.mock import MagicMock, patch +from collections.abc import Iterator -import _io +import pytest from argenta import App, Orchestrator, Router from argenta.command import Command, PredefinedFlags @@ -14,244 +12,261 @@ from argenta.response import Response -class PatchedArgvTestCase(TestCase): - def setUp(self): - super().setUp() - self.patcher = patch.object(sys, 'argv', ['program.py']) - self.mock_argv = self.patcher.start() - self.addCleanup(self.patcher.stop) +@pytest.fixture(autouse=True) +def patch_argv(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(sys, 'argv', ['program.py']) -class TestSystemHandlerNormalWork(PatchedArgvTestCase): - @patch("builtins.input", side_effect=["test", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() +def _mock_input(inputs: Iterator[str]) -> str: + return next(inputs) - @router.command(Command('test')) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print('test command') - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - orchestrator.start_polling(app) +# ============================================================================ +# Tests for basic command execution +# ============================================================================ - output = mock_stdout.getvalue() - self.assertIn('\ntest command\n', output) +def test_simple_command_executes_successfully(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() + @router.command(Command('test')) + def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('test command') - @patch("builtins.input", side_effect=["TeSt", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + orchestrator.start_polling(app) - @router.command(Command('test')) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print('test command') + output = capsys.readouterr().out - app = App(ignore_command_register=True, - override_system_messages=True, - print_func=print) - app.include_router(router) - orchestrator.start_polling(app) + assert '\ntest command\n' in output - output = mock_stdout.getvalue() - self.assertIn('\ntest command\n', output) +def test_case_insensitive_command_executes_when_enabled(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["TeSt", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() + @router.command(Command('test')) + def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('test command') - @patch("builtins.input", side_effect=["test --help", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command_with_custom_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() - flag = Flag('help', prefix='--', possible_values=PossibleValues.NEITHER) + app = App(ignore_command_register=True, override_system_messages=True, print_func=print) + app.include_router(router) + orchestrator.start_polling(app) - @router.command(Command('test', flags=flag)) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - valid_flag = response.input_flags.get_flag_by_name('help') - if valid_flag and valid_flag.status == ValidationStatus.VALID: - print(f'\nhelp for {valid_flag.name} flag\n') + output = capsys.readouterr().out - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - orchestrator.start_polling(app) + assert '\ntest command\n' in output - output = mock_stdout.getvalue() - self.assertIn('\nhelp for help flag\n', output) +def test_two_commands_execute_sequentially(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test", "some", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() - @patch("builtins.input", side_effect=["test --port 22", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command_with_custom_flag2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() - flag = Flag('port', prefix='--', possible_values=re.compile(r'^\d{1,5}$')) + @router.command(Command('test')) + def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('test command') - @router.command(Command('test', flags=flag)) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - valid_flag = response.input_flags.get_flag_by_name('port') - if valid_flag and valid_flag.status == ValidationStatus.VALID: - print(f'flag value for {valid_flag.name} flag : {valid_flag.input_value}') + @router.command(Command('some')) + def test2(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('some command') - app = App( - override_system_messages=True, - repeat_command_groups_printing=True, - print_func=print - ) - app.include_router(router) - orchestrator.start_polling(app) + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + orchestrator.start_polling(app) - output = mock_stdout.getvalue() + output = capsys.readouterr().out - self.assertIn('\nflag value for port flag : 22\n', output) + assert re.search(r'\ntest command\n(.|\n)*\nsome command\n', output) - @patch("builtins.input", side_effect=["test -H", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command_with_default_flag(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() - flag = PredefinedFlags.SHORT_HELP +def test_three_commands_execute_sequentially(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test", "some", "more", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() - @router.command(Command('test', flags=flag)) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - valid_flag = response.input_flags.get_flag_by_name('H') - if valid_flag and valid_flag.status == ValidationStatus.VALID: - print(f'help for {valid_flag.name} flag') + @router.command(Command('test')) + def test(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('test command') - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - orchestrator.start_polling(app) + @router.command(Command('some')) + def test1(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('some command') - output = mock_stdout.getvalue() + @router.command(Command('more')) + def test2(_response: Response) -> None: # pyright: ignore[reportUnusedFunction] + print('more command') - self.assertIn('\nhelp for H flag\n', output) + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + orchestrator.start_polling(app) + output = capsys.readouterr().out - @patch("builtins.input", side_effect=["test --info", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command_with_default_flag2(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() - flag = PredefinedFlags.INFO + assert re.search(r'\ntest command\n(.|\n)*\nsome command\n(.|\n)*\nmore command', output) - @router.command(Command('test', flags=flag)) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - valid_flag = response.input_flags.get_flag_by_name('info') - if valid_flag and valid_flag.status == ValidationStatus.VALID: - print('info about test command') - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - orchestrator.start_polling(app) +# ============================================================================ +# Tests for custom flag handling +# ============================================================================ - output = mock_stdout.getvalue() - self.assertIn('\ninfo about test command\n', output) +def test_custom_flag_without_value_is_recognized(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test --help", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() + flag = Flag('help', prefix='--', possible_values=PossibleValues.NEITHER) + @router.command(Command('test', flags=flag)) + def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] + valid_flag = response.input_flags.get_flag_by_name('help') + if valid_flag and valid_flag.status == ValidationStatus.VALID: + print(f'\nhelp for {valid_flag.name} flag\n') - @patch("builtins.input", side_effect=["test --host 192.168.0.1", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command_with_default_flag3(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() - flag = PredefinedFlags.HOST + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + orchestrator.start_polling(app) - @router.command(Command('test', flags=flag)) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - valid_flag = response.input_flags.get_flag_by_name('host') - if valid_flag and valid_flag.status == ValidationStatus.VALID: - print(f'connecting to host {valid_flag.input_value}') + output = capsys.readouterr().out - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - orchestrator.start_polling(app) + assert '\nhelp for help flag\n' in output - output = mock_stdout.getvalue() - self.assertIn('\nconnecting to host 192.168.0.1\n', output) +def test_custom_flag_with_regex_validation_accepts_valid_value(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test --port 22", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() + flag = Flag('port', prefix='--', possible_values=re.compile(r'^\d{1,5}$')) + @router.command(Command('test', flags=flag)) + def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] + valid_flag = response.input_flags.get_flag_by_name('port') + if valid_flag and valid_flag.status == ValidationStatus.VALID: + print(f'flag value for {valid_flag.name} flag : {valid_flag.input_value}') - @patch("builtins.input", side_effect=["test --host 192.168.32.1 --port 132", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_correct_command_with_two_flags(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() - flags = Flags([PredefinedFlags.HOST, PredefinedFlags.PORT]) + app = App(override_system_messages=True, repeat_command_groups_printing=True, print_func=print) + app.include_router(router) + orchestrator.start_polling(app) - @router.command(Command('test', flags=flags)) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - host_flag = response.input_flags.get_flag_by_name('host') - port_flag = response.input_flags.get_flag_by_name('port') - if (host_flag and host_flag.status == ValidationStatus.VALID) and (port_flag and port_flag.status == ValidationStatus.VALID): - print(f'connecting to host {host_flag.input_value} and port {port_flag.input_value}') + output = capsys.readouterr().out - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - orchestrator.start_polling(app) + assert '\nflag value for port flag : 22\n' in output - output = mock_stdout.getvalue() - self.assertIn('\nconnecting to host 192.168.32.1 and port 132\n', output) +# ============================================================================ +# Tests for predefined flag handling +# ============================================================================ - @patch("builtins.input", side_effect=["test", "some", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_two_correct_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() +def test_predefined_short_help_flag_is_recognized(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test -H", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() + flag = PredefinedFlags.SHORT_HELP - @router.command(Command('test')) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print(f'test command') + @router.command(Command('test', flags=flag)) + def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] + valid_flag = response.input_flags.get_flag_by_name('H') + if valid_flag and valid_flag.status == ValidationStatus.VALID: + print(f'help for {valid_flag.name} flag') - @router.command(Command('some')) - def test2(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print(f'some command') + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + orchestrator.start_polling(app) - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - orchestrator.start_polling(app) + output = capsys.readouterr().out - output = mock_stdout.getvalue() + assert '\nhelp for H flag\n' in output - self.assertRegex(output, re.compile(r'\ntest command\n(.|\n)*\nsome command\n')) +def test_predefined_info_flag_is_recognized(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test --info", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() + flag = PredefinedFlags.INFO - @patch("builtins.input", side_effect=["test", "some", "more", "q"]) - @patch("sys.stdout", new_callable=io.StringIO) - def test_input_three_correct_command(self, mock_stdout: _io.StringIO, magick_mock: MagicMock): - router = Router() - orchestrator = Orchestrator() + @router.command(Command('test', flags=flag)) + def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] + valid_flag = response.input_flags.get_flag_by_name('info') + if valid_flag and valid_flag.status == ValidationStatus.VALID: + print('info about test command') - @router.command(Command('test')) - def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print(f'test command') + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + orchestrator.start_polling(app) - @router.command(Command('some')) - def test1(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print(f'some command') + output = capsys.readouterr().out - @router.command(Command('more')) - def test2(response: Response) -> None: # pyright: ignore[reportUnusedFunction] - print(f'more command') + assert '\ninfo about test command\n' in output - app = App(override_system_messages=True, - print_func=print) - app.include_router(router) - orchestrator.start_polling(app) - output = mock_stdout.getvalue() +def test_predefined_host_flag_with_value_is_recognized(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test --host 192.168.0.1", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() + flag = PredefinedFlags.HOST - self.assertRegex(output, re.compile(r'\ntest command\n(.|\n)*\nsome command\n(.|\n)*\nmore command')) + @router.command(Command('test', flags=flag)) + def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] + valid_flag = response.input_flags.get_flag_by_name('host') + if valid_flag and valid_flag.status == ValidationStatus.VALID: + print(f'connecting to host {valid_flag.input_value}') + + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + orchestrator.start_polling(app) + + output = capsys.readouterr().out + + assert '\nconnecting to host 192.168.0.1\n' in output + + +# ============================================================================ +# Tests for multiple flag handling +# ============================================================================ + + +def test_two_predefined_flags_are_recognized_together(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: + inputs = iter(["test --host 192.168.32.1 --port 132", "q"]) + monkeypatch.setattr('builtins.input', lambda _prompt="": _mock_input(inputs)) + + router = Router() + orchestrator = Orchestrator() + flags = Flags([PredefinedFlags.HOST, PredefinedFlags.PORT]) + + @router.command(Command('test', flags=flags)) + def test(response: Response) -> None: # pyright: ignore[reportUnusedFunction] + host_flag = response.input_flags.get_flag_by_name('host') + port_flag = response.input_flags.get_flag_by_name('port') + if (host_flag and host_flag.status == ValidationStatus.VALID) and (port_flag and port_flag.status == ValidationStatus.VALID): + print(f'connecting to host {host_flag.input_value} and port {port_flag.input_value}') + + app = App(override_system_messages=True, print_func=print) + app.include_router(router) + orchestrator.start_polling(app) + + output = capsys.readouterr().out + + assert '\nconnecting to host 192.168.32.1 and port 132\n' in output diff --git a/tests/unit_tests/test_flag.py b/tests/unit_tests/test_flag.py index 2e077d4..deb092e 100644 --- a/tests/unit_tests/test_flag.py +++ b/tests/unit_tests/test_flag.py @@ -1,5 +1,4 @@ import re -from sys import flags from argenta.command.flag import Flag, InputFlag, PossibleValues from argenta.command.flag.flags import Flags, InputFlags diff --git a/tests/unit_tests/test_router.py b/tests/unit_tests/test_router.py index 506baf4..125cb4c 100644 --- a/tests/unit_tests/test_router.py +++ b/tests/unit_tests/test_router.py @@ -1,5 +1,6 @@ import re import pytest +from pytest import CaptureFixture from argenta.command import Command, InputCommand from argenta.command.flag import Flag, InputFlag @@ -89,64 +90,59 @@ def handler(response: Response) -> None: def test_get_router_aliases2(): router = Router() @router.command(Command('some', aliases={'test', 'case'})) - def handler(response: Response): + def handler(response: Response): pass @router.command(Command('ext', aliases={'more', 'foo'})) - def handler2(response: Response): + def handler2(response: Response): pass assert router.aliases == {'test', 'case', 'more', 'foo'} def test_get_router_aliases3(): router = Router() @router.command(Command('some')) - def handler(response: Response): + def handler(response: Response): pass assert router.aliases == set() - + def test_find_appropiate_handler(capsys: pytest.CaptureFixture[str]): router = Router() - + @router.command(Command('hello', aliases={'hi'})) def handler(res: Response): print("Hello World!") - + router.finds_appropriate_handler(InputCommand('hi')) - + output = capsys.readouterr() - + assert "Hello World!" in output.out - + def test_find_appropiate_handler2(capsys: CaptureFixture[str]): router = Router() - + @router.command(Command('hello', flags=Flag('flag'), aliases={'hi'})) def handler(res: Response): print("Hello World!") - + router.finds_appropriate_handler(InputCommand('hi')) - + output = capsys.readouterr() - + assert "Hello World!" in output.out - + def test_wrong_typehint(capsys: pytest.CaptureFixture[str]): class NotResponse: pass - + def func(response: NotResponse): pass - + _validate_func_args(func) - + output = capsys.readouterr() - + assert "WARNING" in output.out - + def test_missing_typehint(capsys: pytest.CaptureFixture[str]): def func(response): pass # pyright: ignore[reportMissingParameterType, reportUnknownParameterType] - _validate_func_args(func) # pyright: ignore[reportUnknownArgumentType] - output = capsys.readouterr() - assert output.out == '' - - From 9423034a089b8f2c5fb2ca374ba65b97351a4f6b Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 7 Dec 2025 01:33:39 +0300 Subject: [PATCH 7/8] better testsssssss --- tests/unit_tests/test_app.py | 340 ++++++++++++++----------- tests/unit_tests/test_argparser.py | 186 +++++++++----- tests/unit_tests/test_autocompleter.py | 86 +++++-- tests/unit_tests/test_command.py | 105 +++++--- tests/unit_tests/test_di.py | 93 +++++-- tests/unit_tests/test_dividing_line.py | 53 +++- tests/unit_tests/test_flag.py | 324 ++++++++++++++--------- tests/unit_tests/test_response.py | 76 ++++-- tests/unit_tests/test_router.py | 156 ++++++++---- 9 files changed, 922 insertions(+), 497 deletions(-) diff --git a/tests/unit_tests/test_app.py b/tests/unit_tests/test_app.py index 7e88545..fd1f7f8 100644 --- a/tests/unit_tests/test_app.py +++ b/tests/unit_tests/test_app.py @@ -1,288 +1,343 @@ -from argenta.app.dividing_line import DynamicDividingLine, StaticDividingLine -from argenta.app.protocols import DescriptionMessageGenerator, NonStandardBehaviorHandler +import pytest from pytest import CaptureFixture from argenta.app import App -from argenta.response import Response +from argenta.app.dividing_line import DynamicDividingLine, StaticDividingLine +from argenta.app.protocols import DescriptionMessageGenerator, NonStandardBehaviorHandler from argenta.command.models import Command, InputCommand +from argenta.response import Response from argenta.router import Router -import pytest -def test_is_exit_command1(): +# ============================================================================ +# Tests for exit command detection +# ============================================================================ + + +def test_default_exit_command_lowercase_q_is_recognized() -> None: app = App() assert app._is_exit_command(InputCommand('q')) is True -def test_is_exit_command5(): +def test_default_exit_command_uppercase_q_is_recognized() -> None: app = App() assert app._is_exit_command(InputCommand('Q')) is True -def test_is_exit_command2(): +def test_exit_command_not_recognized_when_case_sensitivity_enabled() -> None: app = App(ignore_command_register=False) assert app._is_exit_command(InputCommand('q')) is False -def test_is_exit_command3(): +def test_custom_exit_command_is_recognized() -> None: app = App(exit_command=Command('quit')) assert app._is_exit_command(InputCommand('quit')) is True -def test_is_exit_command4(): +def test_custom_exit_command_case_insensitive_by_default() -> None: app = App(exit_command=Command('quit')) assert app._is_exit_command(InputCommand('qUIt')) is True -def test_is_exit_command6(): - app = App(ignore_command_register=False, - exit_command=Command('quit')) +def test_custom_exit_command_case_sensitive_when_enabled() -> None: + app = App(ignore_command_register=False, exit_command=Command('quit')) assert app._is_exit_command(InputCommand('qUIt')) is False -def test_is_unknown_command1(): +def test_exit_command_alias_is_recognized() -> None: + app = App(exit_command=Command('q', aliases={'exit'})) + assert app._is_exit_command(InputCommand('exit')) is True + + +def test_exit_command_alias_case_sensitive_when_enabled() -> None: + app = App(exit_command=Command('q', aliases={'exit'}), ignore_command_register=False) + assert app._is_exit_command(InputCommand('exit')) is True + + +def test_non_exit_command_is_not_recognized() -> None: + app = App(exit_command=Command('q', aliases={'exit'})) + assert app._is_exit_command(InputCommand('quit')) is False + + +def test_non_exit_command_with_wrong_case_is_not_recognized() -> None: + app = App(exit_command=Command('q', aliases={'exit'}), ignore_command_register=False) + assert app._is_exit_command(InputCommand('Exit')) is False + + +# ============================================================================ +# Tests for unknown command detection +# ============================================================================ + + +def test_registered_command_is_not_unknown() -> None: app = App() app.set_unknown_command_handler(lambda command: None) app._current_matching_triggers_with_routers = {'fr': Router(), 'tr': Router(), 'de': Router()} assert app._is_unknown_command(InputCommand('fr')) is False -def test_is_unknown_command2(): +def test_unregistered_command_is_unknown() -> None: app = App() app.set_unknown_command_handler(lambda command: None) app._current_matching_triggers_with_routers = {'fr': Router(), 'tr': Router(), 'de': Router()} assert app._is_unknown_command(InputCommand('cr')) is True -def test_is_unknown_command3(): + +def test_command_with_wrong_case_is_unknown_when_case_sensitivity_enabled() -> None: app = App(ignore_command_register=False) app.set_unknown_command_handler(lambda command: None) app._current_matching_triggers_with_routers = {'Pr': Router(), 'tW': Router(), 'deQW': Router()} assert app._is_unknown_command(InputCommand('pr')) is True -def test_is_unknown_command4(): +def test_command_with_exact_case_is_not_unknown_when_case_sensitivity_enabled() -> None: app = App(ignore_command_register=False) app.set_unknown_command_handler(lambda command: None) app._current_matching_triggers_with_routers = {'Pr': Router(), 'tW': Router(), 'deQW': Router()} assert app._is_unknown_command(InputCommand('tW')) is False -def test_add_messages_on_startup(): - app = App() - app.add_message_on_startup('Some message') - assert app._messages_on_startup == ['Some message'] -def test_include_routers(): - app = App() - router = Router() - router2 = Router() - app.include_routers(router, router2) +# ============================================================================ +# Tests for similar command suggestions +# ============================================================================ - assert app.registered_routers.registered_routers == [router, router2] -def test_overlapping_aliases(capsys: CaptureFixture[str]): +def test_most_similar_command_finds_closest_match() -> None: app = App(override_system_messages=True) router = Router() - @router.command(Command('test', aliases={'alias'})) - def handler(res: Response): + @router.command(Command('port', aliases={'p'})) + def handler(_res: Response) -> None: pass - @router.command(Command('test2', aliases={'alias'})) - def handler2(res: Response): + @router.command(Command('host', aliases={'h'})) + def handler2(_res: Response) -> None: pass app.include_routers(router) app._pre_cycle_setup() - - captured = capsys.readouterr() - - assert "Overlapping" in captured.out - -def test_print_framed_text(capsys: CaptureFixture[str]): - app = App( - override_system_messages=True, - dividing_line=StaticDividingLine(length=5) - ) - app._print_framed_text('test') - - captured = capsys.readouterr() - - assert '\n-----\n\ntest\n\n-----\n' in captured.out - -def test_print_framed_text2(capsys: CaptureFixture[str]): - app = App( - override_system_messages=True, - dividing_line=DynamicDividingLine() - ) - app._print_framed_text('test as test as test') - - captured = capsys.readouterr() - - assert '\n' + '-'*20 + '\n\ntest as test as test\n\n' + '-'*20 + '\n' in captured.out -def test_print_framed_text3(capsys: CaptureFixture[str]): - app = App( - override_system_messages=True, - dividing_line=DynamicDividingLine() - ) - app._print_framed_text('some long test') + assert app._most_similar_command('por') == 'port' - captured = capsys.readouterr() - assert '\n--------------\n\nsome long test\n\n--------------\n' in captured.out - -def test_print_framed_text4(): - class OtherDividingLine: - pass - - app = App( - override_system_messages=True, - dividing_line=OtherDividingLine() # pyright: ignore[reportArgumentType] - ) - - with pytest.raises(NotImplementedError): - app._print_framed_text('some long test') - -def test_most_similiar_command(): +def test_most_similar_command_prefers_shorter_match() -> None: app = App(override_system_messages=True) router = Router() - @router.command(Command('port', aliases={'p'})) - def handler(res: Response): + @router.command(Command('command')) + def handler(_res: Response) -> None: pass - @router.command(Command('host', aliases={'h'})) - def handler2(res: Response): + @router.command(Command('command_other')) + def handler2(_res: Response) -> None: pass app.include_routers(router) app._pre_cycle_setup() - assert app._most_similar_command('por') == 'port' - -def test_most_similiar_command2(): + assert app._most_similar_command('com') == 'command' + + +def test_most_similar_command_finds_longer_match_when_closer() -> None: app = App(override_system_messages=True) router = Router() @router.command(Command('command')) - def handler(res: Response): + def handler(_res: Response) -> None: pass @router.command(Command('command_other')) - def handler2(res: Response): + def handler2(_res: Response) -> None: pass app.include_routers(router) app._pre_cycle_setup() - assert app._most_similar_command('com') == 'command' - -def test_most_similiar_command3(): + assert app._most_similar_command('command_') == 'command_other' + + +def test_most_similar_command_returns_none_for_no_match() -> None: app = App(override_system_messages=True) router = Router() @router.command(Command('command')) - def handler(res: Response): + def handler(_res: Response) -> None: pass @router.command(Command('command_other')) - def handler2(res: Response): + def handler2(_res: Response) -> None: pass app.include_routers(router) app._pre_cycle_setup() - assert app._most_similar_command('command_') == 'command_other' - -def test_most_similiar_command4(): + assert app._most_similar_command('nonexists') is None + + +def test_most_similar_command_matches_aliases() -> None: app = App(override_system_messages=True) router = Router() - @router.command(Command('command')) - def handler(res: Response): + @router.command(Command('command', aliases={'other_name'})) + def handler(_res: Response) -> None: pass - @router.command(Command('command_other')) - def handler2(res: Response): + @router.command(Command('command_other', aliases={'more_name'})) + def handler2(_res: Response) -> None: pass app.include_routers(router) app._pre_cycle_setup() - assert app._most_similar_command('nonexists') is None - -def test_most_similiar_command5(): + assert app._most_similar_command('othe') == 'other_name' + + +# ============================================================================ +# Tests for router registration +# ============================================================================ + + +def test_include_routers_registers_multiple_routers() -> None: + app = App() + router = Router() + router2 = Router() + app.include_routers(router, router2) + + assert app.registered_routers.registered_routers == [router, router2] + + +def test_overlapping_aliases_prints_warning(capsys: CaptureFixture[str]) -> None: app = App(override_system_messages=True) router = Router() - @router.command(Command('command', aliases={'other_name'})) - def handler(res: Response): + @router.command(Command('test', aliases={'alias'})) + def handler(_res: Response) -> None: pass - @router.command(Command('command_other', aliases={'more_name'})) - def handler2(res: Response): + @router.command(Command('test2', aliases={'alias'})) + def handler2(_res: Response) -> None: pass app.include_routers(router) app._pre_cycle_setup() + + captured = capsys.readouterr() + + assert "Overlapping" in captured.out + + +# ============================================================================ +# Tests for startup messages +# ============================================================================ + + +def test_add_message_on_startup_stores_message() -> None: + app = App() + app.add_message_on_startup('Some message') + assert app._messages_on_startup == ['Some message'] + + +def test_pre_cycle_setup_prints_startup_messages(capsys: CaptureFixture[str]) -> None: + app = App() + app.add_message_on_startup('some message') + app._pre_cycle_setup() + stdout = capsys.readouterr() - assert app._most_similar_command('othe') == 'other_name' + assert 'some message' in stdout.out + + +# ============================================================================ +# Tests for framed text printing +# ============================================================================ + + +def test_print_framed_text_with_static_dividing_line(capsys: CaptureFixture[str]) -> None: + app = App(override_system_messages=True, dividing_line=StaticDividingLine(length=5)) + app._print_framed_text('test') + + captured = capsys.readouterr() + + assert '\n-----\n\ntest\n\n-----\n' in captured.out + + +def test_print_framed_text_with_dynamic_dividing_line_short_text(capsys: CaptureFixture[str]) -> None: + app = App(override_system_messages=True, dividing_line=DynamicDividingLine()) + app._print_framed_text('some long test') + + captured = capsys.readouterr() + + assert '\n--------------\n\nsome long test\n\n--------------\n' in captured.out + + +def test_print_framed_text_with_dynamic_dividing_line_long_text(capsys: CaptureFixture[str]) -> None: + app = App(override_system_messages=True, dividing_line=DynamicDividingLine()) + app._print_framed_text('test as test as test') + + captured = capsys.readouterr() + + assert '\n' + '-'*20 + '\n\ntest as test as test\n\n' + '-'*20 + '\n' in captured.out + + +def test_print_framed_text_with_unsupported_dividing_line_raises_error() -> None: + class OtherDividingLine: + pass + + app = App(override_system_messages=True, dividing_line=OtherDividingLine()) # pyright: ignore[reportArgumentType] -def test_app_set_description_message_gen(): + with pytest.raises(NotImplementedError): + app._print_framed_text('some long test') + + +# ============================================================================ +# Tests for handler configuration +# ============================================================================ + + +def test_set_description_message_pattern_stores_generator() -> None: app = App() descr_gen: DescriptionMessageGenerator = lambda command, description: command + '-+-' + description app.set_description_message_pattern(descr_gen) assert app._description_message_gen is descr_gen - -def test_app_set_exit_handler(): + + +def test_set_exit_command_handler_stores_handler() -> None: app = App() handler: NonStandardBehaviorHandler[Response] = lambda response: print('goodbye') app.set_exit_command_handler(handler) assert app._exit_command_handler is handler - -def test_app_is_exit_command(): - app = App(exit_command=Command('q', aliases={'exit'})) - assert app._is_exit_command(InputCommand('exit')) is True - -def test_app_is_exit_command2(): - app = App(exit_command=Command('q', aliases={'exit'}), ignore_command_register=False) - assert app._is_exit_command(InputCommand('exit')) is True - -def test_app_is_exit_command3(): - app = App(exit_command=Command('q', aliases={'exit'})) - assert app._is_exit_command(InputCommand('quit')) is False - -def test_app_is_exit_command4(): - app = App(exit_command=Command('q', aliases={'exit'}), ignore_command_register=False) - assert app._is_exit_command(InputCommand('Exit')) is False - -def test_app_setup_default_view(): + + +# ============================================================================ +# Tests for default view setup +# ============================================================================ + + +def test_setup_default_view_formats_prompt() -> None: app = App(prompt='>>') app._setup_default_view() assert app._prompt == '[italic dim bold]>>' - -def test_app_setup_default_view2(): + + +def test_setup_default_view_sets_default_unknown_command_handler() -> None: app = App() app._setup_default_view() assert app._unknown_command_handler(InputCommand('nonexists')) is None - -def test_app_pre_cycle_setup(capsys: CaptureFixture[str]): - app = App() - app.add_message_on_startup('some message') - app._pre_cycle_setup() - stdout = capsys.readouterr() - - assert 'some message' in stdout.out - -def test_app_with_router_with_disable_redirect_stdout(capsys: CaptureFixture[str]): + + +# ============================================================================ +# Tests for command processing +# ============================================================================ + + +def test_process_command_with_router_with_disabled_stdout_redirect(capsys: CaptureFixture[str]) -> None: app = App(repeat_command_groups_printing=True) router = Router(disable_redirect_stdout=True) @router.command('command') - def handler(res: Response): + def handler(_res: Response) -> None: print("Hello!") app.include_router(router) @@ -293,4 +348,3 @@ def handler(res: Response): stdout = capsys.readouterr() assert 'Hello!' in stdout.out - diff --git a/tests/unit_tests/test_argparser.py b/tests/unit_tests/test_argparser.py index 8b6b07d..0c8b29c 100644 --- a/tests/unit_tests/test_argparser.py +++ b/tests/unit_tests/test_argparser.py @@ -1,7 +1,6 @@ import sys from argparse import Namespace from typing import TYPE_CHECKING -from unittest.mock import call import pytest from pytest_mock import MockerFixture @@ -18,7 +17,12 @@ from pytest_mock.plugin import MockType -def test_value_argument_creation() -> None: +# ============================================================================ +# Tests for argument model creation +# ============================================================================ + + +def test_value_argument_stores_all_properties() -> None: arg: ValueArgument = ValueArgument( name="test_arg", prefix="--", @@ -39,7 +43,7 @@ def test_value_argument_creation() -> None: assert arg.string_entity == "--test_arg" -def test_boolean_argument_creation() -> None: +def test_boolean_argument_stores_all_properties() -> None: arg: BooleanArgument = BooleanArgument( name="verbose", prefix="-", help="Enable verbose mode.", is_deprecated=True ) @@ -51,7 +55,7 @@ def test_boolean_argument_creation() -> None: assert arg.string_entity == "-verbose" -def test_input_argument_creation() -> None: +def test_input_argument_stores_all_properties() -> None: arg: InputArgument = InputArgument( name="file", value="/path/to/file", founder_class=ValueArgument ) @@ -60,6 +64,21 @@ def test_input_argument_creation() -> None: assert arg.founder_class is ValueArgument +def test_input_argument_str_representation() -> None: + arg = InputArgument('host', value='192.168.0.0', founder_class=ValueArgument) + assert str(arg) == 'InputArgument(host=192.168.0.0)' + + +def test_input_argument_repr_representation() -> None: + arg = InputArgument('host', value='192.168.0.0', founder_class=ValueArgument) + assert repr(arg) == "InputArgument" + + +# ============================================================================ +# Fixtures for ArgSpace tests +# ============================================================================ + + @pytest.fixture def mock_arguments() -> list[InputArgument]: return [ @@ -74,42 +93,49 @@ def arg_space(mock_arguments: list[InputArgument]) -> ArgSpace: return ArgSpace(all_arguments=mock_arguments) -def test_argspace_initialization(arg_space: ArgSpace, mock_arguments: list[InputArgument]) -> None: +# ============================================================================ +# Tests for ArgSpace initialization and basic operations +# ============================================================================ + + +def test_argspace_initializes_with_arguments(arg_space: ArgSpace, mock_arguments: list[InputArgument]) -> None: assert len(arg_space.all_arguments) == 3 assert arg_space.all_arguments == mock_arguments -def test_argspace_get_by_name(arg_space: ArgSpace, mock_arguments: list[InputArgument]) -> None: +def test_argspace_get_by_name_finds_existing_argument(arg_space: ArgSpace, mock_arguments: list[InputArgument]) -> None: found_arg: InputArgument | None = arg_space.get_by_name("arg1") assert found_arg is not None assert found_arg == mock_arguments[0] -def test_argspace_get_by_name_not_found(arg_space: ArgSpace) -> None: +def test_argspace_get_by_name_returns_none_for_missing_argument(arg_space: ArgSpace) -> None: found_arg: InputArgument | None = arg_space.get_by_name("non_existent_arg") assert found_arg is None -def test_argspace_get_by_type(arg_space: ArgSpace, mock_arguments: list[InputArgument]) -> None: - value_args: list[InputArgument] = arg_space.get_by_type(ValueArgument) +def test_argspace_get_by_type_filters_value_arguments(arg_space: ArgSpace, mock_arguments: list[InputArgument]) -> None: + value_args = arg_space.get_by_type(ValueArgument) assert len(value_args) == 2 assert mock_arguments[0] in value_args assert mock_arguments[2] in value_args - bool_args: list[InputArgument] = arg_space.get_by_type(BooleanArgument) + +def test_argspace_get_by_type_filters_boolean_arguments(arg_space: ArgSpace, mock_arguments: list[InputArgument]) -> None: + bool_args = arg_space.get_by_type(BooleanArgument) assert len(bool_args) == 1 assert mock_arguments[1] in bool_args -def test_argspace_get_by_type_not_found(arg_space: ArgSpace) -> None: +def test_argspace_get_by_type_returns_empty_list_for_unknown_type(arg_space: ArgSpace) -> None: class OtherArgument(BaseArgument): pass - other_args: list[InputArgument] = arg_space.get_by_type(OtherArgument) # pyright: ignore[reportAssignmentType] + other_args = arg_space.get_by_type(OtherArgument) # pyright: ignore[reportAssignmentType] assert other_args == [] -def test_argspace_from_namespace() -> None: +def test_argspace_from_namespace_creates_argspace_from_parsed_namespace() -> None: namespace: Namespace = Namespace(config="config.json", debug=True, verbose=False) processed_args: list[ValueArgument | BooleanArgument] = [ ValueArgument(name="config", prefix="--"), @@ -132,6 +158,11 @@ def test_argspace_from_namespace() -> None: assert debug_arg.founder_class is BooleanArgument +# ============================================================================ +# Fixtures for ArgParser tests +# ============================================================================ + + @pytest.fixture def value_arg() -> ValueArgument: return ValueArgument( @@ -153,7 +184,12 @@ def processed_args(value_arg: ValueArgument, bool_arg: BooleanArgument) -> list[ return [value_arg, bool_arg] -def test_argparser_initialization(processed_args: list[ValueArgument | BooleanArgument]) -> None: +# ============================================================================ +# Tests for ArgParser initialization +# ============================================================================ + + +def test_argparser_initializes_with_all_properties(processed_args: list[ValueArgument | BooleanArgument]) -> None: parser: ArgParser = ArgParser( processed_args=processed_args, name="TestApp", @@ -168,61 +204,93 @@ def test_argparser_initialization(processed_args: list[ValueArgument | BooleanAr assert parser.parsed_argspace.all_arguments == [] +# ============================================================================ +# Tests for ArgParser argument registration (Python version specific) +# ============================================================================ + + @pytest.mark.skipif(sys.version_info < (3, 13), reason="requires python3.13 or higher") -def test_argparser_register_args_py313( +def test_argparser_registers_arguments_with_deprecated_flag_py313( mocker: MockerFixture, value_arg: ValueArgument, bool_arg: BooleanArgument ) -> None: mock_add_argument: MockType = mocker.patch("argparse.ArgumentParser.add_argument") - parser: ArgParser = ArgParser(processed_args=[value_arg, bool_arg]) # pyright: ignore[reportUnusedVariable] - - expected_calls: list[call] = [ - call( - value_arg.string_entity, - action=value_arg.action, - help=value_arg.help, - default=value_arg.default, - choices=value_arg.possible_values, - required=value_arg.is_required, - deprecated=value_arg.is_deprecated, - ), - call( - bool_arg.string_entity, - action=bool_arg.action, - help=bool_arg.help, - deprecated=bool_arg.is_deprecated, - ), - ] - mock_add_argument.assert_has_calls(expected_calls, any_order=True) + _parser: ArgParser = ArgParser(processed_args=[value_arg, bool_arg]) + + # ArgParser may add additional arguments (like help), so check at least 2 + assert mock_add_argument.call_count >= 2 + + # Check that value_arg was registered correctly + value_arg_call = None + bool_arg_call = None + + for call_args in mock_add_argument.call_args_list: + args, kwargs = call_args + if len(args) > 0 and args[0] == value_arg.string_entity: + value_arg_call = (args, kwargs) + elif len(args) > 0 and args[0] == bool_arg.string_entity: + bool_arg_call = (args, kwargs) + + assert value_arg_call is not None, "value_arg was not registered" + _, value_kwargs = value_arg_call + assert value_kwargs['action'] == value_arg.action + assert value_kwargs['help'] == value_arg.help + assert value_kwargs['default'] == value_arg.default + assert value_kwargs['choices'] == value_arg.possible_values + assert value_kwargs['required'] == value_arg.is_required + assert value_kwargs['deprecated'] == value_arg.is_deprecated + + assert bool_arg_call is not None, "bool_arg was not registered" + _, bool_kwargs = bool_arg_call + assert bool_kwargs['action'] == bool_arg.action + assert bool_kwargs['help'] == bool_arg.help + assert bool_kwargs['deprecated'] == bool_arg.is_deprecated @pytest.mark.skipif(sys.version_info > (3, 12), reason="for more latest python version has been other test") -def test_argparser_register_args_py312( +def test_argparser_registers_arguments_without_deprecated_flag_py312( mocker: MockerFixture, value_arg: ValueArgument, bool_arg: BooleanArgument ) -> None: mock_add_argument: MockType = mocker.patch("argparse.ArgumentParser.add_argument") - parser: ArgParser = ArgParser(processed_args=[value_arg, bool_arg]) - - expected_calls: list[call] = [ - call( - value_arg.string_entity, - action=value_arg.action, - help=value_arg.help, - default=value_arg.default, - choices=value_arg.possible_values, - required=value_arg.is_required, - ), - call( - bool_arg.string_entity, - action=bool_arg.action, - help=bool_arg.help, - ), - ] - mock_add_argument.assert_has_calls(expected_calls, any_order=True) + _parser: ArgParser = ArgParser(processed_args=[value_arg, bool_arg]) + # ArgParser may add additional arguments (like help), so check at least 2 + assert mock_add_argument.call_count >= 2 + + # Check that value_arg was registered correctly + value_arg_call = None + bool_arg_call = None + + for call_args in mock_add_argument.call_args_list: + args, kwargs = call_args + if len(args) > 0 and args[0] == value_arg.string_entity: + value_arg_call = (args, kwargs) + elif len(args) > 0 and args[0] == bool_arg.string_entity: + bool_arg_call = (args, kwargs) + + assert value_arg_call is not None, "value_arg was not registered" + _, value_kwargs = value_arg_call + assert value_kwargs['action'] == value_arg.action + assert value_kwargs['help'] == value_arg.help + assert value_kwargs['default'] == value_arg.default + assert value_kwargs['choices'] == value_arg.possible_values + assert value_kwargs['required'] == value_arg.is_required + assert 'deprecated' not in value_kwargs + + assert bool_arg_call is not None, "bool_arg was not registered" + _, bool_kwargs = bool_arg_call + assert bool_kwargs['action'] == bool_arg.action + assert bool_kwargs['help'] == bool_arg.help + assert 'deprecated' not in bool_kwargs + + +# ============================================================================ +# Tests for ArgParser argument parsing +# ============================================================================ -def test_argparser_parse_args_populates_argspace( + +def test_argparser_parse_args_populates_argspace_correctly( mocker: MockerFixture, processed_args: list[ValueArgument | BooleanArgument] ) -> None: mock_namespace: Namespace = Namespace(config='config.json', debug=True) @@ -245,11 +313,3 @@ def test_argparser_parse_args_populates_argspace( assert debug_arg is not None assert debug_arg.value is True assert debug_arg.founder_class is BooleanArgument - -def test_str_input_argument(): - arg = InputArgument('host', value='192.168.0.0', founder_class=ValueArgument) - assert str(arg) == 'InputArgument(host=192.168.0.0)' - -def test_repr_input_argument(): - arg = InputArgument('host', value='192.168.0.0', founder_class=ValueArgument) - assert repr(arg) == "InputArgument" diff --git a/tests/unit_tests/test_autocompleter.py b/tests/unit_tests/test_autocompleter.py index 37ecb70..904c460 100644 --- a/tests/unit_tests/test_autocompleter.py +++ b/tests/unit_tests/test_autocompleter.py @@ -1,9 +1,9 @@ import os +from typing import Any import pytest from pyfakefs.fake_filesystem import FakeFilesystem from pytest_mock import MockerFixture -from pytest_mock.plugin import MockType from argenta.app.autocompleter.entity import ( AutoCompleter, @@ -12,8 +12,17 @@ ) +HISTORY_FILE: str = "test_history.txt" +COMMANDS: list[str] = ["start", "stop", "status"] + + +# ============================================================================ +# Fixtures +# ============================================================================ + + @pytest.fixture -def mock_readline(mocker: MockerFixture) -> MockType: +def mock_readline(mocker: MockerFixture) -> Any: _history: list[str] = [] def add_history(item: str) -> None: @@ -30,31 +39,37 @@ def get_current_history_length() -> int: def clear_history() -> None: _history.clear() - mock: MockType = mocker.MagicMock() - mocker.patch('argenta.app.autocompleter.entity.readline', mock) + mock: Any = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] + mocker.patch('argenta.app.autocompleter.entity.readline', mock) # pyright: ignore[reportUnknownArgumentType] - mock.reset_mock() + mock.reset_mock() # pyright: ignore[reportUnknownMemberType] clear_history() - mock.add_history.side_effect = add_history - mock.get_history_item.side_effect = get_history_item - mock.get_current_history_length.side_effect = get_current_history_length - mock.get_completer_delims.return_value = " " + mock.add_history.side_effect = add_history # pyright: ignore[reportUnknownMemberType] + mock.get_history_item.side_effect = get_history_item # pyright: ignore[reportUnknownMemberType] + mock.get_current_history_length.side_effect = get_current_history_length # pyright: ignore[reportUnknownMemberType] + mock.get_completer_delims.return_value = " " # pyright: ignore[reportUnknownMemberType] - return mock + return mock # pyright: ignore[reportReturnType, reportUnknownVariableType] -HISTORY_FILE: str = "test_history.txt" -COMMANDS: list[str] = ["start", "stop", "status"] +# ============================================================================ +# Tests for AutoCompleter initialization +# ============================================================================ -def test_initialization() -> None: +def test_autocompleter_initializes_with_history_file_and_button() -> None: completer: AutoCompleter = AutoCompleter(history_filename=HISTORY_FILE, autocomplete_button="tab") assert completer.history_filename == HISTORY_FILE assert completer.autocomplete_button == "tab" -def test_initial_setup_if_history_file_does_not_exist(fs: FakeFilesystem, mock_readline: MockType) -> None: +# ============================================================================ +# Tests for initial setup +# ============================================================================ + + +def test_initial_setup_creates_history_when_file_does_not_exist(fs: FakeFilesystem, mock_readline: Any) -> None: if os.path.exists(HISTORY_FILE): os.remove(HISTORY_FILE) @@ -68,8 +83,8 @@ def test_initial_setup_if_history_file_does_not_exist(fs: FakeFilesystem, mock_r mock_readline.parse_and_bind.assert_called_with("tab: complete") -def test_initial_setup_if_history_file_exists(fs: FakeFilesystem, mock_readline: MockType) -> None: - fs.create_file(HISTORY_FILE, contents="previous_command\n") +def test_initial_setup_reads_existing_history_file(fs: FakeFilesystem, mock_readline: Any) -> None: + fs.create_file(HISTORY_FILE, contents="previous_command\n") # pyright: ignore[reportUnknownMemberType] completer: AutoCompleter = AutoCompleter(history_filename=HISTORY_FILE) completer.initial_setup(COMMANDS) @@ -80,7 +95,7 @@ def test_initial_setup_if_history_file_exists(fs: FakeFilesystem, mock_readline: mock_readline.parse_and_bind.assert_called_once() -def test_initial_setup_with_no_history_filename(mock_readline: MockType) -> None: +def test_initial_setup_works_without_history_filename(mock_readline: Any) -> None: completer: AutoCompleter = AutoCompleter(history_filename=None) completer.initial_setup(COMMANDS) @@ -88,7 +103,12 @@ def test_initial_setup_with_no_history_filename(mock_readline: MockType) -> None assert mock_readline.add_history.call_count == len(COMMANDS) -def test_exit_setup_writes_and_filters_history(fs: FakeFilesystem, mock_readline: MockType) -> None: +# ============================================================================ +# Tests for exit setup and history filtering +# ============================================================================ + + +def test_exit_setup_writes_and_filters_duplicate_commands(fs: FakeFilesystem, mock_readline: Any) -> None: mock_readline.add_history.side_effect = None mock_readline.add_history("start server") mock_readline.add_history("stop client") @@ -96,7 +116,7 @@ def test_exit_setup_writes_and_filters_history(fs: FakeFilesystem, mock_readline mock_readline.add_history("start server") raw_history_content: str = "\n".join(["start server", "stop client", "invalid command", "start server"]) - fs.create_file(HISTORY_FILE, contents=raw_history_content) + fs.create_file(HISTORY_FILE, contents=raw_history_content) # pyright: ignore[reportUnknownMemberType] completer: AutoCompleter = AutoCompleter(history_filename=HISTORY_FILE) completer.exit_setup(all_commands=["start", "stop"], ignore_command_register=False) @@ -109,13 +129,18 @@ def test_exit_setup_writes_and_filters_history(fs: FakeFilesystem, mock_readline assert lines == ["start server", "stop client"] -def test_exit_setup_with_no_history_filename(mock_readline: MockType) -> None: +def test_exit_setup_skips_writing_when_no_history_filename(mock_readline: Any) -> None: completer: AutoCompleter = AutoCompleter(history_filename=None) completer.exit_setup(all_commands=COMMANDS, ignore_command_register=False) mock_readline.write_history_file.assert_not_called() -def test_complete_with_no_matches(mock_readline: MockType) -> None: +# ============================================================================ +# Tests for autocomplete functionality +# ============================================================================ + + +def test_complete_returns_none_when_no_matches_found(mock_readline: Any) -> None: cmd: str for cmd in ["start", "stop"]: mock_readline.add_history(cmd) @@ -125,7 +150,7 @@ def test_complete_with_no_matches(mock_readline: MockType) -> None: assert completer._complete("run", 1) is None -def test_complete_with_one_match(mock_readline: MockType) -> None: +def test_complete_returns_single_match(mock_readline: Any) -> None: mock_readline.add_history("start server") mock_readline.add_history("stop server") @@ -134,7 +159,7 @@ def test_complete_with_one_match(mock_readline: MockType) -> None: assert completer._complete("start", 1) is None -def test_complete_with_multiple_matches(mock_readline: MockType) -> None: +def test_complete_inserts_common_prefix_for_multiple_matches(mock_readline: Any) -> None: mock_readline.add_history("status client") mock_readline.add_history("status server") mock_readline.add_history("stop") @@ -152,21 +177,32 @@ def test_complete_with_multiple_matches(mock_readline: MockType) -> None: mock_readline.insert_text.assert_not_called() -def test_is_command_exist() -> None: +# ============================================================================ +# Tests for helper functions +# ============================================================================ + + +def test_is_command_exist_checks_case_sensitive_when_enabled() -> None: existing: list[str] = ["start", "stop", "status"] assert _is_command_exist("start", existing, ignore_command_register=False) is True assert _is_command_exist("START", existing, ignore_command_register=False) is False assert _is_command_exist("unknown", existing, ignore_command_register=False) is False + +def test_is_command_exist_checks_case_insensitive_when_enabled() -> None: + existing: list[str] = ["start", "stop", "status"] + assert _is_command_exist("start", existing, ignore_command_register=True) is True assert _is_command_exist("START", existing, ignore_command_register=True) is True assert _is_command_exist("unknown", existing, ignore_command_register=True) is False -def test_get_history_items(mock_readline: MockType) -> None: +def test_get_history_items_returns_empty_list_initially(mock_readline: Any) -> None: assert _get_history_items() == [] + +def test_get_history_items_returns_all_added_items(mock_readline: Any) -> None: mock_readline.add_history("first item") mock_readline.add_history("second item") diff --git a/tests/unit_tests/test_command.py b/tests/unit_tests/test_command.py index 4a97dcc..db8dada 100644 --- a/tests/unit_tests/test_command.py +++ b/tests/unit_tests/test_command.py @@ -2,86 +2,113 @@ import pytest -from argenta.command.exceptions import (EmptyInputCommandException, - RepeatedInputFlagsException, - UnprocessedInputFlagException) +from argenta.command.exceptions import ( + EmptyInputCommandException, + RepeatedInputFlagsException, + UnprocessedInputFlagException, +) from argenta.command.flag import Flag, InputFlag from argenta.command.flag.flags import Flags from argenta.command.flag.models import PossibleValues, ValidationStatus from argenta.command.models import Command, InputCommand -def test_parse_correct_raw_command(): +# ============================================================================ +# Tests for InputCommand parsing - successful cases +# ============================================================================ + + +def test_parse_extracts_trigger_from_command_with_flags() -> None: assert InputCommand.parse('ssh --host 192.168.0.3').trigger == 'ssh' -def test_parse_raw_command_without_flag_name_with_value(): - with pytest.raises(UnprocessedInputFlagException): - InputCommand.parse('ssh 192.168.0.3') +def test_parse_returns_input_command_instance() -> None: + cmd = InputCommand.parse('ssh --host 192.168.0.3') + assert isinstance(cmd, InputCommand) -def test_parse_raw_command_with_repeated_flag_name(): - with pytest.raises(RepeatedInputFlagsException): - InputCommand.parse('ssh --host 192.168.0.3 --host 172.198.0.43') - - -def test_parse_raw_command_with_triple_prefix(): +def test_parse_handles_triple_prefix_flags() -> None: assert InputCommand.parse( 'ssh ---host 192.168.0.0' ).input_flags.get_flag_by_name('host') == \ InputFlag('host', input_value='192.168.0.0', prefix='---') - - -def test_parse_raw_command_with_unprocessed_entity(): + + +# ============================================================================ +# Tests for InputCommand parsing - error cases +# ============================================================================ + + +def test_parse_raises_error_for_value_without_flag_name() -> None: + with pytest.raises(UnprocessedInputFlagException): + InputCommand.parse('ssh 192.168.0.3') + + +def test_parse_raises_error_for_repeated_flag_names() -> None: + with pytest.raises(RepeatedInputFlagsException): + InputCommand.parse('ssh --host 192.168.0.3 --host 172.198.0.43') + + +def test_parse_raises_error_for_unprocessed_entity_after_flags() -> None: with pytest.raises(UnprocessedInputFlagException): InputCommand.parse('ssh --host 192.168.0.3 9977') -def test_parse_empty_raw_command(): +def test_parse_raises_error_for_empty_command() -> None: with pytest.raises(EmptyInputCommandException): InputCommand.parse('') -def test_validate_invalid_input_flag1(): - command = Command('some', flags=Flag('test')) - assert command.validate_input_flag(InputFlag('test', input_value='', status=None)) == ValidationStatus.INVALID +# ============================================================================ +# Tests for flag validation - valid flags +# ============================================================================ -def test_validate_valid_input_flag2(): +def test_validate_input_flag_returns_valid_for_registered_flag() -> None: command = Command('some', flags=Flags([Flag('test'), Flag('more')])) assert command.validate_input_flag(InputFlag('more', input_value='random-value', status=None)) == ValidationStatus.VALID -def test_validate_undefined_input_flag1(): - command = Command('some', flags=Flag('test')) - assert command.validate_input_flag(InputFlag('more', input_value='', status=None)) == ValidationStatus.UNDEFINED - - -def test_validate_undefined_input_flag2(): - command = Command('some', flags=Flags([Flag('test'), Flag('more')])) - assert command.validate_input_flag(InputFlag('case', input_value='', status=None)) == ValidationStatus.UNDEFINED +# ============================================================================ +# Tests for flag validation - invalid flags +# ============================================================================ -def test_validate_undefined_input_flag3(): - command = Command('some') - assert command.validate_input_flag(InputFlag('case', input_value='', status=None)) == ValidationStatus.UNDEFINED +def test_validate_input_flag_returns_invalid_for_flag_with_empty_value() -> None: + command = Command('some', flags=Flag('test')) + assert command.validate_input_flag(InputFlag('test', input_value='', status=None)) == ValidationStatus.INVALID -def test_invalid_input_flag1(): +def test_validate_input_flag_returns_invalid_when_value_provided_for_neither_flag() -> None: command = Command('some', flags=Flag('test', possible_values=PossibleValues.NEITHER)) assert command.validate_input_flag(InputFlag('test', input_value='example', status=None)) == ValidationStatus.INVALID -def test_invalid_input_flag2(): +def test_validate_input_flag_returns_invalid_when_value_not_in_allowed_list() -> None: command = Command('some', flags=Flag('test', possible_values=['some', 'case'])) assert command.validate_input_flag(InputFlag('test', input_value='slay', status=None)) == ValidationStatus.INVALID -def test_invalid_input_flag3(): - command = Command('some', flags=Flag('test', possible_values=re.compile(r'^ex\d{, 2}op$'))) +def test_validate_input_flag_returns_invalid_when_value_does_not_match_regex() -> None: + command = Command('some', flags=Flag('test', possible_values=re.compile(r'^ex\d{1,2}op$'))) assert command.validate_input_flag(InputFlag('test', input_value='example', status=None)) == ValidationStatus.INVALID -def test_isinstance_parse_correct_raw_command(): - cmd = InputCommand.parse('ssh --host 192.168.0.3') - assert isinstance(cmd, InputCommand) +# ============================================================================ +# Tests for flag validation - undefined flags +# ============================================================================ + + +def test_validate_input_flag_returns_undefined_for_unregistered_flag_name() -> None: + command = Command('some', flags=Flag('test')) + assert command.validate_input_flag(InputFlag('more', input_value='', status=None)) == ValidationStatus.UNDEFINED + + +def test_validate_input_flag_returns_undefined_for_unregistered_flag_in_multiple_flags() -> None: + command = Command('some', flags=Flags([Flag('test'), Flag('more')])) + assert command.validate_input_flag(InputFlag('case', input_value='', status=None)) == ValidationStatus.UNDEFINED + + +def test_validate_input_flag_returns_undefined_when_command_has_no_flags() -> None: + command = Command('some') + assert command.validate_input_flag(InputFlag('case', input_value='', status=None)) == ValidationStatus.UNDEFINED diff --git a/tests/unit_tests/test_di.py b/tests/unit_tests/test_di.py index 54f6ac2..271401b 100644 --- a/tests/unit_tests/test_di.py +++ b/tests/unit_tests/test_di.py @@ -1,81 +1,120 @@ from typing import Generator + +import pytest +from dishka import Container, make_container + from argenta import App, DataBridge, Router +from argenta.di.integration import ( + FromDishka, + _auto_inject_handlers, + _get_container_from_response, + setup_dishka, +) from argenta.di.providers import SystemProvider from argenta.orchestrator.argparser import ArgParser, ArgSpace from argenta.response import ResponseStatus -from dishka import Container, make_container -import pytest from argenta.response.entity import Response -from argenta.di.integration import FromDishka, _get_container_from_response, setup_dishka, _auto_inject_handlers + + +# ============================================================================ +# Fixtures +# ============================================================================ @pytest.fixture def argparser() -> ArgParser: return ArgParser(processed_args=[]) + @pytest.fixture -def container(argparser: ArgParser) -> Generator[Container]: +def container(argparser: ArgParser) -> Generator[Container, None, None]: container = make_container(SystemProvider(), context={ArgParser: argparser}) yield container container.close() -def test_get_container_from_response(container: Container): +# ============================================================================ +# Tests for container retrieval from response +# ============================================================================ + + +def test_get_container_from_response_extracts_container_from_first_response_arg(container: Container) -> None: Response.patch_by_container(container) response = Response(ResponseStatus.ALL_FLAGS_VALID) assert _get_container_from_response((response,), {}) == container - -def test_get_container_from_response4(container: Container): + + +def test_get_container_from_response_extracts_container_from_second_response_arg(container: Container) -> None: Response.patch_by_container(container) response = Response(ResponseStatus.ALL_FLAGS_VALID) assert _get_container_from_response((object(), response,), {}) == container - -def test_get_container_from_response2(container: Container): + + +def test_get_container_from_response_raises_error_when_container_not_patched() -> None: delattr(Response, '__dishka_container__') response = Response(ResponseStatus.ALL_FLAGS_VALID) with pytest.raises(RuntimeError): _get_container_from_response((response,), {}) - -def test_get_container_from_response3(container: Container): + + +def test_get_container_from_response_raises_error_when_no_response_in_args(container: Container) -> None: Response.patch_by_container(container) with pytest.raises(RuntimeError): - assert _get_container_from_response((), {}) == container - -def test_setup_dishka(container: Container): + _get_container_from_response((), {}) + + +# ============================================================================ +# Tests for dishka setup +# ============================================================================ + + +def test_setup_dishka_with_auto_inject_enabled(container: Container) -> None: app = App() router = Router() @router.command('command') - def handler(res: Response, data_bridge: FromDishka[DataBridge]): + def handler(_res: Response, data_bridge: FromDishka[DataBridge]) -> None: print(data_bridge) app.include_router(router) assert setup_dishka(app, container, auto_inject=True) is None - -def test_setup_dishka2(container: Container): + + +def test_setup_dishka_with_auto_inject_disabled(container: Container) -> None: app = App() assert setup_dishka(app, container, auto_inject=False) is None - -def test_auto_inject_handlers(container: Container): + + +# ============================================================================ +# Tests for auto injection +# ============================================================================ + + +def test_auto_inject_handlers_injects_dependencies_into_handlers(container: Container) -> None: Response.patch_by_container(container) app = App() router = Router() @router.command('command') - def handler(res: Response, data_bridge: FromDishka[DataBridge]): + def handler(_res: Response, data_bridge: FromDishka[DataBridge]) -> None: print(data_bridge) app.include_router(router) _auto_inject_handlers(app) - _auto_inject_handlers(app) # check idempotency - -def test_get_from_container(container: Container): + _auto_inject_handlers(app) # check idempotency + + +# ============================================================================ +# Tests for container dependency resolution +# ============================================================================F + + +def test_container_resolves_argspace_dependency(container: Container) -> None: assert isinstance(container.get(ArgSpace), ArgSpace) - -def test_get_from_container2(container: Container): + + +def test_container_resolves_databridge_dependency(container: Container) -> None: assert isinstance(container.get(DataBridge), DataBridge) - - \ No newline at end of file diff --git a/tests/unit_tests/test_dividing_line.py b/tests/unit_tests/test_dividing_line.py index 151fd2a..4324826 100644 --- a/tests/unit_tests/test_dividing_line.py +++ b/tests/unit_tests/test_dividing_line.py @@ -1,36 +1,61 @@ from argenta.app.dividing_line import DynamicDividingLine, StaticDividingLine -def test_get_static_dividing_line_full_line(): +# ============================================================================ +# Tests for StaticDividingLine - full line generation +# ============================================================================ + + +def test_static_dividing_line_generates_default_length_with_override() -> None: line = StaticDividingLine('-') assert line.get_full_static_line(is_override=True).count('-') == 25 - -def test_get_static_dividing_line2_full_line(): + + +def test_static_dividing_line_generates_custom_length_with_formatting() -> None: line = StaticDividingLine('-', length=5) assert line.get_full_static_line(is_override=False) == '\n[dim]-----[/dim]\n' - -def test_get_dividing_line_unit_part(): + + +# ============================================================================ +# Tests for StaticDividingLine - unit part extraction +# ============================================================================ + + +def test_static_dividing_line_returns_space_for_empty_unit() -> None: line = StaticDividingLine('') assert line.get_unit_part() == ' ' -def test_get_dividing_line2_unit_part(): + +def test_static_dividing_line_returns_first_character_as_unit() -> None: line = StaticDividingLine('+-0987654321!@#$%^&*()_') assert line.get_unit_part() == '+' -def test_get_dynamic_dividing_line_full_line(): + +# ============================================================================ +# Tests for DynamicDividingLine - full line generation +# ============================================================================ + + +def test_dynamic_dividing_line_generates_line_with_specified_length_and_override() -> None: line = DynamicDividingLine() assert line.get_full_dynamic_line(length=20, is_override=True).count('-') == 20 - -def test_get_dynamic_dividing_line2_full_line(): + + +def test_dynamic_dividing_line_generates_line_with_specified_length_and_formatting() -> None: line = DynamicDividingLine() assert line.get_full_dynamic_line(length=5, is_override=False) == '\n[dim]-----[/dim]\n' - -def test_get_dynamic_dividing_line_unit_part(): + + +# ============================================================================ +# Tests for DynamicDividingLine - unit part extraction +# ============================================================================ + + +def test_dynamic_dividing_line_returns_space_for_empty_unit() -> None: line = DynamicDividingLine('') assert line.get_unit_part() == ' ' -def test_get_dynamic_dividing_line2_unit_part(): + +def test_dynamic_dividing_line_returns_first_character_as_unit() -> None: line = DynamicDividingLine('45n352834528&^%@&*T$G') assert line.get_unit_part() == '4' - - diff --git a/tests/unit_tests/test_flag.py b/tests/unit_tests/test_flag.py index deb092e..3d4e211 100644 --- a/tests/unit_tests/test_flag.py +++ b/tests/unit_tests/test_flag.py @@ -1,100 +1,226 @@ import re +import pytest + from argenta.command.flag import Flag, InputFlag, PossibleValues from argenta.command.flag.flags import Flags, InputFlags -import pytest -def test_get_string_entity(): +# ============================================================================ +# Tests for Flag - basic properties +# ============================================================================ + + +def test_flag_string_entity_with_default_prefix() -> None: assert Flag(name='test').string_entity == '--test' -def test_get_string_entity2(): +def test_flag_string_entity_with_custom_prefix() -> None: assert Flag(name='test', prefix='---').string_entity == '---test' -def test_get_flag_name(): +def test_flag_name_property() -> None: assert Flag(name='test').name == 'test' -def test_get_flag_prefix(): +def test_flag_prefix_property_default() -> None: assert Flag(name='test').prefix == '--' -def test_get_flag_prefix2(): +def test_flag_prefix_property_custom() -> None: assert Flag(name='test', prefix='--').prefix == '--' -def test_get_flag_value_without_set(): - assert InputFlag(name='test', input_value='', status=None).input_value == '' +# ============================================================================ +# Tests for Flag - string representations +# ============================================================================ -def test_get_flag_value_with_set(): - flag = InputFlag(name='test', input_value='example', status=None) - assert flag.input_value == 'example' +def test_flag_str_representation() -> None: + flag = Flag('two') + assert str(flag) == '--two' -def test_validate_incorrect_flag_value_with_list_of_possible_flag_values(): - flag = Flag(name='test', possible_values=['1', '2', '3']) - assert flag.validate_input_flag_value('bad value') is False +def test_flag_repr_representation() -> None: + flag = Flag('two') + assert repr(flag) == 'Flag' + + +def test_flag_equality_with_non_flag_raises_error() -> None: + flag = Flag('two') + not_flag = object() + with pytest.raises(NotImplementedError): + flag == not_flag # pyright: ignore[reportUnusedExpression] + + +# ============================================================================ +# Tests for Flag - value validation with list of possible values +# ============================================================================ -def test_validate_correct_flag_value_with_list_of_possible_flag_values(): +def test_flag_validates_value_in_allowed_list() -> None: flag = Flag(name='test', possible_values=['1', '2', '3']) assert flag.validate_input_flag_value('1') is True -def test_validate_incorrect_flag_value_with_pattern_of_possible_flag_values(): - flag = Flag(name='test', possible_values=re.compile(r'192.168.\d+.\d+')) - assert flag.validate_input_flag_value('152.123.9.8') is False +def test_flag_rejects_value_not_in_allowed_list() -> None: + flag = Flag(name='test', possible_values=['1', '2', '3']) + assert flag.validate_input_flag_value('bad value') is False + + +# ============================================================================ +# Tests for Flag - value validation with regex pattern +# ============================================================================ -def test_validate_correct_flag_value_with_pattern_of_possible_flag_values(): +def test_flag_validates_value_matching_regex_pattern() -> None: flag = Flag(name='test', possible_values=re.compile(r'192.168.\d+.\d+')) assert flag.validate_input_flag_value('192.168.9.8') is True -def test_validate_correct_empty_flag_value_without_possible_flag_values(): - flag = Flag(name='test', possible_values=PossibleValues.NEITHER) - assert flag.validate_input_flag_value('') is True +def test_flag_rejects_value_not_matching_regex_pattern() -> None: + flag = Flag(name='test', possible_values=re.compile(r'192.168.\d+.\d+')) + assert flag.validate_input_flag_value('152.123.9.8') is False + +# ============================================================================ +# Tests for Flag - value validation with NEITHER and ALL +# ============================================================================ -def test_validate_correct_empty_flag_value_with_possible_flag_values(): + +def test_flag_validates_empty_value_when_neither_allowed() -> None: flag = Flag(name='test', possible_values=PossibleValues.NEITHER) assert flag.validate_input_flag_value('') is True -def test_validate_incorrect_random_flag_value_without_possible_flag_values(): +def test_flag_rejects_non_empty_value_when_neither_allowed() -> None: flag = Flag(name='test', possible_values=PossibleValues.NEITHER) assert flag.validate_input_flag_value('random value') is False -def test_validate_correct_random_flag_value_with_possible_flag_values(): +def test_flag_validates_any_value_when_all_allowed() -> None: flag = Flag(name='test', possible_values=PossibleValues.ALL) assert flag.validate_input_flag_value('random value') is True -def test_get_input_flag1(): +# ============================================================================ +# Tests for InputFlag - basic properties +# ============================================================================ + + +def test_input_flag_stores_empty_value() -> None: + assert InputFlag(name='test', input_value='', status=None).input_value == '' + + +def test_input_flag_stores_provided_value() -> None: + flag = InputFlag(name='test', input_value='example', status=None) + assert flag.input_value == 'example' + + +# ============================================================================ +# Tests for InputFlag - string representations +# ============================================================================ + + +def test_input_flag_str_representation() -> None: + flag = InputFlag('two', input_value='value') + assert str(flag) == '--two value' + + +def test_input_flag_repr_representation() -> None: + flag = InputFlag('two', input_value='some_value') + assert repr(flag) == 'InputFlag' + + +def test_input_flag_equality_with_non_flag_raises_error() -> None: + flag = InputFlag('two', input_value='') + not_flag = object() + with pytest.raises(NotImplementedError): + flag == not_flag # pyright: ignore[reportUnusedExpression] + + +# ============================================================================ +# Tests for InputFlags collection - retrieval +# ============================================================================ + + +def test_input_flags_get_by_name_finds_single_flag() -> None: flag = InputFlag(name='test', input_value='', status=None) input_flags = InputFlags([flag]) assert input_flags.get_flag_by_name('test') == flag -def test_get_input_flag2(): +def test_input_flags_get_by_name_finds_flag_in_multiple() -> None: flag = InputFlag(name='test', input_value='', status=None) flag2 = InputFlag(name='some', input_value='', status=None) input_flags = InputFlags([flag, flag2]) assert input_flags.get_flag_by_name('some') == flag2 -def test_get_undefined_input_flag(): +def test_input_flags_get_by_name_returns_none_for_missing_flag() -> None: flag = InputFlag(name='test', input_value='', status=None) flag2 = InputFlag(name='some', input_value='', status=None) input_flags = InputFlags([flag, flag2]) assert input_flags.get_flag_by_name('case') is None -def test_get_flags(): +# ============================================================================ +# Tests for InputFlags collection - equality and containment +# ============================================================================ + + +def test_input_flags_not_equal_when_different_length() -> None: + flags = InputFlags([InputFlag('some', input_value='')]) + flags2 = InputFlags([ + InputFlag('some', input_value=''), + InputFlag('some2', input_value='') + ]) + assert flags != flags2 + + +def test_input_flags_not_equal_to_non_input_flags() -> None: + flags = InputFlags([InputFlag('some', input_value='')]) + not_flags = object() + assert flags != not_flags + + +def test_input_flags_contains_existing_flag() -> None: + flag = InputFlag('some', input_value='') + flags = InputFlags([flag]) + assert flag in flags + + +def test_input_flags_does_not_contain_missing_flag() -> None: + flags = InputFlags([InputFlag('some', input_value='')]) + flag = InputFlag('nonexists', input_value='') + assert flag not in flags + + +def test_input_flags_contains_raises_error_for_non_flag() -> None: + flags = InputFlags([InputFlag('some', input_value='')]) + not_flag = object + with pytest.raises(TypeError): + not_flag in flags # pyright: ignore[reportUnusedExpression] + + +# ============================================================================ +# Tests for Flags collection - adding flags +# ============================================================================ + + +def test_flags_add_single_flag() -> None: + flags = Flags() + flags.add_flag(Flag('test')) + assert len(flags.flags) == 1 + + +def test_flags_add_multiple_flags() -> None: + flags = Flags() + flags.add_flags([Flag('test'), Flag('test2')]) + assert len(flags.flags) == 2 + + +def test_flags_stores_added_flags() -> None: flags = Flags() list_of_flags = [ Flag('test1'), @@ -105,122 +231,84 @@ def test_get_flags(): assert flags.flags == list_of_flags -def test_add_flag(): - flags = Flags() - flags.add_flag(Flag('test')) - assert len(flags.flags) == 1 +# ============================================================================ +# Tests for Flags collection - retrieval +# ============================================================================ -def test_add_flags(): - flags = Flags() - flags.add_flags([Flag('test'), Flag('test2')]) - assert len(flags.flags) == 2 - -def test_eq_flags(): +def test_flags_get_by_name_finds_flag() -> None: + flags = Flags([Flag('some')]) + assert flags.get_flag_by_name('some') == Flag('some') + + +# ============================================================================ +# Tests for Flags collection - equality and containment +# ============================================================================ + + +def test_flags_equal_when_same_flags() -> None: flags = Flags([Flag('some')]) flags2 = Flags([Flag('some')]) assert flags == flags2 - -def test_contains_flags(): - flags = Flags([Flag('some')]) - flag = Flag('some') - assert flag in flags - -def test_eq_flags2(): + + +def test_flags_not_equal_when_different_flags() -> None: flags = Flags([Flag('some')]) flags2 = Flags([Flag('other')]) assert flags != flags2 - -def test_eq_flags3(): + + +def test_flags_not_equal_when_different_length() -> None: flags = Flags([Flag('some')]) flags2 = Flags([Flag('some'), Flag('other')]) assert flags != flags2 - -def test_eq_flags4(): + + +def test_flags_not_equal_to_non_flags() -> None: flags = Flags([Flag('some')]) not_flags = object() assert flags != not_flags - -def test_contains_flags2(): + + +def test_flags_contains_existing_flag() -> None: + flags = Flags([Flag('some')]) + flag = Flag('some') + assert flag in flags + + +def test_flags_does_not_contain_missing_flag() -> None: flags = Flags([Flag('some')]) flag = Flag('nonexists') assert flag not in flags - -def test_contains_flags3(): - flags = Flags([Flag('some')]) - not_flag = object - with pytest.raises(TypeError): - not_flag in flags # pyright: ignore[reportUnusedExpression] - -def test_get_flag_by_name(): + + +def test_flags_contains_raises_error_for_non_flag() -> None: flags = Flags([Flag('some')]) - assert flags.get_flag_by_name('some') == Flag('some') - -def test_eq_input_flags3(): - flags = InputFlags([InputFlag('some', input_value='')]) - flags2 = InputFlags([ - InputFlag('some', input_value=''), - InputFlag('some2', input_value='') - ]) - assert flags != flags2 - -def test_eq_input_flags4(): - flags = InputFlags([InputFlag('some', input_value='')]) - not_flags = object() - assert flags != not_flags - -def test_contains_input_flags2(): - flags = InputFlags([InputFlag('some', input_value='')]) - flag = InputFlag('nonexists', input_value='') - assert flag not in flags - -def test_contains_input_flags3(): - flags = InputFlags([InputFlag('some', input_value='')]) not_flag = object with pytest.raises(TypeError): not_flag in flags # pyright: ignore[reportUnusedExpression] -def test_len_flags(): + +# ============================================================================ +# Tests for Flags collection - special methods +# ============================================================================ + + +def test_flags_len_returns_count() -> None: flags = Flags([Flag('one'), Flag('two')]) assert len(flags) == 2 - -def test_bool_flags(): + + +def test_flags_bool_returns_true_when_not_empty() -> None: flags = Flags([Flag('one'), Flag('two')]) assert bool(flags) - -def test_bool_flags2(): + + +def test_flags_bool_returns_false_when_empty() -> None: flags = Flags([]) assert not bool(flags) - -def test_getitem_flags(): + + +def test_flags_getitem_returns_flag_at_index() -> None: flags = Flags([Flag('one'), Flag('two')]) assert flags[1] == Flag('two') - -def test_str_flag(): - flag = Flag('two') - assert str(flag) == '--two' - -def test_repr_flag(): - flag = Flag('two') - assert repr(flag) == 'Flag' - -def test_eq_flag(): - flag = Flag('two') - not_flag = object() - with pytest.raises(NotImplementedError): - flag == not_flag # pyright: ignore[reportUnusedExpression] - -def test_str_input_flag(): - flag = InputFlag('two', input_value='value') - assert str(flag) == '--two value' - -def test_repr_input_flag(): - flag = InputFlag('two', input_value='some_value') - assert repr(flag) == 'InputFlag' - -def test_eq_input_flag(): - flag = InputFlag('two', input_value='') - not_flag = object() - with pytest.raises(NotImplementedError): - flag == not_flag # pyright: ignore[reportUnusedExpression] - \ No newline at end of file diff --git a/tests/unit_tests/test_response.py b/tests/unit_tests/test_response.py index 2110b2d..fdbdf3d 100644 --- a/tests/unit_tests/test_response.py +++ b/tests/unit_tests/test_response.py @@ -2,27 +2,37 @@ import pytest -from argenta.data_bridge import DataBridge -from argenta.command.flag.models import InputFlag from argenta.command.flag.flags.models import InputFlags +from argenta.command.flag.models import InputFlag +from argenta.data_bridge import DataBridge from argenta.response.entity import EMPTY_INPUT_FLAGS, Response from argenta.response.status import ResponseStatus +# ============================================================================ +# Fixtures +# ============================================================================ + + @pytest.fixture -def data_bridge(): +def data_bridge() -> DataBridge: """Create a new DataBridge instance for each test""" return DataBridge() -def test_update_data_basic(data_bridge: DataBridge): +# ============================================================================ +# Tests for DataBridge - basic data operations +# ============================================================================ + + +def test_databridge_update_stores_basic_data(data_bridge: DataBridge) -> None: """Test basic data update functionality""" test_data = {"key1": "value1", "key2": "value2"} data_bridge.update(test_data) assert data_bridge.get_all() == test_data -def test_update_data_with_datetime(data_bridge: DataBridge): +def test_databridge_update_stores_datetime_objects(data_bridge: DataBridge) -> None: """Test updating data with datetime objects""" test_datetime = datetime(2024, 1, 15, 10, 30, 45) test_data = {"created_at": test_datetime, "name": "test"} @@ -33,7 +43,7 @@ def test_update_data_with_datetime(data_bridge: DataBridge): assert result["name"] == "test" -def test_update_data_multiple_calls(data_bridge: DataBridge): +def test_databridge_multiple_updates_merge_data(data_bridge: DataBridge) -> None: """Test multiple update calls merge data""" first_data = {"key1": "value1"} second_data = {"key2": "value2"} @@ -42,12 +52,37 @@ def test_update_data_multiple_calls(data_bridge: DataBridge): assert len(data_bridge.get_all()) == 2 -def test_get_data_empty(data_bridge: DataBridge): +# ============================================================================ +# Tests for DataBridge - data retrieval +# ============================================================================ + + +def test_databridge_get_all_returns_empty_dict_initially(data_bridge: DataBridge) -> None: """Test get_all returns empty dict when no data""" assert data_bridge.get_all() == {} -def test_clear_data(data_bridge: DataBridge): +def test_databridge_get_by_key_retrieves_correct_values(data_bridge: DataBridge) -> None: + """Test get_by_key retrieves correct value""" + test_data = {"key1": "value1", "key2": date(2024, 1, 1)} + data_bridge.update(test_data) + assert data_bridge.get_by_key("key1") == "value1" + assert data_bridge.get_by_key("key2") == date(2024, 1, 1) + + +def test_databridge_get_by_key_returns_none_for_missing_key(data_bridge: DataBridge) -> None: + """Test get_by_key returns None for nonexistent key""" + test_data = {"key1": "value1"} + data_bridge.update(test_data) + assert data_bridge.get_by_key("nonexistent") is None + + +# ============================================================================ +# Tests for DataBridge - data deletion +# ============================================================================ + + +def test_databridge_clear_all_removes_all_data(data_bridge: DataBridge) -> None: """Test clear_all removes all data""" data_bridge.update({"key": "value"}) assert data_bridge.get_all() != {} @@ -55,7 +90,7 @@ def test_clear_data(data_bridge: DataBridge): assert data_bridge.get_all() == {} -def test_delete_from_data(data_bridge: DataBridge): +def test_databridge_delete_by_key_removes_specific_key(data_bridge: DataBridge) -> None: """Test delete_by_key removes specific key""" test_data = {"key1": "value1", "key2": "value2"} data_bridge.update(test_data) @@ -65,29 +100,25 @@ def test_delete_from_data(data_bridge: DataBridge): assert "key2" in result -def test_delete_from_data_nonexistent_key(data_bridge: DataBridge): +def test_databridge_delete_by_key_raises_error_for_missing_key(data_bridge: DataBridge) -> None: """Test delete_by_key with nonexistent key raises KeyError""" with pytest.raises(KeyError): data_bridge.delete_by_key("nonexistent_key") -def test_get_by_key(data_bridge: DataBridge): - """Test get_by_key retrieves correct value""" - test_data = {"key1": "value1", "key2": date(2024, 1, 1)} - data_bridge.update(test_data) - assert data_bridge.get_by_key("key1") == "value1" - assert data_bridge.get_by_key("key2") == date(2024, 1, 1) - assert data_bridge.get_by_key("nonexistent") is None +# ============================================================================ +# Tests for Response - initialization +# ============================================================================ -def test_response_initialization_basic(): +def test_response_initializes_with_status_and_empty_flags() -> None: """Test basic Response initialization""" response = Response(ResponseStatus.ALL_FLAGS_VALID) assert response.status == ResponseStatus.ALL_FLAGS_VALID assert response.input_flags == EMPTY_INPUT_FLAGS -def test_response_initialization_with_flags(): +def test_response_initializes_with_status_and_input_flags() -> None: """Test Response initialization with input flags""" input_flags = InputFlags([InputFlag('test', input_value='value', status=None)]) response = Response(ResponseStatus.INVALID_VALUE_FLAGS, input_flags) @@ -95,7 +126,12 @@ def test_response_initialization_with_flags(): assert response.input_flags == input_flags -def test_response_status_types(): +# ============================================================================ +# Tests for Response - status types +# ============================================================================ + + +def test_response_accepts_all_status_types() -> None: """Test Response with different status types""" statuses = [ ResponseStatus.ALL_FLAGS_VALID, diff --git a/tests/unit_tests/test_router.py b/tests/unit_tests/test_router.py index 125cb4c..1ec082f 100644 --- a/tests/unit_tests/test_router.py +++ b/tests/unit_tests/test_router.py @@ -1,4 +1,5 @@ import re + import pytest from pytest import CaptureFixture @@ -9,106 +10,181 @@ from argenta.response.entity import Response from argenta.router import Router from argenta.router.entity import _structuring_input_flags, _validate_func_args # pyright: ignore[reportPrivateUsage] -from argenta.router.exceptions import (RepeatedFlagNameException, - RequiredArgumentNotPassedException, - TriggerContainSpacesException) +from argenta.router.exceptions import ( + RepeatedFlagNameException, + RequiredArgumentNotPassedException, + TriggerContainSpacesException, +) + + +# ============================================================================ +# Tests for command validation +# ============================================================================ -def test_register_command_with_spaces_in_trigger(): +def test_validate_command_raises_error_for_trigger_with_spaces() -> None: router = Router() with pytest.raises(TriggerContainSpacesException): router._validate_command(Command(trigger='command with spaces')) -def test_register_command_with_repeated_flags(): + +def test_validate_command_raises_error_for_repeated_flag_names() -> None: router = Router() with pytest.raises(RepeatedFlagNameException): router._validate_command(Command(trigger='command', flags=Flags([Flag('test'), Flag('test')]))) -def test_structuring_input_flags1(): + +# ============================================================================ +# Tests for function argument validation +# ============================================================================ + + +def test_validate_func_args_raises_error_for_missing_response_parameter() -> None: + def handler() -> None: + pass + with pytest.raises(RequiredArgumentNotPassedException): + _validate_func_args(handler) # pyright: ignore[reportArgumentType] + + +def test_validate_func_args_prints_warning_for_wrong_type_hint(capsys: CaptureFixture[str]) -> None: + class NotResponse: + pass + + def func(_response: NotResponse) -> None: + pass + + _validate_func_args(func) + + output = capsys.readouterr() + + assert "WARNING" in output.out + + +def test_validate_func_args_accepts_missing_type_hint(capsys: CaptureFixture[str]) -> None: + def func(response) -> None: # pyright: ignore[reportMissingParameterType, reportUnknownParameterType] + pass + _validate_func_args(func) # pyright: ignore[reportUnknownArgumentType] + output = capsys.readouterr() + assert output.out == '' + + +# ============================================================================ +# Tests for input flag structuring - undefined flags +# ============================================================================ + + +def test_structuring_input_flags_marks_unregistered_flag_as_undefined() -> None: cmd = Command('cmd') input_flags = InputFlags([InputFlag('ssh', input_value='', status=None)]) assert _structuring_input_flags(cmd, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='', status=ValidationStatus.UNDEFINED)]) -def test_structuring_input_flags2(): + +def test_structuring_input_flags_marks_unregistered_flag_with_value_as_undefined() -> None: cmd = Command('cmd') input_flags = InputFlags([InputFlag('ssh', input_value='some', status=None)]) assert _structuring_input_flags(cmd, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some', status=ValidationStatus.UNDEFINED)]) -def test_structuring_input_flags3(): + +def test_structuring_input_flags_marks_flag_undefined_when_different_flag_registered() -> None: cmd = Command('cmd', flags=Flag('port')) input_flags = InputFlags([InputFlag('ssh', input_value='some2', status=None)]) assert _structuring_input_flags(cmd, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some2', status=ValidationStatus.UNDEFINED)]) -def test_structuring_input_flags4(): + +# ============================================================================ +# Tests for input flag structuring - invalid flags +# ============================================================================ + + +def test_structuring_input_flags_marks_flag_invalid_when_value_provided_for_neither() -> None: command = Command('cmd', flags=Flag('ssh', possible_values=PossibleValues.NEITHER)) input_flags = InputFlags([InputFlag('ssh', input_value='some3', status=None)]) assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some3', status=ValidationStatus.INVALID)]) -def test_structuring_input_flags5(): + +def test_structuring_input_flags_marks_flag_invalid_when_value_not_matching_regex() -> None: command = Command('cmd', flags=Flag('ssh', possible_values=re.compile(r'some[1-5]$'))) input_flags = InputFlags([InputFlag('ssh', input_value='some40', status=None)]) assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='some40', status=ValidationStatus.INVALID)]) -def test_structuring_input_flags6(): + +def test_structuring_input_flags_marks_flag_invalid_when_value_not_in_list() -> None: command = Command('cmd', flags=Flag('ssh', possible_values=['example'])) input_flags = InputFlags([InputFlag('ssh', input_value='example2', status=None)]) assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='example2', status=ValidationStatus.INVALID)]) -def test_structuring_input_flags7(): + +# ============================================================================ +# Tests for input flag structuring - valid flags +# ============================================================================ + + +def test_structuring_input_flags_marks_registered_flag_as_valid() -> None: command = Command('cmd', flags=Flag('port')) input_flags = InputFlags([InputFlag('port', input_value='some2', status=None)]) assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('port', input_value='some2', status=ValidationStatus.VALID)]) -def test_structuring_input_flags8(): + +def test_structuring_input_flags_marks_flag_valid_when_value_in_list() -> None: command = Command('cmd', flags=Flag('port', possible_values=['some2', 'some3'])) input_flags = InputFlags([InputFlag('port', input_value='some2', status=None)]) assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('port', input_value='some2', status=ValidationStatus.VALID)]) -def test_structuring_input_flags9(): + +def test_structuring_input_flags_marks_flag_valid_when_value_matches_regex() -> None: command = Command('cmd', flags=Flag('ssh', possible_values=re.compile(r'more[1-5]$'))) input_flags = InputFlags([InputFlag('ssh', input_value='more5', status=None)]) assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='more5', status=ValidationStatus.VALID)]) -def test_structuring_input_flags10(): + +def test_structuring_input_flags_marks_flag_valid_when_empty_value_for_neither() -> None: command = Command('cmd', flags=Flag('ssh', possible_values=PossibleValues.NEITHER)) input_flags = InputFlags([InputFlag('ssh', input_value='', status=None)]) assert _structuring_input_flags(command, input_flags).input_flags == InputFlags([InputFlag('ssh', input_value='', status=ValidationStatus.VALID)]) -def test_validate_incorrect_func_args1(): - def handler(): - pass - with pytest.raises(RequiredArgumentNotPassedException): - _validate_func_args(handler) # pyright: ignore[reportArgumentType] -def test_get_router_aliases(): +# ============================================================================ +# Tests for router aliases +# ============================================================================ + + +def test_router_aliases_returns_command_aliases() -> None: router = Router() @router.command(Command('some', aliases={'test', 'case'})) - def handler(response: Response) -> None: + def handler(_response: Response) -> None: pass assert router.aliases == {'test', 'case'} -def test_get_router_aliases2(): + +def test_router_aliases_returns_combined_aliases_from_multiple_commands() -> None: router = Router() @router.command(Command('some', aliases={'test', 'case'})) - def handler(response: Response): + def handler(_response: Response) -> None: pass @router.command(Command('ext', aliases={'more', 'foo'})) - def handler2(response: Response): + def handler2(_response: Response) -> None: pass assert router.aliases == {'test', 'case', 'more', 'foo'} -def test_get_router_aliases3(): + +def test_router_aliases_returns_empty_set_when_no_aliases() -> None: router = Router() @router.command(Command('some')) - def handler(response: Response): + def handler(_response: Response) -> None: pass assert router.aliases == set() -def test_find_appropiate_handler(capsys: pytest.CaptureFixture[str]): + +# ============================================================================ +# Tests for handler finding and execution +# ============================================================================ + + +def test_finds_appropriate_handler_executes_handler_by_alias(capsys: CaptureFixture[str]) -> None: router = Router() @router.command(Command('hello', aliases={'hi'})) - def handler(res: Response): + def handler(_res: Response) -> None: print("Hello World!") router.finds_appropriate_handler(InputCommand('hi')) @@ -117,11 +193,12 @@ def handler(res: Response): assert "Hello World!" in output.out -def test_find_appropiate_handler2(capsys: CaptureFixture[str]): + +def test_finds_appropriate_handler_executes_handler_with_flags_by_alias(capsys: CaptureFixture[str]) -> None: router = Router() @router.command(Command('hello', flags=Flag('flag'), aliases={'hi'})) - def handler(res: Response): + def handler(_res: Response) -> None: print("Hello World!") router.finds_appropriate_handler(InputCommand('hi')) @@ -129,20 +206,3 @@ def handler(res: Response): output = capsys.readouterr() assert "Hello World!" in output.out - -def test_wrong_typehint(capsys: pytest.CaptureFixture[str]): - class NotResponse: pass - - def func(response: NotResponse): pass - - _validate_func_args(func) - - output = capsys.readouterr() - - assert "WARNING" in output.out - -def test_missing_typehint(capsys: pytest.CaptureFixture[str]): - def func(response): pass # pyright: ignore[reportMissingParameterType, reportUnknownParameterType] - _validate_func_args(func) # pyright: ignore[reportUnknownArgumentType] - output = capsys.readouterr() - assert output.out == '' From 425342c05985d5454f978694890fe362357c5a30 Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 7 Dec 2025 01:51:24 +0300 Subject: [PATCH 8/8] new tests --- src/argenta/app/models.py | 2 +- src/argenta/command/flag/models.py | 9 +- tests/unit_tests/test_app.py | 290 ++++++++++++++++++++++++++ tests/unit_tests/test_orchestrator.py | 259 +++++++++++++++++++++++ 4 files changed, 554 insertions(+), 6 deletions(-) create mode 100644 tests/unit_tests/test_orchestrator.py diff --git a/src/argenta/app/models.py b/src/argenta/app/models.py index af50cb1..1da6c74 100644 --- a/src/argenta/app/models.py +++ b/src/argenta/app/models.py @@ -340,7 +340,7 @@ def _pre_cycle_setup(self) -> None: if not self._repeat_command_groups_printing_description: self._print_command_group_description() - def _process_exist_and_valid_command(self, input_command: InputCommand): + def _process_exist_and_valid_command(self, input_command: InputCommand) -> None: processing_router = self._current_matching_triggers_with_routers[input_command.trigger.lower()] if processing_router.disable_redirect_stdout: diff --git a/src/argenta/command/flag/models.py b/src/argenta/command/flag/models.py index 45630c7..2cea82f 100644 --- a/src/argenta/command/flag/models.py +++ b/src/argenta/command/flag/models.py @@ -43,11 +43,10 @@ def validate_input_flag_value(self, input_flag_value: str) -> bool: Private. Validates the input flag value :param input_flag_value: The input flag value to validate :return: whether the entered flag is valid as bool - """ - if self.possible_values == PossibleValues.NEITHER: - return input_flag_value == '' - - if self.possible_values == PossibleValues.ALL: + """ + if isinstance(self.possible_values, PossibleValues): + if self.possible_values == PossibleValues.NEITHER: + return input_flag_value == '' return input_flag_value != '' if isinstance(self.possible_values, Pattern): diff --git a/tests/unit_tests/test_app.py b/tests/unit_tests/test_app.py index fd1f7f8..2b99db4 100644 --- a/tests/unit_tests/test_app.py +++ b/tests/unit_tests/test_app.py @@ -6,6 +6,7 @@ from argenta.app.protocols import DescriptionMessageGenerator, NonStandardBehaviorHandler from argenta.command.models import Command, InputCommand from argenta.response import Response +from argenta.response.status import ResponseStatus from argenta.router import Router @@ -348,3 +349,292 @@ def handler(_res: Response) -> None: stdout = capsys.readouterr() assert 'Hello!' in stdout.out + + +# ============================================================================ +# Tests for handler setters and execution +# ============================================================================ + + +def test_set_unknown_command_handler_stores_handler() -> None: + app = App() + call_tracker = {'called': False} + + def custom_handler(_command: InputCommand) -> None: + call_tracker['called'] = True + + app.set_unknown_command_handler(custom_handler) + app._unknown_command_handler(InputCommand('test')) + + assert call_tracker['called'] + + +def test_set_exit_handler_stores_handler() -> None: + app = App() + call_tracker = {'called': False} + + def custom_handler(_response: Response) -> None: + call_tracker['called'] = True + + app.set_exit_command_handler(custom_handler) + app._exit_command_handler(Response(ResponseStatus.ALL_FLAGS_VALID)) + + assert call_tracker['called'] + + +def test_set_empty_command_handler_stores_handler() -> None: + app = App() + call_tracker = {'called': False} + + def custom_handler() -> None: + call_tracker['called'] = True + + app.set_empty_command_handler(custom_handler) + app._empty_input_command_handler() + + assert call_tracker['called'] + + +def test_set_incorrect_input_syntax_handler_stores_handler() -> None: + app = App() + call_tracker = {'called': False} + + def custom_handler(_command: str) -> None: + call_tracker['called'] = True + + app.set_incorrect_input_syntax_handler(custom_handler) + app._incorrect_input_syntax_handler('test --flag') + + assert call_tracker['called'] + + +def test_set_repeated_input_flags_handler_stores_handler() -> None: + app = App() + call_tracker = {'called': False} + + def custom_handler(_command: str) -> None: + call_tracker['called'] = True + + app.set_repeated_input_flags_handler(custom_handler) + app._repeated_input_flags_handler('test --flag --flag') + + assert call_tracker['called'] + + +# ============================================================================ +# Tests for handler execution with output +# ============================================================================ + + +def test_unknown_command_handler_prints_custom_message(capsys: CaptureFixture[str]) -> None: + app = App(override_system_messages=True) + + def custom_handler(command: InputCommand) -> None: + print(f'Command not found: {command.trigger}') + + app.set_unknown_command_handler(custom_handler) + app._unknown_command_handler(InputCommand('unknown')) + + output = capsys.readouterr() + assert 'Command not found: unknown' in output.out + + +def test_exit_command_handler_prints_custom_message(capsys: CaptureFixture[str]) -> None: + app = App(override_system_messages=True) + + def custom_handler(_response: Response) -> None: + print('Goodbye!') + + app.set_exit_command_handler(custom_handler) + app._exit_command_handler(Response(ResponseStatus.ALL_FLAGS_VALID)) + + output = capsys.readouterr() + assert 'Goodbye!' in output.out + + +def test_empty_command_handler_prints_custom_message(capsys: CaptureFixture[str]) -> None: + app = App(override_system_messages=True) + + def custom_handler() -> None: + print('Please enter a command') + + app.set_empty_command_handler(custom_handler) + app._empty_input_command_handler() + + output = capsys.readouterr() + assert 'Please enter a command' in output.out + + +def test_incorrect_syntax_handler_prints_custom_message(capsys: CaptureFixture[str]) -> None: + app = App(override_system_messages=True) + + def custom_handler(command: str) -> None: + print(f'Syntax error in: {command}') + + app.set_incorrect_input_syntax_handler(custom_handler) + app._incorrect_input_syntax_handler('test --flag') + + output = capsys.readouterr() + assert 'Syntax error in: test --flag' in output.out + + +def test_repeated_flags_handler_prints_custom_message(capsys: CaptureFixture[str]) -> None: + app = App(override_system_messages=True) + + def custom_handler(command: str) -> None: + print(f'Duplicate flags in: {command}') + + app.set_repeated_input_flags_handler(custom_handler) + app._repeated_input_flags_handler('test --flag --flag') + + output = capsys.readouterr() + assert 'Duplicate flags in: test --flag --flag' in output.out + + +# ============================================================================ +# Tests for default handler behavior +# ============================================================================ + + +def test_default_unknown_command_handler_prints_message(capsys: CaptureFixture[str]) -> None: + app = App(override_system_messages=True) + app._unknown_command_handler(InputCommand('unknown')) + + output = capsys.readouterr() + assert 'Unknown command: unknown' in output.out + + +def test_default_empty_command_handler_prints_message(capsys: CaptureFixture[str]) -> None: + app = App(override_system_messages=True) + app._empty_input_command_handler() + + output = capsys.readouterr() + assert 'Empty input command' in output.out + + +def test_default_incorrect_syntax_handler_prints_message(capsys: CaptureFixture[str]) -> None: + app = App(override_system_messages=True) + app._incorrect_input_syntax_handler('test --flag') + + output = capsys.readouterr() + assert 'Incorrect flag syntax: test --flag' in output.out + + +def test_default_repeated_flags_handler_prints_message(capsys: CaptureFixture[str]) -> None: + app = App(override_system_messages=True) + app._repeated_input_flags_handler('test --flag --flag') + + output = capsys.readouterr() + assert 'Repeated input flags: test --flag --flag' in output.out + + +# ============================================================================ +# Tests for handler chaining and multiple calls +# ============================================================================ + + +def test_handler_can_be_replaced_multiple_times() -> None: + app = App() + call_tracker = {'count': 0} + + def handler1(_command: InputCommand) -> None: + call_tracker['count'] += 1 + + def handler2(_command: InputCommand) -> None: + call_tracker['count'] += 10 + + app.set_unknown_command_handler(handler1) + app._unknown_command_handler(InputCommand('test')) + assert call_tracker['count'] == 1 + + app.set_unknown_command_handler(handler2) + app._unknown_command_handler(InputCommand('test')) + assert call_tracker['count'] == 11 + + +def test_handler_receives_correct_parameters() -> None: + app = App() + received_data = {'trigger': None} + + def custom_handler(command: InputCommand) -> None: + received_data['trigger'] = command.trigger + + app.set_unknown_command_handler(custom_handler) + app._unknown_command_handler(InputCommand('mycommand')) + + assert received_data['trigger'] == 'mycommand' + + +def test_exit_handler_receives_response_object() -> None: + app = App() + received_data = {'response': None} + + def custom_handler(response: Response) -> None: + received_data['response'] = response + + app.set_exit_command_handler(custom_handler) + test_response = Response(ResponseStatus.ALL_FLAGS_VALID) + app._exit_command_handler(test_response) + + assert received_data['response'] is test_response + + +# ============================================================================ +# Tests for handler integration with routers +# ============================================================================ + + +def test_app_with_router_and_custom_unknown_handler(capsys: CaptureFixture[str]) -> None: + app = App(override_system_messages=True) + router = Router() + + @router.command(Command('test')) + def handler(_res: Response) -> None: + print('test executed') + + app.include_router(router) + + def custom_unknown_handler(command: InputCommand) -> None: + print(f'Not found: {command.trigger}') + + app.set_unknown_command_handler(custom_unknown_handler) + + # Test that unknown command uses custom handler + assert app._is_unknown_command(InputCommand('unknown')) + app._unknown_command_handler(InputCommand('unknown')) + + output = capsys.readouterr() + assert 'Not found: unknown' in output.out + + +def test_app_handlers_work_with_multiple_routers() -> None: + app = App(override_system_messages=True) + router1 = Router() + router2 = Router() + + @router1.command(Command('cmd1')) + def handler1(_res: Response) -> None: + pass + + @router2.command(Command('cmd2')) + def handler2(_res: Response) -> None: + pass + + app.include_routers(router1, router2) + app._pre_cycle_setup() + + call_tracker = {'called': False} + + def custom_handler(_command: InputCommand) -> None: + call_tracker['called'] = True + + app.set_unknown_command_handler(custom_handler) + + # Both commands should be known + assert not app._is_unknown_command(InputCommand('cmd1')) + assert not app._is_unknown_command(InputCommand('cmd2')) + + # Unknown command should trigger handler + assert app._is_unknown_command(InputCommand('unknown')) + app._unknown_command_handler(InputCommand('unknown')) + assert call_tracker['called'] diff --git a/tests/unit_tests/test_orchestrator.py b/tests/unit_tests/test_orchestrator.py new file mode 100644 index 0000000..9393006 --- /dev/null +++ b/tests/unit_tests/test_orchestrator.py @@ -0,0 +1,259 @@ +import pytest +from dishka import Provider +from pytest_mock import MockerFixture + +from argenta import App, Router +from argenta.command import Command +from argenta.orchestrator import Orchestrator +from argenta.orchestrator.argparser import ArgParser +from argenta.response import Response + + +# ============================================================================ +# Fixtures +# ============================================================================ + + +@pytest.fixture +def mock_argparser(mocker: MockerFixture) -> ArgParser: + """Create a mock ArgParser that doesn't actually parse sys.argv""" + argparser = ArgParser(processed_args=[]) + mocker.patch.object(argparser, '_parse_args') + return argparser + + +@pytest.fixture +def sample_app() -> App: + """Create a sample App for testing""" + return App(override_system_messages=True) + + +@pytest.fixture +def sample_router() -> Router: + """Create a sample Router with a test command""" + router = Router() + + @router.command(Command('test')) + def handler(_res: Response) -> None: + print('test command executed') + + return router + + +# ============================================================================ +# Tests for Orchestrator initialization +# ============================================================================ + + +def test_orchestrator_initializes_with_default_argparser(mocker: MockerFixture) -> None: + """Test Orchestrator initialization with default ArgParser""" + mocker.patch('sys.argv', ['test_program']) + orchestrator = Orchestrator() + assert orchestrator._arg_parser is not None + assert isinstance(orchestrator._arg_parser, ArgParser) + + +def test_orchestrator_initializes_with_custom_argparser(mock_argparser: ArgParser) -> None: + """Test Orchestrator initialization with custom ArgParser""" + orchestrator = Orchestrator(arg_parser=mock_argparser) + assert orchestrator._arg_parser is mock_argparser + + +def test_orchestrator_initializes_with_custom_providers(mocker: MockerFixture) -> None: + """Test Orchestrator initialization with custom providers""" + mocker.patch('sys.argv', ['test_program']) + custom_provider = Provider() + orchestrator = Orchestrator(custom_providers=[custom_provider]) + assert custom_provider in orchestrator._custom_providers + + +def test_orchestrator_initializes_with_auto_inject_disabled(mocker: MockerFixture) -> None: + """Test Orchestrator initialization with auto_inject_handlers disabled""" + mocker.patch('sys.argv', ['test_program']) + orchestrator = Orchestrator(auto_inject_handlers=False) + assert orchestrator._auto_inject_handlers is False + + +def test_orchestrator_initializes_with_auto_inject_enabled(mocker: MockerFixture) -> None: + """Test Orchestrator initialization with auto_inject_handlers enabled (default)""" + mocker.patch('sys.argv', ['test_program']) + orchestrator = Orchestrator() + assert orchestrator._auto_inject_handlers is True + + +def test_orchestrator_parses_args_on_initialization(mocker: MockerFixture, mock_argparser: ArgParser) -> None: + """Test that Orchestrator calls _parse_args on initialization""" + parse_spy = mocker.spy(mock_argparser, '_parse_args') + _orchestrator = Orchestrator(arg_parser=mock_argparser) + parse_spy.assert_called_once() + + +# ============================================================================ +# Tests for start_polling method +# ============================================================================ + + +def test_start_polling_creates_dishka_container( + mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App +) -> None: + """Test that start_polling creates a dishka container""" + mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container') + _mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka') + mocker.patch.object(sample_app, 'run_polling') + + orchestrator = Orchestrator(arg_parser=mock_argparser) + orchestrator.start_polling(sample_app) + + mock_make_container.assert_called_once() + assert mock_make_container.call_args[1]['context'] == {ArgParser: mock_argparser} + + +def test_start_polling_calls_setup_dishka_with_auto_inject_enabled( + mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App +) -> None: + """Test that start_polling calls setup_dishka with auto_inject=True""" + mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] + mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container) + mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka') + mocker.patch.object(sample_app, 'run_polling') + + orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=True) + orchestrator.start_polling(sample_app) + + mock_setup_dishka.assert_called_once_with(sample_app, mock_container, auto_inject=True) + + +def test_start_polling_calls_setup_dishka_with_auto_inject_disabled( + mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App +) -> None: + """Test that start_polling calls setup_dishka with auto_inject=False""" + mock_container = mocker.MagicMock() # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] + mocker.patch('argenta.orchestrator.entity.make_container', return_value=mock_container) + mock_setup_dishka = mocker.patch('argenta.orchestrator.entity.setup_dishka') + mocker.patch.object(sample_app, 'run_polling') + + orchestrator = Orchestrator(arg_parser=mock_argparser, auto_inject_handlers=False) + orchestrator.start_polling(sample_app) + + mock_setup_dishka.assert_called_once_with(sample_app, mock_container, auto_inject=False) + + +def test_start_polling_calls_app_run_polling( + mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App +) -> None: + """Test that start_polling calls app.run_polling()""" + mocker.patch('argenta.orchestrator.entity.make_container') + mocker.patch('argenta.orchestrator.entity.setup_dishka') + mock_run_polling = mocker.patch.object(sample_app, 'run_polling') + + orchestrator = Orchestrator(arg_parser=mock_argparser) + orchestrator.start_polling(sample_app) + + mock_run_polling.assert_called_once() + + +def test_start_polling_includes_custom_providers_in_container( + mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App +) -> None: + """Test that start_polling includes custom providers in container""" + custom_provider = Provider() + mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container') + mocker.patch('argenta.orchestrator.entity.setup_dishka') + mocker.patch.object(sample_app, 'run_polling') + + orchestrator = Orchestrator(arg_parser=mock_argparser, custom_providers=[custom_provider]) + orchestrator.start_polling(sample_app) + + # Check that custom_provider was passed to make_container + call_args = mock_make_container.call_args[0] + assert custom_provider in call_args + + +# ============================================================================ +# Tests for integration with App +# ============================================================================ + + +def test_orchestrator_integrates_with_app_with_router( + mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App, sample_router: Router +) -> None: + """Test that Orchestrator properly integrates with App that has routers""" + mocker.patch('argenta.orchestrator.entity.make_container') + mocker.patch('argenta.orchestrator.entity.setup_dishka') + mock_run_polling = mocker.patch.object(sample_app, 'run_polling') + + sample_app.include_router(sample_router) + + orchestrator = Orchestrator(arg_parser=mock_argparser) + orchestrator.start_polling(sample_app) + + mock_run_polling.assert_called_once() + assert len(sample_app.registered_routers.registered_routers) == 1 + + +# ============================================================================ +# Tests for ArgParser integration +# ============================================================================ + + +def test_orchestrator_passes_argparser_to_container_context( + mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App +) -> None: + """Test that Orchestrator passes ArgParser instance to container context""" + mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container') + mocker.patch('argenta.orchestrator.entity.setup_dishka') + mocker.patch.object(sample_app, 'run_polling') + + orchestrator = Orchestrator(arg_parser=mock_argparser) + orchestrator.start_polling(sample_app) + + # Verify that ArgParser was passed in context + call_kwargs = mock_make_container.call_args[1] + assert 'context' in call_kwargs + assert ArgParser in call_kwargs['context'] + assert call_kwargs['context'][ArgParser] is mock_argparser + + +# ============================================================================ +# Tests for error handling +# ============================================================================ + + +def test_orchestrator_handles_app_run_polling_exception( + mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App +) -> None: + """Test that Orchestrator propagates exceptions from app.run_polling()""" + mocker.patch('argenta.orchestrator.entity.make_container') + mocker.patch('argenta.orchestrator.entity.setup_dishka') + mocker.patch.object(sample_app, 'run_polling', side_effect=RuntimeError("Test error")) + + orchestrator = Orchestrator(arg_parser=mock_argparser) + + with pytest.raises(RuntimeError, match="Test error"): + orchestrator.start_polling(sample_app) + + +# ============================================================================ +# Tests for multiple providers +# ============================================================================ + + +def test_orchestrator_accepts_multiple_custom_providers( + mocker: MockerFixture, mock_argparser: ArgParser, sample_app: App +) -> None: + """Test that Orchestrator accepts multiple custom providers""" + provider1 = Provider() + provider2 = Provider() + mock_make_container = mocker.patch('argenta.orchestrator.entity.make_container') + mocker.patch('argenta.orchestrator.entity.setup_dishka') + mocker.patch.object(sample_app, 'run_polling') + + orchestrator = Orchestrator( + arg_parser=mock_argparser, + custom_providers=[provider1, provider2] + ) + orchestrator.start_polling(sample_app) + + call_args = mock_make_container.call_args[0] + assert provider1 in call_args + assert provider2 in call_args