mirror of
https://github.com/koloideal/Argenta.git
synced 2026-06-10 10:05:28 +03:00
tests
This commit is contained in:
@@ -14,7 +14,7 @@ arg_parser: ArgParser = ArgParser(
|
|||||||
)
|
)
|
||||||
app: App = App(
|
app: App = App(
|
||||||
dividing_line=DynamicDividingLine(),
|
dividing_line=DynamicDividingLine(),
|
||||||
autocompleter=AutoCompleter(),
|
autocompleter=AutoCompleter(history_filename="history.txt")
|
||||||
)
|
)
|
||||||
orchestrator: Orchestrator = Orchestrator(arg_parser)
|
orchestrator: Orchestrator = Orchestrator(arg_parser)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from argenta.router.defaults import system_router
|
|||||||
from dishka import FromDishka
|
from dishka import FromDishka
|
||||||
|
|
||||||
|
|
||||||
work_router: Router = Router(title="Work points:", disable_redirect_stdout=True)
|
work_router: Router = Router(title="Work points:")
|
||||||
|
|
||||||
flag = Flag("csdv", possible_values=PossibleValues.NEITHER)
|
flag = Flag("csdv", possible_values=PossibleValues.NEITHER)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ from typing import Never
|
|||||||
|
|
||||||
class AutoCompleter:
|
class AutoCompleter:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, history_filename: str | None = None, autocomplete_button: str = "tab"
|
self, history_filename: str | None = None,
|
||||||
|
autocomplete_button: str = "tab"
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Public. Configures and implements auto-completion of input command
|
Public. Configures and implements auto-completion of input command
|
||||||
@@ -60,12 +61,16 @@ class AutoCompleter:
|
|||||||
else:
|
else:
|
||||||
for line in all_commands:
|
for line in all_commands:
|
||||||
readline.add_history(line)
|
readline.add_history(line)
|
||||||
|
|
||||||
|
if not self.history_filename:
|
||||||
|
for line in all_commands:
|
||||||
|
readline.add_history(line)
|
||||||
|
|
||||||
readline.set_completer(self._complete)
|
readline.set_completer(self._complete)
|
||||||
readline.set_completer_delims(readline.get_completer_delims().replace(" ", ""))
|
readline.set_completer_delims(readline.get_completer_delims().replace(" ", ""))
|
||||||
readline.parse_and_bind(f"{self.autocomplete_button}: complete")
|
readline.parse_and_bind(f"{self.autocomplete_button}: complete")
|
||||||
|
|
||||||
def exit_setup(self, all_commands: list[str]) -> None:
|
def exit_setup(self, all_commands: list[str], ignore_command_register: bool) -> None:
|
||||||
"""
|
"""
|
||||||
Private. Exit setup function
|
Private. Exit setup function
|
||||||
:return: None
|
:return: None
|
||||||
@@ -76,10 +81,16 @@ class AutoCompleter:
|
|||||||
raw_history = history_file.read()
|
raw_history = history_file.read()
|
||||||
pretty_history: list[str] = []
|
pretty_history: list[str] = []
|
||||||
for line in set(raw_history.strip().split("\n")):
|
for line in set(raw_history.strip().split("\n")):
|
||||||
if line.split()[0] in all_commands:
|
if _is_command_exist(line.split()[0], all_commands, ignore_command_register):
|
||||||
pretty_history.append(line)
|
pretty_history.append(line)
|
||||||
with open(self.history_filename, "w") as history_file:
|
with open(self.history_filename, "w") as history_file:
|
||||||
_ = history_file.write("\n".join(pretty_history))
|
_ = history_file.write("\n".join(pretty_history))
|
||||||
|
|
||||||
|
|
||||||
|
def _is_command_exist(command: str, existing_commands: list[str], ignore_command_register: bool) -> bool:
|
||||||
|
if ignore_command_register:
|
||||||
|
return command.lower() in existing_commands
|
||||||
|
return command in existing_commands
|
||||||
|
|
||||||
def _get_history_items() -> list[str] | list[Never]:
|
def _get_history_items() -> list[str] | list[Never]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -465,7 +465,8 @@ class App(BaseApp):
|
|||||||
if self._is_exit_command(input_command):
|
if self._is_exit_command(input_command):
|
||||||
system_router.finds_appropriate_handler(input_command)
|
system_router.finds_appropriate_handler(input_command)
|
||||||
self._autocompleter.exit_setup(
|
self._autocompleter.exit_setup(
|
||||||
list(self._current_matching_triggers_with_routers.keys())
|
list(self._current_matching_triggers_with_routers.keys()),
|
||||||
|
self._ignore_command_register
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ class BooleanArgument(BaseArgument):
|
|||||||
|
|
||||||
class InputArgument:
|
class InputArgument:
|
||||||
def __init__(self, name: str,
|
def __init__(self, name: str,
|
||||||
value: str | None,
|
value: str | Literal[True],
|
||||||
founder_class: type[BaseArgument]) -> None:
|
founder_class: type[BaseArgument]) -> None:
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
self.value: str | None = value
|
self.value: str | Literal[True] = value
|
||||||
self.founder_class: type[BaseArgument] = founder_class
|
self.founder_class: type[BaseArgument] = founder_class
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ class ArgParser:
|
|||||||
|
|
||||||
def _parse_args(self) -> None:
|
def _parse_args(self) -> None:
|
||||||
self.parsed_argspace = ArgSpace.from_namespace(namespace=self._core.parse_args(),
|
self.parsed_argspace = ArgSpace.from_namespace(namespace=self._core.parse_args(),
|
||||||
processed_args=self.processed_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:
|
||||||
for arg in processed_args:
|
for arg in processed_args:
|
||||||
|
|||||||
+169
-111
@@ -1,18 +1,21 @@
|
|||||||
import unittest
|
import pytest
|
||||||
from unittest.mock import MagicMock, patch
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
|
from unittest.mock import MagicMock, call
|
||||||
|
|
||||||
from argenta.orchestrator.argparser.entity import ArgParser, ArgSpace
|
from argenta.orchestrator.argparser.entity import ArgParser, ArgSpace
|
||||||
from argenta.orchestrator.argparser.arguments.models import (
|
from argenta.orchestrator.argparser.arguments.models import (
|
||||||
ValueArgument,
|
ValueArgument,
|
||||||
BooleanArgument,
|
BooleanArgument,
|
||||||
InputArgument,
|
InputArgument,
|
||||||
BaseArgument
|
BaseArgument,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestArgumentClasses(unittest.TestCase):
|
class TestArgumentCreation:
|
||||||
|
"""Tests for the creation and attribute validation of argument model classes."""
|
||||||
|
|
||||||
def test_value_argument_creation(self):
|
def test_value_argument_creation(self):
|
||||||
|
"""Ensures ValueArgument instances are created with correct attributes."""
|
||||||
arg = ValueArgument(
|
arg = ValueArgument(
|
||||||
name="test_arg",
|
name="test_arg",
|
||||||
prefix="--",
|
prefix="--",
|
||||||
@@ -20,145 +23,200 @@ class TestArgumentClasses(unittest.TestCase):
|
|||||||
possible_values=["one", "two"],
|
possible_values=["one", "two"],
|
||||||
default="one",
|
default="one",
|
||||||
is_required=True,
|
is_required=True,
|
||||||
is_deprecated=False
|
is_deprecated=False,
|
||||||
)
|
)
|
||||||
self.assertEqual(arg.name, "test_arg")
|
assert arg.name == "test_arg"
|
||||||
self.assertEqual(arg.prefix, "--")
|
assert arg.prefix == "--"
|
||||||
self.assertEqual(arg.help, "A test argument.")
|
assert arg.help == "A test argument."
|
||||||
self.assertEqual(arg.possible_values, ["one", "two"])
|
assert arg.possible_values == ["one", "two"]
|
||||||
self.assertEqual(arg.default, "one")
|
assert arg.default == "one"
|
||||||
self.assertTrue(arg.is_required)
|
assert arg.is_required is True
|
||||||
self.assertFalse(arg.is_deprecated)
|
assert arg.is_deprecated is False
|
||||||
self.assertEqual(arg.action, "store")
|
assert arg.action == "store"
|
||||||
self.assertEqual(arg.string_entity, "--test_arg")
|
assert arg.string_entity == "--test_arg"
|
||||||
|
|
||||||
def test_boolean_argument_creation(self):
|
def test_boolean_argument_creation(self):
|
||||||
|
"""Ensures BooleanArgument instances are created with correct attributes."""
|
||||||
arg = BooleanArgument(
|
arg = BooleanArgument(
|
||||||
name="verbose",
|
name="verbose", prefix="-", help="Enable verbose mode.", is_deprecated=True
|
||||||
prefix="-",
|
|
||||||
help="Enable verbose mode.",
|
|
||||||
is_deprecated=True
|
|
||||||
)
|
)
|
||||||
self.assertEqual(arg.name, "verbose")
|
assert arg.name == "verbose"
|
||||||
self.assertEqual(arg.prefix, "-")
|
assert arg.prefix == "-"
|
||||||
self.assertEqual(arg.help, "Enable verbose mode.")
|
assert arg.help == "Enable verbose mode."
|
||||||
self.assertTrue(arg.is_deprecated)
|
assert arg.is_deprecated is True
|
||||||
self.assertEqual(arg.action, "store_true")
|
assert arg.action == "store_true"
|
||||||
self.assertEqual(arg.string_entity, "-verbose")
|
assert arg.string_entity == "-verbose"
|
||||||
|
|
||||||
def test_input_argument_creation(self):
|
def test_input_argument_creation(self):
|
||||||
|
"""Ensures InputArgument instances are created with correct attributes."""
|
||||||
arg = InputArgument(
|
arg = InputArgument(
|
||||||
name="file",
|
name="file", value="/path/to/file", founder_class=ValueArgument
|
||||||
value="/path/to/file",
|
|
||||||
founder_class=ValueArgument
|
|
||||||
)
|
)
|
||||||
self.assertEqual(arg.name, "file")
|
assert arg.name == "file"
|
||||||
self.assertEqual(arg.value, "/path/to/file")
|
assert arg.value == "/path/to/file"
|
||||||
self.assertEqual(arg.founder_class, ValueArgument)
|
assert arg.founder_class is ValueArgument
|
||||||
|
|
||||||
|
|
||||||
class TestArgParser(unittest.TestCase):
|
class TestArgSpace:
|
||||||
def setUp(self):
|
"""Tests for the ArgSpace class, which holds parsed argument values."""
|
||||||
self.value_arg = ValueArgument(name="config", help="Path to config file")
|
|
||||||
self.bool_arg = BooleanArgument(name="debug", help="Enable debug mode")
|
|
||||||
self.processed_args = [self.value_arg, self.bool_arg]
|
|
||||||
|
|
||||||
def test_argparser_initialization(self):
|
@pytest.fixture
|
||||||
parser = ArgParser(
|
def mock_arguments(self) -> list[InputArgument]:
|
||||||
processed_args=self.processed_args,
|
"""Provides a list of mock InputArgument objects for testing."""
|
||||||
name="TestApp",
|
return [
|
||||||
description="A test application.",
|
InputArgument(name="arg1", value="val1", founder_class=ValueArgument),
|
||||||
epilog="Test epilog."
|
InputArgument(name="arg2", value=True, founder_class=BooleanArgument),
|
||||||
)
|
InputArgument(name="arg3", value="val3", founder_class=ValueArgument),
|
||||||
self.assertEqual(parser.name, "TestApp")
|
]
|
||||||
self.assertEqual(parser.description, "A test application.")
|
|
||||||
self.assertEqual(parser.epilog, "Test epilog.")
|
|
||||||
self.assertEqual(parser.processed_args, self.processed_args)
|
|
||||||
|
|
||||||
@patch('argenta.orchestrator.argparser.entity.ArgumentParser.parse_args')
|
@pytest.fixture
|
||||||
def test_parse_args(self, mock_parse_args: MagicMock):
|
def arg_space(self, mock_arguments: list[InputArgument]) -> ArgSpace:
|
||||||
mock_namespace = Namespace(config='config.json', debug=True)
|
"""Provides a pre-populated ArgSpace instance."""
|
||||||
mock_parse_args.return_value = mock_namespace
|
return ArgSpace(all_arguments=mock_arguments)
|
||||||
|
|
||||||
parser = ArgParser(processed_args=self.processed_args)
|
def test_initialization(self, arg_space: ArgSpace, mock_arguments: list[InputArgument]):
|
||||||
arg_space = parser.parse_args()
|
"""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
|
||||||
|
|
||||||
self.assertIsInstance(arg_space, ArgSpace)
|
def test_get_by_name(self, arg_space: ArgSpace, mock_arguments: list[InputArgument]):
|
||||||
self.assertEqual(len(arg_space.all_arguments), 2)
|
"""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')
|
config_arg = arg_space.get_by_name('config')
|
||||||
debug_arg = arg_space.get_by_name('debug')
|
debug_arg = arg_space.get_by_name('debug')
|
||||||
|
|
||||||
self.assertIsNotNone(config_arg)
|
assert config_arg is not None
|
||||||
if config_arg:
|
assert config_arg.value == "config.json"
|
||||||
self.assertEqual(config_arg.value, 'config.json')
|
assert config_arg.founder_class is ValueArgument
|
||||||
self.assertEqual(config_arg.founder_class, ValueArgument)
|
|
||||||
|
|
||||||
self.assertIsNotNone(debug_arg)
|
assert debug_arg is not None
|
||||||
if debug_arg:
|
assert debug_arg.value is True
|
||||||
self.assertTrue(debug_arg.value)
|
assert debug_arg.founder_class is BooleanArgument
|
||||||
self.assertEqual(debug_arg.founder_class, BooleanArgument)
|
|
||||||
|
|
||||||
|
|
||||||
class TestArgSpace(unittest.TestCase):
|
class TestArgParser:
|
||||||
def setUp(self):
|
"""Tests for the ArgParser class, which orchestrates argument parsing."""
|
||||||
self.input_arg1 = InputArgument(name="arg1", value="val1", founder_class=ValueArgument)
|
|
||||||
self.input_arg2 = InputArgument(name="arg2", value="val2", founder_class=BooleanArgument)
|
|
||||||
self.input_arg3 = InputArgument(name="arg3", value="val3", founder_class=ValueArgument)
|
|
||||||
self.arg_space = ArgSpace(all_arguments=[self.input_arg1, self.input_arg2, self.input_arg3])
|
|
||||||
|
|
||||||
def test_argspace_initialization(self):
|
@pytest.fixture
|
||||||
self.assertEqual(len(self.arg_space.all_arguments), 3)
|
def value_arg(self) -> ValueArgument:
|
||||||
self.assertIn(self.input_arg1, self.arg_space.all_arguments)
|
"""Provides a sample ValueArgument."""
|
||||||
self.assertIn(self.input_arg2, self.arg_space.all_arguments)
|
return ValueArgument(name="config", help="Path to config file", default="dev.json", is_required=False, possible_values=["dev.json", "prod.json"])
|
||||||
self.assertIn(self.input_arg3, self.arg_space.all_arguments)
|
|
||||||
|
|
||||||
def test_get_by_name(self):
|
@pytest.fixture
|
||||||
found_arg = self.arg_space.get_by_name("arg1")
|
def bool_arg(self) -> BooleanArgument:
|
||||||
self.assertIsNotNone(found_arg)
|
"""Provides a sample BooleanArgument."""
|
||||||
if found_arg:
|
return BooleanArgument(name="debug", help="Enable debug mode")
|
||||||
self.assertEqual(found_arg, self.input_arg1)
|
|
||||||
|
|
||||||
def test_get_by_name_not_found(self):
|
@pytest.fixture
|
||||||
found_arg = self.arg_space.get_by_name("non_existent_arg")
|
def processed_args(self, value_arg: ValueArgument, bool_arg: BooleanArgument) -> list:
|
||||||
self.assertIsNone(found_arg)
|
"""Provides a list of processed arguments."""
|
||||||
|
return [value_arg, bool_arg]
|
||||||
|
|
||||||
def test_get_by_type(self):
|
def test_initialization(self, processed_args: list):
|
||||||
value_args = self.arg_space.get_by_type(ValueArgument)
|
"""Tests that the ArgParser constructor correctly assigns attributes."""
|
||||||
self.assertEqual(len(value_args), 2)
|
parser = ArgParser(
|
||||||
self.assertIn(self.input_arg1, value_args)
|
processed_args=processed_args,
|
||||||
self.assertIn(self.input_arg3, value_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 == []
|
||||||
|
|
||||||
bool_args = self.arg_space.get_by_type(BooleanArgument)
|
def test_register_args(self, mocker, value_arg: ValueArgument, bool_arg: BooleanArgument):
|
||||||
self.assertEqual(len(bool_args), 1)
|
"""Tests that arguments are correctly registered with the underlying ArgumentParser."""
|
||||||
self.assertIn(self.input_arg2, bool_args)
|
mock_add_argument = mocker.patch("argparse.ArgumentParser.add_argument")
|
||||||
|
|
||||||
def test_get_by_type_not_found(self):
|
parser = ArgParser(processed_args=[value_arg, bool_arg])
|
||||||
class OtherArgument(BaseArgument):
|
|
||||||
pass
|
|
||||||
|
|
||||||
other_args = self.arg_space.get_by_type(OtherArgument)
|
expected_calls = [
|
||||||
self.assertEqual(len(other_args), 0)
|
# Call for the ValueArgument
|
||||||
|
call(
|
||||||
def test_from_namespace(self):
|
value_arg.string_entity,
|
||||||
namespace = Namespace(arg1="val1", debug=True)
|
action=value_arg.action,
|
||||||
processed_args = [
|
help=value_arg.help,
|
||||||
ValueArgument(name="arg1", prefix="--"),
|
default=value_arg.default,
|
||||||
BooleanArgument(name="debug", prefix="-")
|
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)
|
||||||
|
|
||||||
arg_space = ArgSpace.from_namespace(namespace, processed_args)
|
def test_parse_args_populates_argspace(self, mocker, processed_args: list):
|
||||||
self.assertEqual(len(arg_space.all_arguments), 2)
|
"""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
|
||||||
|
)
|
||||||
|
|
||||||
arg1 = arg_space.get_by_name('arg1')
|
# 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')
|
debug_arg = arg_space.get_by_name('debug')
|
||||||
|
|
||||||
self.assertIsNotNone(arg1)
|
assert config_arg is not None
|
||||||
if arg1:
|
assert config_arg.value == 'config.json'
|
||||||
self.assertEqual(arg1.value, "val1")
|
assert config_arg.founder_class is ValueArgument
|
||||||
self.assertEqual(arg1.founder_class, ValueArgument)
|
|
||||||
|
|
||||||
self.assertIsNotNone(debug_arg)
|
assert debug_arg is not None
|
||||||
if debug_arg:
|
assert debug_arg.value is True
|
||||||
self.assertTrue(debug_arg.value)
|
assert debug_arg.founder_class is BooleanArgument
|
||||||
self.assertEqual(debug_arg.founder_class, BooleanArgument)
|
|
||||||
|
|||||||
@@ -0,0 +1,196 @@
|
|||||||
|
import os
|
||||||
|
from unittest.mock import MagicMock, patch, call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# 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"]
|
||||||
Reference in New Issue
Block a user