diff --git a/cortex/__init__.py b/cortex/__init__.py index 57abaed..1f824b2 100644 --- a/cortex/__init__.py +++ b/cortex/__init__.py @@ -1,2 +1,6 @@ from .cli import main +from .packages import PackageManager, PackageManagerType + __version__ = "0.1.0" + +__all__ = ["main", "PackageManager", "PackageManagerType"] diff --git a/cortex/packages.py b/cortex/packages.py new file mode 100644 index 0000000..a846cff --- /dev/null +++ b/cortex/packages.py @@ -0,0 +1,453 @@ +#!/usr/bin/env python3 +""" +Intelligent Package Manager Wrapper for Cortex Linux + +Translates natural language requests into apt/yum package manager commands. +Supports common software installations, development tools, and libraries. +""" + +import re +import subprocess +import platform +from typing import List, Dict, Optional, Tuple, Set +from enum import Enum + + +class PackageManagerType(Enum): + """Supported package manager types.""" + APT = "apt" # Ubuntu/Debian + YUM = "yum" # RHEL/CentOS/Fedora (older) + DNF = "dnf" # RHEL/CentOS/Fedora (newer) + + +class PackageManager: + """ + Intelligent wrapper that translates natural language into package manager commands. + + Example: + pm = PackageManager() + commands = pm.parse("install python with data science libraries") + # Returns: ["apt install python3 python3-pip python3-numpy python3-pandas python3-scipy"] + """ + + def __init__(self, pm_type: Optional[PackageManagerType] = None): + """ + Initialize the package manager. + + Args: + pm_type: Package manager type (auto-detected if None) + """ + self.pm_type = pm_type or self._detect_package_manager() + self.package_mappings = self._build_package_mappings() + self.action_patterns = self._build_action_patterns() + + def _detect_package_manager(self) -> PackageManagerType: + """Detect the package manager based on the system.""" + try: + # Check for apt + result = subprocess.run( + ["which", "apt"], + capture_output=True, + text=True, + timeout=2 + ) + if result.returncode == 0: + return PackageManagerType.APT + + # Check for dnf (preferred over yum on newer systems) + result = subprocess.run( + ["which", "dnf"], + capture_output=True, + text=True, + timeout=2 + ) + if result.returncode == 0: + return PackageManagerType.DNF + + # Check for yum + result = subprocess.run( + ["which", "yum"], + capture_output=True, + text=True, + timeout=2 + ) + if result.returncode == 0: + return PackageManagerType.YUM + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + # Default to apt (most common) + return PackageManagerType.APT + + def _build_action_patterns(self) -> Dict[str, List[str]]: + """Build regex patterns for common actions.""" + return { + "install": [ + r"\binstall\b", + r"\bsetup\b", + r"\bget\b", + r"\badd\b", + r"\bfetch\b", + r"\bdownload\b", + ], + "remove": [ + r"\bremove\b", + r"\buninstall\b", + r"\bdelete\b", + r"\bpurge\b", + ], + "update": [ + r"\bupdate\b", + r"\bupgrade\b", + r"\brefresh\b", + ], + "search": [ + r"\bsearch\b", + r"\bfind\b", + r"\blookup\b", + ], + } + + def _build_package_mappings(self) -> Dict[str, Dict[str, List[str]]]: + """ + Build comprehensive package mappings for common software requests. + Maps natural language terms to actual package names for apt/yum. + """ + return { + # Python and development tools + "python": { + "apt": ["python3", "python3-pip", "python3-venv"], + "yum": ["python3", "python3-pip"], + }, + "python development": { + "apt": ["python3-dev", "python3-pip", "build-essential"], + "yum": ["python3-devel", "python3-pip", "gcc", "gcc-c++", "make"], + }, + "python data science": { + "apt": ["python3", "python3-pip", "python3-numpy", "python3-pandas", + "python3-scipy", "python3-matplotlib", "python3-jupyter"], + "yum": ["python3", "python3-pip", "python3-numpy", "python3-pandas", + "python3-scipy", "python3-matplotlib"], + }, + "python machine learning": { + "apt": ["python3", "python3-pip", "python3-numpy", "python3-scipy", + "python3-scikit-learn", "python3-tensorflow", "python3-keras"], + "yum": ["python3", "python3-pip", "python3-numpy", "python3-scipy"], + }, + + # Web development + "web development": { + "apt": ["nodejs", "npm", "git", "curl", "wget"], + "yum": ["nodejs", "npm", "git", "curl", "wget"], + }, + "nodejs": { + "apt": ["nodejs", "npm"], + "yum": ["nodejs", "npm"], + }, + "docker": { + "apt": ["docker.io", "docker-compose"], + "yum": ["docker", "docker-compose"], + }, + "nginx": { + "apt": ["nginx"], + "yum": ["nginx"], + }, + "apache": { + "apt": ["apache2"], + "yum": ["httpd"], + }, + + # Database + "mysql": { + "apt": ["mysql-server", "mysql-client"], + "yum": ["mysql-server", "mysql"], + }, + "postgresql": { + "apt": ["postgresql", "postgresql-contrib"], + "yum": ["postgresql-server", "postgresql"], + }, + "mongodb": { + "apt": ["mongodb"], + "yum": ["mongodb-server", "mongodb"], + }, + "redis": { + "apt": ["redis-server"], + "yum": ["redis"], + }, + + # Development tools + "build tools": { + "apt": ["build-essential", "gcc", "g++", "make", "cmake"], + "yum": ["gcc", "gcc-c++", "make", "cmake"], + }, + "git": { + "apt": ["git"], + "yum": ["git"], + }, + "vim": { + "apt": ["vim"], + "yum": ["vim"], + }, + "emacs": { + "apt": ["emacs"], + "yum": ["emacs"], + }, + "curl": { + "apt": ["curl"], + "yum": ["curl"], + }, + "wget": { + "apt": ["wget"], + "yum": ["wget"], + }, + + # System utilities + "system monitoring": { + "apt": ["htop", "iotop", "nethogs", "sysstat"], + "yum": ["htop", "iotop", "nethogs", "sysstat"], + }, + "network tools": { + "apt": ["net-tools", "iputils-ping", "tcpdump", "wireshark"], + "yum": ["net-tools", "iputils", "tcpdump", "wireshark"], + }, + "compression tools": { + "apt": ["zip", "unzip", "gzip", "bzip2", "xz-utils"], + "yum": ["zip", "unzip", "gzip", "bzip2", "xz"], + }, + + # Media and graphics + "image tools": { + "apt": ["imagemagick", "ffmpeg", "libjpeg-dev", "libpng-dev"], + "yum": ["ImageMagick", "ffmpeg", "libjpeg-turbo-devel", "libpng-devel"], + }, + "video tools": { + "apt": ["ffmpeg", "vlc"], + "yum": ["ffmpeg", "vlc"], + }, + + # Security tools + "security tools": { + "apt": ["ufw", "fail2ban", "openssh-server", "ssl-cert"], + "yum": ["firewalld", "fail2ban", "openssh-server"], + }, + "firewall": { + "apt": ["ufw"], + "yum": ["firewalld"], + }, + + # Cloud and containers + "kubernetes": { + "apt": ["kubectl"], + "yum": ["kubectl"], + }, + "terraform": { + "apt": ["terraform"], + "yum": ["terraform"], + }, + + # Text processing + "text editors": { + "apt": ["vim", "nano", "emacs"], + "yum": ["vim", "nano", "emacs"], + }, + + # Version control + "version control": { + "apt": ["git", "subversion"], + "yum": ["git", "subversion"], + }, + } + + def _normalize_text(self, text: str) -> str: + """Normalize input text for matching.""" + # Convert to lowercase and remove extra whitespace + text = text.lower().strip() + # Remove common punctuation + text = re.sub(r'[^\w\s]', ' ', text) + # Normalize whitespace + text = re.sub(r'\s+', ' ', text) + # Final strip to remove any leading/trailing whitespace + return text.strip() + + def _extract_action(self, text: str) -> str: + """Extract the action (install, remove, etc.) from the text.""" + normalized = self._normalize_text(text) + + for action, patterns in self.action_patterns.items(): + for pattern in patterns: + if re.search(pattern, normalized): + return action + + # Default to install if no action specified + return "install" + + def _find_matching_packages(self, text: str) -> List[str]: + """ + Find matching packages based on natural language input. + Returns list of package names. + """ + normalized = self._normalize_text(text) + matched_packages = set() + + # Get the appropriate package manager key + pm_key = "apt" if self.pm_type == PackageManagerType.APT else "yum" + + # Handle Python with priority - check most specific first + if "python" in normalized: + if "machine learning" in normalized or "ml" in normalized: + matched_packages.update(self.package_mappings["python machine learning"].get(pm_key, [])) + elif "data science" in normalized: + matched_packages.update(self.package_mappings["python data science"].get(pm_key, [])) + elif "development" in normalized or "dev" in normalized: + matched_packages.update(self.package_mappings["python development"].get(pm_key, [])) + else: + # Basic python - only include basic packages + matched_packages.update(self.package_mappings["python"].get(pm_key, [])) + + # Handle other specific combinations + if "web" in normalized and "development" in normalized: + matched_packages.update(self.package_mappings["web development"].get(pm_key, [])) + + if "build" in normalized and "tools" in normalized: + matched_packages.update(self.package_mappings["build tools"].get(pm_key, [])) + + if "system" in normalized and "monitoring" in normalized: + matched_packages.update(self.package_mappings["system monitoring"].get(pm_key, [])) + + if "network" in normalized and "tools" in normalized: + matched_packages.update(self.package_mappings["network tools"].get(pm_key, [])) + + if "security" in normalized and "tools" in normalized: + matched_packages.update(self.package_mappings["security tools"].get(pm_key, [])) + + if "text" in normalized and "editor" in normalized: + matched_packages.update(self.package_mappings["text editors"].get(pm_key, [])) + + if "version" in normalized and "control" in normalized: + matched_packages.update(self.package_mappings["version control"].get(pm_key, [])) + + if "compression" in normalized and "tools" in normalized: + matched_packages.update(self.package_mappings["compression tools"].get(pm_key, [])) + + if "image" in normalized and "tools" in normalized: + matched_packages.update(self.package_mappings["image tools"].get(pm_key, [])) + + # Handle exact key matches for multi-word categories + for key, packages in self.package_mappings.items(): + # Skip single-word software (handled separately) and Python (handled above) + if " " in key and key not in ["python", "python development", "python data science", "python machine learning"]: + if key in normalized: + matched_packages.update(packages.get(pm_key, [])) + + # Handle individual software packages (only if not already matched above) + # Check for exact key matches for single-word software + single_software = { + "docker", "nginx", "apache", "mysql", "postgresql", "mongodb", + "redis", "git", "vim", "emacs", "curl", "wget", "nodejs", + "kubernetes", "terraform" + } + + for software in single_software: + # Only match if it's a standalone word or exact match + if software in normalized: + # Check if it's part of a larger phrase (e.g., "docker-compose" contains "docker") + # but we want to match "docker" as a standalone request + words = normalized.split() + if software in words or normalized == software or normalized.startswith(software + " ") or normalized.endswith(" " + software): + if software in self.package_mappings: + matched_packages.update(self.package_mappings[software].get(pm_key, [])) + + return sorted(list(matched_packages)) + + def parse(self, request: str) -> List[str]: + """ + Parse natural language request and return package manager commands. + + Args: + request: Natural language request (e.g., "install python with data science libraries") + + Returns: + List of package manager commands + + Raises: + ValueError: If request cannot be parsed or no packages found + """ + if not request or not request.strip(): + raise ValueError("Empty request provided") + + action = self._extract_action(request) + packages = self._find_matching_packages(request) + + if not packages: + raise ValueError(f"No matching packages found for: {request}") + + # Build command based on package manager type + if self.pm_type == PackageManagerType.APT: + if action == "install": + return [f"apt install -y {' '.join(packages)}"] + elif action == "remove": + return [f"apt remove -y {' '.join(packages)}"] + elif action == "update": + return [f"apt update", f"apt upgrade -y {' '.join(packages)}"] + elif action == "search": + return [f"apt search {' '.join(packages)}"] + + elif self.pm_type in (PackageManagerType.YUM, PackageManagerType.DNF): + pm_cmd = "yum" if self.pm_type == PackageManagerType.YUM else "dnf" + if action == "install": + return [f"{pm_cmd} install -y {' '.join(packages)}"] + elif action == "remove": + return [f"{pm_cmd} remove -y {' '.join(packages)}"] + elif action == "update": + return [f"{pm_cmd} update -y {' '.join(packages)}"] + elif action == "search": + return [f"{pm_cmd} search {' '.join(packages)}"] + + return [] + + def get_package_info(self, package_name: str) -> Optional[Dict[str, str]]: + """ + Get information about a specific package. + + Args: + package_name: Name of the package + + Returns: + Dictionary with package information or None if not found + """ + try: + if self.pm_type == PackageManagerType.APT: + result = subprocess.run( + ["apt-cache", "show", package_name], + capture_output=True, + text=True, + timeout=10 + ) + if result.returncode == 0: + info = {} + for line in result.stdout.split('\n'): + if ':' in line: + key, value = line.split(':', 1) + info[key.strip()] = value.strip() + return info + + elif self.pm_type in (PackageManagerType.YUM, PackageManagerType.DNF): + pm_cmd = "yum" if self.pm_type == PackageManagerType.YUM else "dnf" + result = subprocess.run( + [pm_cmd, "info", package_name], + capture_output=True, + text=True, + timeout=10 + ) + if result.returncode == 0: + info = {} + for line in result.stdout.split('\n'): + if ':' in line: + key, value = line.split(':', 1) + info[key.strip()] = value.strip() + return info + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + return None + diff --git a/test/test_packages.py b/test/test_packages.py new file mode 100644 index 0000000..48735b9 --- /dev/null +++ b/test/test_packages.py @@ -0,0 +1,366 @@ +#!/usr/bin/env python3 +""" +Unit tests for the intelligent package manager wrapper. +""" + +import sys +import os +import unittest +from unittest.mock import patch, MagicMock + +# Add project root to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from cortex.packages import PackageManager, PackageManagerType + + +class TestPackageManager(unittest.TestCase): + """Test cases for PackageManager class.""" + + def setUp(self): + """Set up test fixtures.""" + # Mock package manager detection to use apt for consistent testing + with patch('cortex.packages.subprocess.run') as mock_run: + mock_run.return_value = MagicMock(returncode=0) + self.pm = PackageManager(pm_type=PackageManagerType.APT) + + def test_python_installation(self): + """Test basic Python installation request.""" + commands = self.pm.parse("install python") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + self.assertIn("python3", commands[0]) + self.assertIn("apt install", commands[0]) + + def test_python_development_tools(self): + """Test Python development tools installation.""" + commands = self.pm.parse("install python development tools") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + self.assertIn("python3-dev", cmd) + self.assertIn("build-essential", cmd) + + def test_python_data_science(self): + """Test Python data science libraries installation.""" + commands = self.pm.parse("install python with data science libraries") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + self.assertIn("python3", cmd) + self.assertIn("python3-numpy", cmd) + self.assertIn("python3-pandas", cmd) + self.assertIn("python3-scipy", cmd) + + def test_python_machine_learning(self): + """Test Python machine learning libraries.""" + commands = self.pm.parse("install python machine learning libraries") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + self.assertIn("python3", cmd) + self.assertIn("python3-numpy", cmd) + self.assertIn("python3-scipy", cmd) + + def test_web_development(self): + """Test web development tools installation.""" + commands = self.pm.parse("install web development tools") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + self.assertIn("nodejs", cmd) + self.assertIn("npm", cmd) + self.assertIn("git", cmd) + + def test_docker_installation(self): + """Test Docker installation.""" + commands = self.pm.parse("install docker") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + self.assertIn("docker.io", cmd) + self.assertIn("docker-compose", cmd) + + def test_database_installations(self): + """Test various database installations.""" + # MySQL + commands = self.pm.parse("install mysql") + self.assertIsInstance(commands, list) + self.assertIn("mysql-server", commands[0]) + + # PostgreSQL + commands = self.pm.parse("install postgresql") + self.assertIsInstance(commands, list) + self.assertIn("postgresql", commands[0]) + + # Redis + commands = self.pm.parse("install redis") + self.assertIsInstance(commands, list) + self.assertIn("redis-server", commands[0]) + + def test_build_tools(self): + """Test build tools installation.""" + commands = self.pm.parse("install build tools") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + self.assertIn("build-essential", cmd) + self.assertIn("gcc", cmd) + self.assertIn("make", cmd) + + def test_system_monitoring(self): + """Test system monitoring tools.""" + commands = self.pm.parse("install system monitoring tools") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + self.assertIn("htop", cmd) + self.assertIn("iotop", cmd) + + def test_network_tools(self): + """Test network tools installation.""" + commands = self.pm.parse("install network tools") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + self.assertIn("net-tools", cmd) + self.assertIn("tcpdump", cmd) + + def test_security_tools(self): + """Test security tools installation.""" + commands = self.pm.parse("install security tools") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + self.assertIn("ufw", cmd) + self.assertIn("fail2ban", cmd) + + def test_nginx_installation(self): + """Test Nginx web server installation.""" + commands = self.pm.parse("install nginx") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + self.assertIn("nginx", commands[0]) + + def test_apache_installation(self): + """Test Apache web server installation.""" + commands = self.pm.parse("install apache") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + self.assertIn("apache2", commands[0]) + + def test_git_installation(self): + """Test Git installation.""" + commands = self.pm.parse("install git") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + self.assertIn("git", commands[0]) + + def test_text_editors(self): + """Test text editors installation.""" + commands = self.pm.parse("install text editors") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + self.assertIn("vim", cmd) + self.assertIn("nano", cmd) + + def test_version_control(self): + """Test version control tools.""" + commands = self.pm.parse("install version control") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + self.assertIn("git", cmd) + self.assertIn("subversion", cmd) + + def test_compression_tools(self): + """Test compression tools installation.""" + commands = self.pm.parse("install compression tools") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + self.assertIn("zip", cmd) + self.assertIn("unzip", cmd) + self.assertIn("gzip", cmd) + + def test_image_tools(self): + """Test image processing tools.""" + commands = self.pm.parse("install image tools") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + self.assertIn("imagemagick", cmd) + self.assertIn("ffmpeg", cmd) + + def test_kubernetes_tools(self): + """Test Kubernetes tools installation.""" + commands = self.pm.parse("install kubernetes") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + self.assertIn("kubectl", commands[0]) + + def test_remove_action(self): + """Test package removal.""" + commands = self.pm.parse("remove python") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + self.assertIn("apt remove", commands[0]) + + def test_update_action(self): + """Test package update.""" + commands = self.pm.parse("update python") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + # Update should include both update and upgrade commands for apt + self.assertTrue(any("apt update" in cmd for cmd in commands)) + + def test_search_action(self): + """Test package search.""" + commands = self.pm.parse("search python") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + self.assertIn("apt search", commands[0]) + + def test_empty_request(self): + """Test that empty request raises ValueError.""" + with self.assertRaises(ValueError): + self.pm.parse("") + + def test_unknown_package(self): + """Test that unknown packages raise ValueError.""" + with self.assertRaises(ValueError): + self.pm.parse("install xyzabc123unknownpackage") + + def test_case_insensitive(self): + """Test that parsing is case insensitive.""" + commands1 = self.pm.parse("INSTALL PYTHON") + commands2 = self.pm.parse("install python") + self.assertEqual(commands1, commands2) + + def test_yum_package_manager(self): + """Test YUM package manager commands.""" + pm_yum = PackageManager(pm_type=PackageManagerType.YUM) + commands = pm_yum.parse("install python") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + self.assertIn("yum install", commands[0]) + # YUM should use different package names + self.assertIn("python3", commands[0]) + + def test_dnf_package_manager(self): + """Test DNF package manager commands.""" + pm_dnf = PackageManager(pm_type=PackageManagerType.DNF) + commands = pm_dnf.parse("install python") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + self.assertIn("dnf install", commands[0]) + + def test_yum_apache_package_name(self): + """Test that YUM uses correct package name for Apache.""" + pm_yum = PackageManager(pm_type=PackageManagerType.YUM) + commands = pm_yum.parse("install apache") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + # YUM uses httpd, not apache2 + self.assertIn("httpd", commands[0]) + + def test_package_name_variations(self): + """Test that package name variations are handled.""" + # Test different ways to request Python + commands1 = self.pm.parse("install python") + commands2 = self.pm.parse("setup python3") + commands3 = self.pm.parse("get python") + + # All should result in similar commands + self.assertTrue(all("python3" in cmd for cmd in commands1)) + self.assertTrue(all("python3" in cmd for cmd in commands2)) + self.assertTrue(all("python3" in cmd for cmd in commands3)) + + def test_multiple_software_requests(self): + """Test requests that match multiple software categories.""" + commands = self.pm.parse("install python and docker and git") + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + # Should include packages from multiple categories + self.assertIn("python3", cmd) + self.assertIn("docker", cmd) + self.assertIn("git", cmd) + + def test_normalize_text(self): + """Test text normalization.""" + normalized = self.pm._normalize_text(" INSTALL Python!!! ") + self.assertEqual(normalized, "install python") + + def test_extract_action(self): + """Test action extraction.""" + self.assertEqual(self.pm._extract_action("install python"), "install") + self.assertEqual(self.pm._extract_action("remove docker"), "remove") + self.assertEqual(self.pm.parse("setup git")[0], "apt install -y git") + + @patch('cortex.packages.subprocess.run') + def test_get_package_info_apt(self, mock_run): + """Test getting package info for apt.""" + mock_run.return_value = MagicMock( + returncode=0, + stdout="Package: python3\nVersion: 3.10.0\nDescription: Python interpreter" + ) + info = self.pm.get_package_info("python3") + self.assertIsNotNone(info) + self.assertIn("Package", info) + + @patch('cortex.packages.subprocess.run') + def test_get_package_info_yum(self, mock_run): + """Test getting package info for yum.""" + pm_yum = PackageManager(pm_type=PackageManagerType.YUM) + mock_run.return_value = MagicMock( + returncode=0, + stdout="Name: python3\nVersion: 3.10.0\nDescription: Python interpreter" + ) + info = pm_yum.get_package_info("python3") + self.assertIsNotNone(info) + + def test_comprehensive_software_requests(self): + """Test 20+ common software requests as per requirements.""" + test_cases = [ + ("install python", ["python3"]), + ("install python development tools", ["python3-dev", "build-essential"]), + ("install python with data science libraries", ["python3-numpy", "python3-pandas"]), + ("install docker", ["docker.io"]), + ("install mysql", ["mysql-server"]), + ("install postgresql", ["postgresql"]), + ("install nginx", ["nginx"]), + ("install apache", ["apache2"]), + ("install git", ["git"]), + ("install nodejs", ["nodejs"]), + ("install redis", ["redis-server"]), + ("install build tools", ["build-essential"]), + ("install system monitoring", ["htop"]), + ("install network tools", ["net-tools"]), + ("install security tools", ["ufw"]), + ("install text editors", ["vim"]), + ("install version control", ["git"]), + ("install compression tools", ["zip"]), + ("install image tools", ["imagemagick"]), + ("install kubernetes", ["kubectl"]), + ("install web development", ["nodejs", "npm"]), + ("install python machine learning", ["python3-numpy"]), + ] + + for request, expected_packages in test_cases: + with self.subTest(request=request): + commands = self.pm.parse(request) + self.assertIsInstance(commands, list) + self.assertTrue(len(commands) > 0) + cmd = commands[0] + # Check that at least one expected package is in the command + self.assertTrue( + any(pkg in cmd for pkg in expected_packages), + f"Expected one of {expected_packages} in command: {cmd}" + ) + + +if __name__ == "__main__": + unittest.main() +