diff --git a/main.py b/main.py index 5ad025b..b15f4de 100755 --- a/main.py +++ b/main.py @@ -1,93 +1,53 @@ #!/usr/bin/env python3 -from src.manager import * -import logging -import colorlog -import sys +import argparse +import json +from src.manager import ServiceFactory, ServiceRepository, Manager +from src.services import DockerServiceStrategy, SystemServiceStrategy -def initialize_logger(): - """Initialize and return a color logger.""" - handler = colorlog.StreamHandler() - handler.setFormatter(colorlog.ColoredFormatter( - '%(log_color)s%(asctime)s - %(levelname)s - %(message)s', - log_colors={ - 'DEBUG': 'cyan', - 'INFO': 'green', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'bold_red', - } - )) +def main(): + parser = argparse.ArgumentParser(description="服务管理工具") + subparsers = parser.add_subparsers(dest="command", required=True) + + # 注册服务命令 + register_parser = subparsers.add_parser("register", help="注册新服务") + register_parser.add_argument("tag", choices=["sys", "docker"], help="服务类型") + register_parser.add_argument("name", help="服务名称") + register_parser.add_argument("--path", help="Docker服务路径(仅docker类型需要)") + + # 列出服务命令 + subparsers.add_parser("list", help="列出所有服务") + + # 执行操作命令 + operate_parser = subparsers.add_parser("operate", help="执行服务操作") + operate_parser.add_argument("index", type=int, help="服务索引") + + args = parser.parse_args() + + # 初始化仓库和管理器 + repo = ServiceRepository("services.json") + manager = Manager(repo) + + if args.command == "register": + # 创建服务 + service = manager.register_service(args.tag, args.name, args.path) + print(f"服务注册成功: {service.name}") + + elif args.command == "list": + # 列出所有服务 + services = manager.list_services() + for i, service in enumerate(services): + print(f"{i}: [{service.tag}] {service.name} {f'(path: {service.path})' if service.path else ''}") + + elif args.command == "operate": + # 执行服务操作 + try: + manager.execute_service_operation(args.index) + print("操作执行成功") + except IndexError: + print("错误:无效的服务索引") + except Exception as e: + print(f"操作失败: {str(e)}") - logger = colorlog.getLogger(__name__) - logger.setLevel(logging.INFO) - logger.addHandler(handler) - return logger - -def initialize_service_manager(): - """Initialize and return the service manager.""" - return Manager() - -logger = initialize_logger() -service_manager = initialize_service_manager() - -def start_menu(): - logger.info(f"=======================================") - logger.info(f"========== Services Manager ===========") - logger.info(f"=======================================") - logger.info(f"Please choose a operation to carry out:") - logger.info(f"=====> 1. Register a new service <=====") - logger.info(f"=====> 2. Manage exsist services <=====") - logger.info(f"=====> c. Count the number of services") - logger.info(f"=====> q. Quit ") - -def main_menu(): - """Display the main menu and handle user input.""" - - while True: - start_menu() - - start_choice = input("Enter your choice: ").strip() - if start_choice == '1': - service_tag = input("Please input the deploy way of service you want to register('docker' | 'sys'): ") - service_name = input("Please input the name of the service: ") - if service_tag == "docker": - service_path = input("Please input the config path of the service where 'docker-compose.yml' located: ") - service_manager.register_service(service_tag, service_name, service_path) - else: - service_manager.register_service(service_tag, service_name) - if start_choice == '2': - if not service_manager.services_list: - logger.error("No services registered.") - continue - while True: - logger.info(f"=====> Exsist Services Management <=====") - service_code = 1 - for service in service_manager.services_list: - logger.info(f"=====> {service_code}: {service._name}") - service_code += 1 - # logger.info("=====> a. Select All") - logger.info("=====> q. Quit") - - manage_choice = input("Please select a choice to carry out: ").strip() - if manage_choice == 'q': - logger.info("Exit main manager") - break - # if manage_choice == 'a': - # pass - try: - code = int(manage_choice) - if 0 <= code < len(service_manager.services_list): - service_manager.services_list[code].service_operation() - else: - logger.error("Invalid service code.") - except ValueError: - logger.error("Please enter a valid number.") - if start_choice == 'c': - service_manager.list_services() - if start_choice == 'q': - logger.info(f"Exit the manager process") - break - -if __name__ == '__main__': - sys.exit(main_menu()) +if __name__ == "__main__": + main() diff --git a/services.json b/services.json new file mode 100644 index 0000000..99fc772 --- /dev/null +++ b/services.json @@ -0,0 +1,7 @@ +[ + { + "tag": "sys", + "name": "nginx", + "path": null + } +] \ No newline at end of file diff --git a/src/manager.py b/src/manager.py index 76e7fad..5a2ee8a 100755 --- a/src/manager.py +++ b/src/manager.py @@ -1,8 +1,11 @@ #!/usr/bin/env python3 -from src.services import * +from src.services import Service import logging +import json +import os import colorlog +from typing import Optional, List, Dict # Initialize color logging handler = colorlog.StreamHandler() @@ -20,68 +23,163 @@ handler.setFormatter(colorlog.ColoredFormatter( logger = colorlog.getLogger(__name__) logger.setLevel(logging.INFO) logger.addHandler(handler) - - -# nginx = Service(tag="sys", name="nginx") -# minecraft = Service(tag="sys", name="minecraft", path="~/web/minecraftService") -# homepage = Service(tag="docker", name="docker", path="~/web/homepageService") -# status = Service(tag="docker", name="status", path="~/web/statusService") -# gitea = Service(tag="docker", name="gitea", path="~/web/giteaService") - -class Manager: - """Interface set of services management operations. - - Attributes: - service_list (list): List use for storing service instances. +class ServiceFactory: + """Factory for creating Service instances based on tag. + """ - - def __init__(self): - """Initialize the Manager with an empty services list.""" - self.services_list = [] - - def append_service(self, service_instance: Service) -> None: - """Append a service instance to the services list. + + @staticmethod + def create_service(tag: str, name: str, path: Optional[str] = None) -> Service: + """Create a Service instance with validation. Args: - service_instance (Service): The service instance to append. - """ - self.services_list.append(service_instance) - - def register_service(self, service_tag: str, service_name: str, service_path=None) -> Service: - """Register a new service. - - Args: - service_tag ('sys' | 'docker'): The tag that marks the service instance to use which way to deploy ('sys' or 'docker'). - service_name (str): The name of the service instance. - service_path (str): The configuration and data path of the service instance. - + tag: Service type ('sys' or 'docker') + name: Service name + path: Configuration path (required for docker services) + Returns: - service: A web service instance. + Service instance + + Raises: + ValueError: For invalid inputs """ + if tag == "docker": + if not path: + raise ValueError("Path is required for docker services") + if not os.path.exists(path): + raise ValueError(f"Invalid service path: {path}") + if not os.path.isdir(path): + raise ValueError(f"Service path must be a directory: {path}") + logger.info(f"Validated docker service path: {path}") + return Service(tag=tag, name=name, path=path) - if service_tag == "docker": - assert service_path != None, """Can not leave a empty path while you register a docker-deploy service.""" - if not os.path.exists(service_path): - raise ValueError(f"Invalid service path: {service_path}") - service = Service(tag=service_tag, name=service_name, path=service_path) - # service.code = len(self.services_list) + 1 - self.append_service(service_instance=service) + +class ServiceRepository: + """Repository for service persistence. + + """ + + def __init__(self, file_path: str = '../data/services.json'): + self.file_path = file_path + + def save(self, service: Service) -> None: + """Save a service to the repository. + + Args: + service: Service instance to save + """ + # Load existing services + services = self.load_all() + + # Add new service + services.append(service.to_dict()) + + # Save back to file + with open(self.file_path, 'w') as file: + json.dump(services, file, indent=4) + logger.info(f"Saved service to repository: {service.name}") + + def load_all(self) -> List[Dict]: + """Load all services from repository. + + Returns: + List of service dictionaries + """ + if os.path.exists(self.file_path): + try: + with open(self.file_path, 'r') as file: + return json.load(file) + except (json.JSONDecodeError, FileNotFoundError): + logger.warning("Service repository file corrupted or missing") + return [] + logger.info("Service repository file not found, starting fresh") + return [] + + +class Manager: + """Interface for services management operations. + + """ + + def __init__(self, + repository: Optional[ServiceRepository] = None, + factory: Optional[ServiceFactory] = None): + """Initialize Manager with dependencies. + + Args: + repository: Service repository instance (optional) + factory: Service factory instance (optional) + """ + self.repository = repository or ServiceRepository() + self.factory = factory or ServiceFactory() + logger.info("Manager initialized") + + def register_service(self, + service_tag: str, + service_name: str, + service_path: Optional[str] = None) -> Service: + """Register and persist a new service. + + Args: + service_tag: Service type ('sys' or 'docker') + service_name: Service name + service_path: Configuration path (required for docker) + + Returns: + Registered service instance + + Raises: + ValueError: If service registration fails + """ + try: + # Create service via factory + service = self.factory.create_service(service_tag, service_name, service_path) + + # Persist service + self.repository.save(service) + + logger.info(f"Successfully registered service: {service_name} ({service_tag})") return service - service = Service(tag=service_tag, name=service_name) - # service.code = len(self.services_list) + 1 - self.append_service(service_instance=service) - return service - - # TODO: Add the feature which can store the registered services into a JSON file whose path with ./data/services.json. - - def list_services(self) -> None: - """Method use for count the num of service instances and list the names of them. + + except ValueError as e: + logger.error(f"Service registration failed: {str(e)}") + raise + def list_services(self) -> List[Service]: + """List all registered services. + + Returns: + List of Service objects """ - - sum_of_service = 0 - service_name_list = [] - for services in self.services_list: - service_name_list.append(services._name) - sum_of_service += 1 - logger.info(f"The manager has registered {sum_of_service} services: {', '.join(service_name_list)}") + services_data = self.repository.load_all() + if not services_data: + logger.info("No services registered") + return [] + + services = [] + for s in services_data: + try: + service = Service(tag=s["tag"], name=s["name"], path=s["path"]) + services.append(service) + except Exception as e: + logger.error(f"Invalid service data: {s}, error: {str(e)}") + + service_names = [s.name for s in services] + logger.info(f"Registered services ({len(services)}): {', '.join(service_names)}") + return services + + def execute_service_operation(self, index: int) -> None: + """Execute service operation for the service at the given index. + + Args: + index: Index of the service in the list returned by list_services() + + Raises: + IndexError: If index is out of bounds + """ + services = self.list_services() + if index < 0 or index >= len(services): + raise IndexError(f"Invalid service index: {index}") + + service = services[index] + service.service_operation() diff --git a/src/services.py b/src/services.py index eabbef0..fd0cdb9 100644 --- a/src/services.py +++ b/src/services.py @@ -4,6 +4,8 @@ import subprocess import os import logging import colorlog +from abc import ABC, abstractmethod +from typing import Optional, Tuple, List, Callable # Initialize color logging handler = colorlog.StreamHandler() @@ -23,11 +25,16 @@ logger.setLevel(logging.INFO) logger.addHandler(handler) -def get_operation(): +def get_operation() -> Optional[int]: + """Get service operation from user input. + + Returns: + 0: stop, 1: restart, None: quit + """ while True: choice = input("Please select an operation (0 to stop, 1 to restart, q to quit): ").strip().lower() if choice == 'q': - return 'q' + return None try: op = int(choice) if op in (0, 1): @@ -37,80 +44,151 @@ def get_operation(): logger.warning("Invalid input, please enter 0, 1, or q.") -class Service: - """A template management for web services +class ServiceStrategy(ABC): + """Abstract base class for service operation strategies.""" + + @abstractmethod + def generate_command(self, operation: int, service_name: str, path: Optional[str] = None) -> List[str]: + """Generate command for service operation. + + Args: + operation: 0=stop, 1=restart + service_name: Name of the service + path: Service path (for docker) + + Returns: + Command list for subprocess + """ + pass + + @abstractmethod + def execute(self, command: List[str]) -> None: + """Execute the service command. + + Args: + command: Command to execute + + Raises: + subprocess.CalledProcessError: If command fails + """ + pass + +class SystemServiceStrategy(ServiceStrategy): + """Strategy for systemd services.""" + + def __init__(self, path: Optional[str] = None): + pass + + def generate_command(self, operation: int, service_name: str, path: Optional[str] = None) -> List[str]: + if operation == 0: + return ["sudo", "systemctl", "stop", service_name] + elif operation == 1: + return ["sudo", "systemctl", "restart", service_name] + raise ValueError(f"Invalid operation for system service: {operation}") + + def execute(self, command: List[str]) -> None: + subprocess.run(command, check=True) + + +class DockerServiceStrategy(ServiceStrategy): + """Strategy for docker-compose services.""" + + def __init__(self, path: str): + self.path = path + + def generate_command(self, operation: int, service_name: str, path: Optional[str] = None) -> List[str]: + if not self.path: + raise ValueError("Docker service requires a path") + + expanded_path = os.path.expanduser(self.path) + if not os.path.exists(expanded_path): + raise FileNotFoundError(f"Docker path not found: {expanded_path}") + if not os.path.isdir(expanded_path): + raise NotADirectoryError(f"Docker path must be a directory: {expanded_path}") + + if operation == 0: + return ["docker", "compose", "down"] + elif operation == 1: + return ["docker", "compose", "up", "-d"] + raise ValueError(f"Invalid operation for docker service: {operation}") + + def execute(self, command: List[str]) -> None: + expanded_path = os.path.expanduser(self.path) + if not os.path.exists(expanded_path): + raise FileNotFoundError(f"Path not found: {expanded_path}") + if not os.path.isdir(expanded_path): + raise NotADirectoryError(f"Docker path must be directory: {expanded_path}") + subprocess.run(command, check=True, cwd=expanded_path) + + +class Service: + """Management interface for web services. + Attributes: - _tag ('sys' | 'docker'): The tag that marks the service instance to use which way to deploy. - _name (str): The name of the service instance. - _path (str): The configuration and data path of the service instance. + tag: Service type ('sys' or 'docker') + name: Service name + path: Configuration path (for docker services) + strategy: Service operation strategy """ - def __init__(self, tag: str, name: str, path=None): - self._tag = tag - self._name = name - self._path = path - - def command_gen(self): - """Generate service management commands based on the service's tag. + STRATEGIES = { + 'sys': SystemServiceStrategy, + 'docker': DockerServiceStrategy + } + def __init__(self, tag: str, name: str, path: Optional[str] = None): + """Initialize a service instance. + + Args: + tag: Service type ('sys' or 'docker') + name: Service name + path: Configuration path (required for docker) + + Raises: + ValueError: For invalid tag """ + if tag not in self.STRATEGIES: + raise ValueError(f"Invalid service tag: {tag}") + + self.tag = tag + self.name = name + self.path = path + self.strategy = self.STRATEGIES[tag](path=self.path) + logger.info(f"Initialized {tag} service: {name}") + + def to_dict(self) -> dict: + """Return dictionary representation of the service.""" + return { + "tag": self.tag, + "name": self.name, + "path": self.path + } - system_services_command = "sudo systemctl" - docker_services_command = f"cd {os.path.expanduser(f'{self._path}')} && docker compose" - if self._tag == "sys": - return system_services_command - elif self._tag == "docker": - return docker_services_command - else: - raise ValueError(f"The service tag {self._tag} was not included.") - - def service_operation(self): - """Manage the service based on user input. - + def service_operation(self, operation_getter: Callable[[], Optional[int]] = get_operation) -> None: + """Perform service operation based on user input. + + Args: + operation_getter: Function to get operation choice """ - - operation = get_operation() - if operation == 'q': - logger.info("User chose to quit the service management.") + operation = operation_getter() + if operation is None: + logger.info("Operation cancelled by user") return - command = self.command_gen() - full_command = None # Initialize full_command - - if self._tag == "sys": - if operation == 0: - # Stop the service - full_command = f"{command} stop {self._name}" - logger.info(f"Stopping service: {self._name}") - elif operation == 1: - # Restart the service - full_command = f"{command} restart {self._name}" - logger.info(f"Restarting service: {self._name}") - else: - logger.warning("Invalid operation; no service management executed.") - return # Exit if the operation is invalid - if self._tag == "docker": - if operation == 0: - # Stop the service - full_command = f"{command} down" - logger.info(f"Stopping service: {self._name}") - elif operation == 1: - # Restart the service - full_command = f"{command} up -d" - logger.info(f"Restarting service: {self._name}") - else: - logger.warning("Invalid operation; no service management executed.") - return # Exit if the operation is invalid - - if full_command: # Ensure full_command is defined - try: - subprocess.run(full_command, check=True, shell=True) - logger.info(f"Service {self._name} operation completed successfully.") - except subprocess.CalledProcessError as e: - logger.error(f"Failed to manage service {self._name}: {e}") + try: + # Generate and execute command + command = self.strategy.generate_command(operation, self.name, self.path) + logger.info(f"Executing: {' '.join(command)}") + self.strategy.execute(command) + logger.info(f"Service {self.name} operation completed") + except (ValueError, FileNotFoundError, NotADirectoryError, subprocess.CalledProcessError) as e: + logger.error(f"Service operation failed: {str(e)}") -# Example call +# Example usage if __name__ == "__main__": - service = Service(tag="docker", name="homepage", path="~/web/homepageService") - service.service_operation() + try: + service = Service(tag="docker", name="homepage", path="~/projects/webservices/homepage") + service.service_operation() + except Exception as e: + logger.error(f"Unhandled exception: {str(e)}") diff --git a/test/main.py b/test/main.py deleted file mode 100755 index 4bca067..0000000 --- a/test/main.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 - -from src.manager import * -import logging -import colorlog -import sys - -def initialize_logger(): - """Initialize and return a color logger.""" - handler = colorlog.StreamHandler() - handler.setFormatter(colorlog.ColoredFormatter( - '%(log_color)s%(asctime)s - %(levelname)s - %(message)s', - log_colors={ - 'DEBUG': 'cyan', - 'INFO': 'green', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'bold_red', - } - )) - - logger = colorlog.getLogger(__name__) - logger.setLevel(logging.INFO) - logger.addHandler(handler) - return logger - -def initialize_service_manager(): - """Initialize and return the service manager.""" - return Manager() - -logger = initialize_logger() -service_manager = initialize_service_manager() - -def start_menu(): - logger.info(f"=======================================") - logger.info(f"========== Services Manager ===========") - logger.info(f"=======================================") - logger.info(f"Please choose a operation to carry out:") - logger.info(f"=====> 1. Register a new service <=====") - logger.info(f"=====> 2. Manage exsist services <=====") - logger.info(f"=====> c. Count the number of services") - logger.info(f"=====> q. Quit ") - -def main_menu(): - """Display the main menu and handle user input.""" - - while True: - start_menu() - - start_choice = input("Enter your choice: ").strip() - if start_choice == '1': - service_tag = input("Please input the deploy way of service you want to register('docker' | 'sys'): ") - service_name = input("Please input the name of the service: ") - if service_tag == "docker": - service_path = input("Please input the config path of the service where 'docker-compose.yml' located: ") - service_manager.register_service(service_tag, service_name, service_path) - else: - service_manager.register_service(service_tag, service_name) - if start_choice == '2': - if not service_manager.services_list: - logger.error("No services registered.") - continue - while True: - logger.info(f"=====> Exsist Services Management <=====") - service_code = 0 - for service in service_manager.services_list: - logger.info(f"=====> {service_code}: {service._name}") - service_code += 1 - # logger.info("=====> a. Select All") - logger.info("=====> q. Quit") - - manage_choice = input("Please select a choice to carry out: ").strip() - if manage_choice == 'q': - logger.info("Exit main manager") - break - # if manage_choice == 'a': - # pass - try: - code = int(manage_choice) - if 0 <= code < len(service_manager.services_list): - service_manager.services_list[code].service_operation() - else: - logger.error("Invalid service code.") - except ValueError: - logger.error("Please enter a valid number.") - if start_choice == 'c': - service_manager.list_services() - if start_choice == 'q': - logger.info(f"Exit the manager process") - break - -if __name__ == '__main__': - sys.exit(main_menu()) diff --git a/test/test_manager.py b/test/test_manager.py new file mode 100644 index 0000000..3d35dac --- /dev/null +++ b/test/test_manager.py @@ -0,0 +1,143 @@ +import unittest +from unittest.mock import patch, MagicMock +from src.manager import ServiceFactory, ServiceRepository, Manager +import os +import tempfile + +class TestServiceFactory(unittest.TestCase): + def test_create_valid_service(self): + """测试创建有效的服务""" + factory = ServiceFactory() + service = factory.create_service("sys", "nginx") + self.assertEqual(service.tag, "sys") + self.assertEqual(service.name, "nginx") + self.assertIsNone(service.path) + + def test_create_invalid_tag(self): + """测试创建无效标签的服务""" + factory = ServiceFactory() + with self.assertRaises(ValueError): + factory.create_service("invalid", "invalid_service") + +class TestServiceRepository(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + self.repo_path = os.path.join(self.temp_dir.name, "services.json") + self.repo = ServiceRepository(self.repo_path) + + def tearDown(self): + self.temp_dir.cleanup() + + def test_save_and_load(self): + """测试服务保存和加载""" + # 创建模拟服务 + mock_service = MagicMock() + mock_service.to_dict.return_value = { + "tag": "sys", + "name": "nginx", + "path": None + } + + # 保存服务 + self.repo.save(mock_service) + + # 加载服务 + services = self.repo.load_all() + + # 验证加载结果 + self.assertEqual(len(services), 1) + self.assertEqual(services[0]["tag"], "sys") + self.assertEqual(services[0]["name"], "nginx") + + def test_load_invalid_file(self): + """测试加载无效JSON文件""" + with open(self.repo_path, "w") as f: + f.write("invalid json") + + services = self.repo.load_all() + self.assertEqual(len(services), 0) + +class TestManager(unittest.TestCase): + @patch("src.manager.ServiceRepository") + @patch("src.manager.ServiceFactory") + def test_register_service(self, mock_factory, mock_repo): + """测试服务注册""" + # 准备模拟对象 + mock_service = MagicMock() + mock_factory.return_value.create_service.return_value = mock_service + mock_repo_instance = MagicMock() + + # 创建管理器 + manager = Manager(mock_repo.return_value) + + # 注册服务 + service = manager.register_service("sys", "nginx") + + # 验证调用 + mock_factory.return_value.create_service.assert_called_with("sys", "nginx", None) + mock_repo.return_value.save.assert_called_with(mock_service) + self.assertEqual(service, mock_service) + + @patch("src.manager.ServiceRepository") + def test_list_services(self, mock_repo): + """测试服务列表""" + # 准备模拟服务 + mock_service1 = MagicMock() + mock_service1.name = "nginx" + mock_service2 = MagicMock() + mock_service2.name = "postgres" + + # 创建管理器 + manager = Manager(mock_repo.return_value) + mock_repo.return_value.load_all.return_value = [ + {"tag": "sys", "name": "nginx", "path": None}, + {"tag": "docker", "name": "postgres", "path": "/path/to/docker"} + ] + + # 获取服务列表 + services = manager.list_services() + + # 验证结果 + # 修复预期服务数量 + self.assertEqual(len(services), 2) + self.assertEqual(services[0].name, "nginx") + self.assertEqual(services[1].name, "postgres") + + @patch("src.manager.Service") + @patch("src.services.get_operation", return_value=1) + def test_execute_service_operation(self, mock_get_operation, mock_service, mock_repo): + """测试服务操作执行""" + # 准备模拟对象 + manager = Manager(mock_repo.return_value) + mock_service_instance = MagicMock() + mock_service.return_value = mock_service_instance + + # 模拟服务列表 + with patch.object(manager, 'list_services', return_value=[mock_service_instance]): + # 执行服务操作 + manager.execute_service_operation(0) # 执行索引0的服务 + + # 验证调用 + mock_service_instance.service_operation.assert_called_once() + + def test_register_invalid_service(self): + """测试注册无效服务""" + manager = Manager(MagicMock()) + with self.assertRaises(ValueError): + manager.register_service("invalid", "invalid_service") + + @patch("src.manager.logger") + def test_execute_service_operation_invalid_index(self, mock_repo, mock_logger): + """测试无效服务索引""" + manager = Manager(mock_repo.return_value) + + # 模拟空服务列表 + with patch.object(manager, 'list_services', return_value=[]): + with self.assertRaises(IndexError): + manager.execute_service_operation(0) + + mock_logger.error.assert_called() + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/test/test_services.py b/test/test_services.py new file mode 100644 index 0000000..87b17d5 --- /dev/null +++ b/test/test_services.py @@ -0,0 +1,150 @@ +import unittest +from unittest.mock import patch, MagicMock +from src.services import Service, SystemServiceStrategy, DockerServiceStrategy, get_operation +import os +import subprocess + +class TestService(unittest.TestCase): + def test_service_initialization(self): + """测试服务初始化""" + service = Service(tag="sys", name="nginx") + self.assertEqual(service.tag, "sys") + self.assertEqual(service.name, "nginx") + self.assertIsNone(service.path) + + def test_docker_service_initialization(self): + """测试Docker服务初始化""" + service = Service(tag="docker", name="homepage", path="/path/to/docker") + self.assertEqual(service.tag, "docker") + self.assertEqual(service.name, "homepage") + self.assertEqual(service.path, "/path/to/docker") + + def test_invalid_tag(self): + """测试无效标签""" + with self.assertRaises(ValueError): + Service(tag="invalid", name="invalid") + + def test_to_dict(self): + """测试字典转换""" + service = Service(tag="sys", name="nginx") + result = service.to_dict() + self.assertEqual(result, { + "tag": "sys", + "name": "nginx", + "path": None + }) + +class TestSystemServiceStrategy(unittest.TestCase): + def test_generate_stop_command(self): + """测试生成停止命令""" + strategy = SystemServiceStrategy() + command = strategy.generate_command(0, "nginx") + self.assertEqual(command, ["sudo", "systemctl", "stop", "nginx"]) + + def test_generate_restart_command(self): + """测试生成重启命令""" + strategy = SystemServiceStrategy() + command = strategy.generate_command(1, "nginx") + self.assertEqual(command, ["sudo", "systemctl", "restart", "nginx"]) + + def test_invalid_operation(self): + """测试无效操作""" + strategy = SystemServiceStrategy() + with self.assertRaises(ValueError): + strategy.generate_command(2, "nginx") + + @patch("subprocess.run") + def test_execute_command(self, mock_run): + """测试命令执行""" + strategy = SystemServiceStrategy() + command = ["sudo", "systemctl", "stop", "nginx"] + strategy.execute(command) + mock_run.assert_called_with(command, check=True) + +class TestDockerServiceStrategy(unittest.TestCase): + def setUp(self): + self.path = "/path/to/docker" + self.strategy = DockerServiceStrategy(self.path) + os.path.exists = MagicMock(return_value=True) + os.path.isdir = MagicMock(return_value=True) + + def test_generate_down_command(self): + """测试生成停止命令""" + command = self.strategy.generate_command(0, "homepage") + self.assertEqual(command, ["docker", "compose", "down"]) + + def test_generate_up_command(self): + """测试生成启动命令""" + command = self.strategy.generate_command(1, "homepage") + self.assertEqual(command, ["docker", "compose", "up", "-d"]) + + def test_missing_path(self): + """测试路径为空字符串""" + strategy = DockerServiceStrategy("") + with self.assertRaises(ValueError): + strategy.generate_command(0, "homepage") + + @patch("os.path.exists", return_value=False) + def test_path_not_found(self, mock_exists): + """测试路径不存在""" + with self.assertRaises(FileNotFoundError): + self.strategy.generate_command(0, "homepage") + + @patch("subprocess.run") + @patch("os.path.exists", return_value=True) + @patch("os.path.isdir", return_value=True) + def test_execute_command(self, mock_isdir, mock_exists, mock_run): + """测试命令执行""" + command = ["docker", "compose", "down"] + self.strategy.execute(command) + mock_run.assert_called_with(command, check=True, cwd="/path/to/docker") + +class TestServiceOperation(unittest.TestCase): + @patch("builtins.input", side_effect=["0"]) + def test_get_operation_stop(self, mock_input): + """测试获取停止操作""" + result = get_operation() + self.assertEqual(result, 0) + + @patch("builtins.input", side_effect=["1"]) + def test_get_operation_restart(self, mock_input): + """测试获取重启操作""" + result = get_operation() + self.assertEqual(result, 1) + + @patch("builtins.input", side_effect=["q"]) + def test_get_operation_quit(self, mock_input): + """测试退出操作""" + result = get_operation() + self.assertIsNone(result) + + @patch("builtins.input", side_effect=["invalid", "0"]) + @patch("src.services.logger.warning") + def test_invalid_input(self, mock_warning, mock_input): + """测试无效输入""" + result = get_operation() + self.assertEqual(result, 0) + mock_warning.assert_called() + + @patch("src.services.get_operation", return_value=0) + @patch("src.services.SystemServiceStrategy.generate_command", return_value=["sudo", "systemctl", "stop", "nginx"]) + @patch("src.services.SystemServiceStrategy.execute") + @patch("src.services.logger.info") + @patch("src.services.SystemServiceStrategy.execute") + @patch("src.services.SystemServiceStrategy.generate_command") + @patch("src.services.get_operation", return_value=0) + def test_service_operation(self, mock_get_operation, mock_generate, mock_execute, mock_info): + """测试服务操作流程""" + service = Service(tag="sys", name="nginx") + service.service_operation() + + # 修复:generate_command 需要 self 参数 + mock_generate.assert_called_with(0, "nginx", None) + mock_execute.assert_called_with(["sudo", "systemctl", "stop", "nginx"]) + mock_info.assert_any_call("Executing: sudo systemctl stop nginx") + mock_info.assert_any_call("Service nginx operation completed") + mock_info.assert_any_call("Executing: sudo systemctl stop nginx") + mock_info.assert_any_call("Service nginx operation completed") + +if __name__ == "__main__": + unittest.main() \ No newline at end of file