From 216eca336e3d2c20c9c4e449a83a2a50fc12c083 Mon Sep 17 00:00:00 2001 From: Wojciech Tobis Date: Sun, 7 Sep 2025 01:07:03 +0200 Subject: [PATCH 1/3] feat: add core lib handling --- docs/core_lib_handler.md | 3 + mkdocs.yml | 1 + solnlib/__init__.py | 2 + solnlib/core_lib_handler.py | 129 ++++++++++++++++++++++++++++++++++++ solnlib/utils.py | 1 + 5 files changed, 136 insertions(+) create mode 100644 docs/core_lib_handler.md create mode 100644 solnlib/core_lib_handler.py diff --git a/docs/core_lib_handler.md b/docs/core_lib_handler.md new file mode 100644 index 00000000..b93d2884 --- /dev/null +++ b/docs/core_lib_handler.md @@ -0,0 +1,3 @@ +# core_lib_handler.py + +::: solnlib.core_lib_handler \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 5e24644d..eaf7c495 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,6 +41,7 @@ nav: - "acl.py": acl.md - "credentials.py": credentials.md - "conf_manager.py": conf_manager.md + - "core_lib_handler.py": core_lib_handler.md - "file_monitor.py": file_monitor.md - "hec_config.py": hec_config.md - "log.py": log.md diff --git a/solnlib/__init__.py b/solnlib/__init__.py index 595f8f22..68611f43 100644 --- a/solnlib/__init__.py +++ b/solnlib/__init__.py @@ -20,6 +20,7 @@ acl, bulletin_rest_client, conf_manager, + core_lib_handler, credentials, file_monitor, hec_config, @@ -40,6 +41,7 @@ "acl", "bulletin_rest_client", "conf_manager", + "core_lib_handler", "credentials", "file_monitor", "hec_config", diff --git a/solnlib/core_lib_handler.py b/solnlib/core_lib_handler.py new file mode 100644 index 00000000..d9edae89 --- /dev/null +++ b/solnlib/core_lib_handler.py @@ -0,0 +1,129 @@ +# +# Copyright 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import sys +import re +from types import ModuleType +import shutil +import importlib + +def _is_module_from_splunk_core(lib_module: ModuleType) -> bool: + """ + Check if the imported module is from the Splunk-provided libraries. + """ + core_site_packages_regex = _get_core_site_packages_regex() + + splunk_site_packages_paths = [ + path for path in sys.path if core_site_packages_regex.search(path) + ] + + return any( + _is_core_site_package_path( + splunk_site_packages_path, lib_module.__name__, lib_module.__file__ + ) + for splunk_site_packages_path in splunk_site_packages_paths + ) + + +def _is_core_site_package_path( + core_site_packages_directory: str, module_name: str, module_path: str +) -> bool: + """ + Check if the module path originates from a core site-packages directory. + """ + return os.path.join(core_site_packages_directory, module_name) in module_path + + +def _get_core_site_packages_regex() -> re.Pattern: + """ + Get the regex pattern for matching site-packages directories. + """ + sep = os.path.sep + sep_escaped = re.escape(sep) + + return ( + re.compile( + r"Python(?:-\d+(?:\.\d+)?)?" + + sep_escaped + + r"lib" + + sep_escaped + + r"site-packages$", + re.IGNORECASE, + ) + if sys.platform.startswith("win32") + else re.compile( + r"lib" + + r"(" + + sep_escaped + + r"python\d+(\.\d+)?" + + r")?" + + sep_escaped + + r"site-packages$" + ) + ) + + +def _cache_lib(lib_name: str): + """ + Import the Splunk-shipped library first, before adding TA paths to sys.path, to ensure it is cached. + This way, even if the TA path added to sys.path contains the specified library, + Python will always reference the already cached library from the Splunk Python path. + """ + lib_module = importlib.import_module(lib_name) + assert _is_module_from_splunk_core( + lib_module + ), f"The module {lib_name} is not from Splunk core site-packages." + +def _get_app_path(absolute_path: str, current_script_folder: str = "lib") -> str: + """Returns app path.""" + marker = os.path.join(os.path.sep, "etc", "apps") + start = absolute_path.rfind(marker) + if start == -1: + return None + end = absolute_path.find(current_script_folder, start) + if end == -1: + return None + end = end - 1 + path = absolute_path[:end] + return path + +def _remove_lib_folder(lib_name: str): + """ + List and attempt to remove any folders directly under the 'lib' directory that contain lib_name in their name. + Handles exceptions during removal, allowing the script to proceed even if errors occur. + """ + + try: + app_dir = _get_app_path(os.path.abspath(__file__)) + lib_dir = os.path.join(app_dir, "lib") + + for entry in os.listdir(lib_dir): + entry_path = os.path.join(lib_dir, entry) + if os.path.isdir(entry_path) and lib_name in entry: + try: + shutil.rmtree(entry_path) + except Exception: + # Bypassing exceptions to ensure uninterrupted execution + pass + except Exception: + # Bypassing exceptions to ensure uninterrupted execution + pass + + +def handle_splunk_provided_lib(lib_name: str): + _cache_lib(lib_name) + _remove_lib_folder(lib_name) diff --git a/solnlib/utils.py b/solnlib/utils.py index b7954208..2f56c561 100644 --- a/solnlib/utils.py +++ b/solnlib/utils.py @@ -219,3 +219,4 @@ def get_appname_from_path(absolute_path): pass continue return "-" + From 426d07562eebebdb0c4caa3727e40218ff2c245d Mon Sep 17 00:00:00 2001 From: Wojciech Tobis Date: Thu, 11 Sep 2025 03:31:33 +0200 Subject: [PATCH 2/3] test(unit): add unit tests for core_lib_handler --- tests/unit/test_core_lib_handler.py | 614 ++++++++++++++++++++++++++++ 1 file changed, 614 insertions(+) create mode 100644 tests/unit/test_core_lib_handler.py diff --git a/tests/unit/test_core_lib_handler.py b/tests/unit/test_core_lib_handler.py new file mode 100644 index 00000000..dfe7bd45 --- /dev/null +++ b/tests/unit/test_core_lib_handler.py @@ -0,0 +1,614 @@ +# +# Copyright 2025 Splunk Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import os +import tempfile +from types import ModuleType +from unittest.mock import MagicMock, patch + +import pytest + +from solnlib import core_lib_handler + + +class TestHandleSplunkProvidedLib: + """Test cases for handle_splunk_provided_lib function.""" + + @patch('solnlib.core_lib_handler._cache_lib') + @patch('solnlib.core_lib_handler._remove_lib_folder') + def test_handle_splunk_provided_lib_calls_both_functions( + self, mock_remove_lib_folder, mock_cache_lib + ): + """Test that handle_splunk_provided_lib calls both _cache_lib and _remove_lib_folder.""" + lib_name = "test_library" + + core_lib_handler.handle_splunk_provided_lib(lib_name) + + mock_cache_lib.assert_called_once_with(lib_name) + mock_remove_lib_folder.assert_called_once_with(lib_name) + + @patch('solnlib.core_lib_handler._cache_lib') + @patch('solnlib.core_lib_handler._remove_lib_folder') + def test_handle_splunk_provided_lib_execution_order( + self, mock_remove_lib_folder, mock_cache_lib + ): + """Test that _cache_lib is called before _remove_lib_folder.""" + lib_name = "test_library" + + # Użyjemy side_effect aby sprawdzić kolejność wywołań + call_order = [] + mock_cache_lib.side_effect = lambda x: call_order.append('cache') + mock_remove_lib_folder.side_effect = lambda x: call_order.append('remove') + + core_lib_handler.handle_splunk_provided_lib(lib_name) + + assert call_order == ['cache', 'remove'] + + +class TestCacheLib: + """Test cases for _cache_lib function.""" + + @patch('solnlib.core_lib_handler.importlib.import_module') + @patch('solnlib.core_lib_handler._is_module_from_splunk_core') + def test_cache_lib_success(self, mock_is_splunk_core, mock_import_module): + """Test successful caching of a Splunk core library.""" + lib_name = "splunk_library" + mock_module = MagicMock(spec=ModuleType) + mock_import_module.return_value = mock_module + mock_is_splunk_core.return_value = True + + core_lib_handler._cache_lib(lib_name) + + mock_import_module.assert_called_once_with(lib_name) + mock_is_splunk_core.assert_called_once_with(mock_module) + + @patch('solnlib.core_lib_handler.importlib.import_module') + @patch('solnlib.core_lib_handler._is_module_from_splunk_core') + def test_cache_lib_not_from_splunk_core(self, mock_is_splunk_core, mock_import_module): + """Test that _cache_lib raises AssertionError when module is not from Splunk core.""" + lib_name = "external_library" + mock_module = MagicMock(spec=ModuleType) + mock_import_module.return_value = mock_module + mock_is_splunk_core.return_value = False + + with pytest.raises(AssertionError) as exc_info: + core_lib_handler._cache_lib(lib_name) + + assert f"The module {lib_name} is not from Splunk core site-packages." in str(exc_info.value) + mock_import_module.assert_called_once_with(lib_name) + mock_is_splunk_core.assert_called_once_with(mock_module) + + @patch('solnlib.core_lib_handler.importlib.import_module') + def test_cache_lib_import_error(self, mock_import_module): + """Test that _cache_lib propagates ImportError when module cannot be imported.""" + lib_name = "nonexistent_library" + mock_import_module.side_effect = ImportError(f"No module named '{lib_name}'") + + with pytest.raises(ImportError): + core_lib_handler._cache_lib(lib_name) + + mock_import_module.assert_called_once_with(lib_name) + + +class TestRemoveLibFolder: + """Test cases for _remove_lib_folder function.""" + + def test_remove_lib_folder_success(self): + """Test successful removal of library folders.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create mock app structure + app_dir = os.path.join(temp_dir, "test_app") + lib_dir = os.path.join(app_dir, "lib") + os.makedirs(lib_dir) + + # Create test folders + lib_name = "urllib3" + test_folders = [ + "urllib3-2.0.7", + "urllib3_secure_extra", + "other_library", + "urllib3.dist-info" + ] + + for folder in test_folders: + folder_path = os.path.join(lib_dir, folder) + os.makedirs(folder_path) + # Add a test file to make sure the folder is not empty + with open(os.path.join(folder_path, "test.py"), "w") as f: + f.write("# test file") + + # Mock _get_app_path to return our test app directory + with patch('solnlib.core_lib_handler._get_app_path', return_value=app_dir): + core_lib_handler._remove_lib_folder(lib_name) + + # Verify that folders containing lib_name were removed + remaining_folders = os.listdir(lib_dir) + assert "other_library" in remaining_folders + assert "urllib3-2.0.7" not in remaining_folders + assert "urllib3_secure_extra" not in remaining_folders + assert "urllib3.dist-info" not in remaining_folders + + def test_remove_lib_folder_no_lib_dir(self): + """Test _remove_lib_folder when lib directory doesn't exist.""" + with tempfile.TemporaryDirectory() as temp_dir: + app_dir = os.path.join(temp_dir, "test_app") + os.makedirs(app_dir) + # Don't create lib directory + + with patch('solnlib.core_lib_handler._get_app_path', return_value=app_dir): + # Should not raise an exception + core_lib_handler._remove_lib_folder("urllib3") + + def test_remove_lib_folder_app_path_none(self): + """Test _remove_lib_folder when _get_app_path returns None.""" + with patch('solnlib.core_lib_handler._get_app_path', return_value=None): + # Should not raise an exception + core_lib_handler._remove_lib_folder("urllib3") + + def test_remove_lib_folder_permission_error(self): + """Test _remove_lib_folder handles permission errors gracefully.""" + with tempfile.TemporaryDirectory() as temp_dir: + app_dir = os.path.join(temp_dir, "test_app") + lib_dir = os.path.join(app_dir, "lib") + os.makedirs(lib_dir) + + lib_name = "urllib3" + folder_to_remove = os.path.join(lib_dir, "urllib3-2.0.7") + os.makedirs(folder_to_remove) + + with patch('solnlib.core_lib_handler._get_app_path', return_value=app_dir), \ + patch('shutil.rmtree', side_effect=PermissionError("Access denied")): + + # Should not raise an exception + core_lib_handler._remove_lib_folder(lib_name) + + def test_remove_lib_folder_empty_lib_dir(self): + """Test _remove_lib_folder when lib directory is empty.""" + with tempfile.TemporaryDirectory() as temp_dir: + app_dir = os.path.join(temp_dir, "test_app") + lib_dir = os.path.join(app_dir, "lib") + os.makedirs(lib_dir) + + with patch('solnlib.core_lib_handler._get_app_path', return_value=app_dir): + # Should not raise an exception + core_lib_handler._remove_lib_folder("urllib3") + + def test_remove_lib_folder_with_files_not_directories(self): + """Test _remove_lib_folder ignores files and only processes directories.""" + with tempfile.TemporaryDirectory() as temp_dir: + app_dir = os.path.join(temp_dir, "test_app") + lib_dir = os.path.join(app_dir, "lib") + os.makedirs(lib_dir) + + lib_name = "urllib3" + + # Create a file with lib_name in its name (should be ignored) + file_path = os.path.join(lib_dir, "urllib3_config.py") + with open(file_path, "w") as f: + f.write("# config file") + + # Create a directory with lib_name in its name (should be removed) + dir_path = os.path.join(lib_dir, "urllib3-2.0.7") + os.makedirs(dir_path) + + with patch('solnlib.core_lib_handler._get_app_path', return_value=app_dir): + core_lib_handler._remove_lib_folder(lib_name) + + # File should remain, directory should be removed + assert os.path.exists(file_path) + assert not os.path.exists(dir_path) + + +class TestIsModuleFromSplunkCore: + """Test cases for _is_module_from_splunk_core function.""" + + @patch('solnlib.core_lib_handler._get_core_site_packages_regex') + @patch('solnlib.core_lib_handler._is_core_site_package_path') + def test_is_module_from_splunk_core_true(self, mock_is_core_path, mock_get_regex): + """Test when module is from Splunk core.""" + mock_module = MagicMock(spec=ModuleType) + mock_module.__name__ = "splunklib" + mock_module.__file__ = "/opt/splunk/lib/python3.9/site-packages/splunklib/__init__.py" + + mock_regex = MagicMock() + mock_regex.search.return_value = True + mock_get_regex.return_value = mock_regex + mock_is_core_path.return_value = True + + with patch('sys.path', ["/opt/splunk/lib/python3.9/site-packages"]): + result = core_lib_handler._is_module_from_splunk_core(mock_module) + + assert result is True + + @patch('solnlib.core_lib_handler._get_core_site_packages_regex') + def test_is_module_from_splunk_core_false_no_matching_paths(self, mock_get_regex): + """Test when no sys.path entries match the regex.""" + mock_module = MagicMock(spec=ModuleType) + mock_module.__name__ = "external_lib" + mock_module.__file__ = "/home/user/external_lib/__init__.py" + + mock_regex = MagicMock() + mock_regex.search.return_value = False + mock_get_regex.return_value = mock_regex + + with patch('sys.path', ["/home/user", "/usr/local/lib"]): + result = core_lib_handler._is_module_from_splunk_core(mock_module) + + assert result is False + + @patch('solnlib.core_lib_handler._get_core_site_packages_regex') + @patch('solnlib.core_lib_handler._is_core_site_package_path') + def test_is_module_from_splunk_core_false_no_match_in_paths(self, mock_is_core_path, mock_get_regex): + """Test when paths match regex but module is not from core.""" + mock_module = MagicMock(spec=ModuleType) + mock_module.__name__ = "external_lib" + mock_module.__file__ = "/home/user/external_lib/__init__.py" + + mock_regex = MagicMock() + mock_regex.search.return_value = True + mock_get_regex.return_value = mock_regex + mock_is_core_path.return_value = False + + with patch('sys.path', ["/opt/splunk/lib/python3.9/site-packages"]): + result = core_lib_handler._is_module_from_splunk_core(mock_module) + + assert result is False + + +class TestIsCoreSitePackagePath: + """Test cases for _is_core_site_package_path function.""" + + def test_is_core_site_package_path_true(self): + """Test when module path is from core site-packages.""" + core_dir = "/opt/splunk/lib/python3.9/site-packages" + module_name = "splunklib" + module_path = "/opt/splunk/lib/python3.9/site-packages/splunklib/__init__.py" + + result = core_lib_handler._is_core_site_package_path( + core_dir, module_name, module_path + ) + + assert result is True + + def test_is_core_site_package_path_false(self): + """Test when module path is not from core site-packages.""" + core_dir = "/opt/splunk/lib/python3.9/site-packages" + module_name = "external_lib" + module_path = "/home/user/external_lib/__init__.py" + + result = core_lib_handler._is_core_site_package_path( + core_dir, module_name, module_path + ) + + assert result is False + + def test_is_core_site_package_path_partial_match(self): + """Test when module name is a substring but not the exact path.""" + core_dir = "/opt/splunk/lib/python3.9/site-packages" + module_name = "lib" + module_path = "/home/user/mylib/__init__.py" + + result = core_lib_handler._is_core_site_package_path( + core_dir, module_name, module_path + ) + + assert result is False + + def test_is_core_site_package_path_none_module_path(self): + """Test when module_path is None.""" + core_dir = "/opt/splunk/lib/python3.9/site-packages" + module_name = "splunklib" + module_path = None + + # Should handle None gracefully + with pytest.raises(TypeError): + core_lib_handler._is_core_site_package_path( + core_dir, module_name, module_path + ) + + +class TestGetCoreSitePackagesRegex: + """Test cases for _get_core_site_packages_regex function.""" + + @patch('sys.platform', 'win32') + @patch('os.path.sep', '\\') + def test_get_core_site_packages_regex_windows(self): + """Test regex pattern for Windows platform.""" + regex = core_lib_handler._get_core_site_packages_regex() + + # Test Windows paths + assert regex.search(r"C:\Python-3.9\lib\site-packages") is not None + assert regex.search(r"C:\Program Files\Splunk\Python-3.9\lib\site-packages") is not None + + # Test case insensitivity on Windows + assert regex.search(r"C:\python-3.9\LIB\SITE-PACKAGES") is not None + + @patch('sys.platform', 'linux') + def test_get_core_site_packages_regex_unix(self): + """Test regex pattern for Unix-like platforms.""" + regex = core_lib_handler._get_core_site_packages_regex() + + # Test Unix paths + assert regex.search("/opt/splunk/lib/python3.9/site-packages") is not None + assert regex.search("/usr/local/lib/site-packages") is not None + assert regex.search("/usr/lib/site-packages") is not None + assert regex.search("/opt/splunk/lib/site-packages") is not None + + # Test paths without python version + assert regex.search("/usr/lib/site-packages") is not None + + def test_get_core_site_packages_regex_invalid_paths(self): + """Test that regex correctly rejects invalid paths.""" + regex = core_lib_handler._get_core_site_packages_regex() + + # Paths that should NOT match + assert regex.search("/home/user/myproject") is None + assert regex.search("/opt/splunk/etc/apps") is None + assert regex.search("/usr/bin") is None + assert regex.search("site-packages") is None # Missing lib directory + assert regex.search("/opt/splunk/lib/python3.9/site-packages/pypng-0.0.20-py3.9.egg") is None + assert regex.search("/opt/splunk/lib/python3.9/site-packages/IPy-1.0-py3.9.egg") is None + assert regex.search("/opt/splunk/lib/python3.9/site-packages/bottle-0.12.25-py3.9.egg") is None + + +class TestGetAppPath: + """Test cases for _get_app_path function.""" + + @pytest.mark.parametrize( + "absolute_path,current_script_folder,expected_result", + [ + # Standard Splunk app structure + ( + "/opt/splunk/etc/apps/my_app/lib/mymodule.py", + "lib", + "/opt/splunk/etc/apps/my_app" + ), + ( + "/opt/splunk/etc/apps/my_app/lib/mymodule/decorators.py", + "lib", + "/opt/splunk/etc/apps/my_app" + ), + # Different script folder + ( + "/opt/splunk/etc/apps/my_app/bin/mymodule.py", + "bin", + "/opt/splunk/etc/apps/my_app" + ), + ( + "/opt/splunk/etc/apps/my_app/scripts/mymodule.py", + "scripts", + "/opt/splunk/etc/apps/my_app" + ), + # Nested structure + ( + "/opt/splunk/etc/apps/my_app/lib/vendor/requests/api.py", + "lib", + "/opt/splunk/etc/apps/my_app" + ), + # Multiple etc/apps in path (should use the last one) + ( + "/home/user/etc/apps/backup/opt/splunk/etc/apps/my_app/lib/mymodule.py", + "lib", + "/home/user/etc/apps/backup/opt/splunk/etc/apps/my_app" + ), + ], + ) + def test_get_app_path_success_cases(self, absolute_path, current_script_folder, expected_result): + """Test successful cases of _get_app_path function.""" + result = core_lib_handler._get_app_path(absolute_path, current_script_folder) + assert result == expected_result + + @pytest.mark.parametrize( + "absolute_path,current_script_folder", + [ + # No etc/apps in path + ( + "/home/user/myproject/lib/mymodule.py", + "lib" + ), + # etc/apps exists but script folder doesn't exist after it + ( + "/opt/splunk/etc/apps/my_app/default/app.conf", + "lib" + ), + # Script folder exists but not after etc/apps + ( + "/home/user/lib/myproject/etc/apps/config", + "lib" + ), + # Empty path + ( + "", + "lib" + ), + ], + ) + def test_get_app_path_returns_none(self, absolute_path, current_script_folder): + """Test cases where _get_app_path should return None.""" + result = core_lib_handler._get_app_path(absolute_path, current_script_folder) + assert result is None + + def test_get_app_path_default_current_script_folder(self): + """Test that _get_app_path uses 'lib' as default current_script_folder.""" + absolute_path = "/opt/splunk/etc/apps/my_app/lib/mymodule.py" + + # Call without current_script_folder parameter + result = core_lib_handler._get_app_path(absolute_path) + expected = "/opt/splunk/etc/apps/my_app" + + assert result == expected + + def test_get_app_path_case_sensitivity(self): + """Test that _get_app_path is case sensitive.""" + # This should work (lowercase) + path_lower = "/opt/splunk/etc/apps/my_app/lib/mymodule.py" + result_lower = core_lib_handler._get_app_path(path_lower, "lib") + assert result_lower == "/opt/splunk/etc/apps/my_app" + + # This should not work (uppercase LIB) + path_upper = "/opt/splunk/etc/apps/my_app/LIB/mymodule.py" + result_upper = core_lib_handler._get_app_path(path_upper, "lib") + assert result_upper is None + + def test_get_app_path_relative_path(self): + """Test _get_app_path with relative path elements.""" + absolute_path = "/opt/splunk/etc/apps/my_app/../my_app/lib/mymodule.py" + result = core_lib_handler._get_app_path(absolute_path, "lib") + # Should still find the pattern despite .. in path + assert "/my_app" in result + + def test_get_app_path_multiple_script_folders(self): + """Test _get_app_path when script folder appears multiple times.""" + # lib appears twice, should use the one after etc/apps + absolute_path = "/home/lib/backup/opt/splunk/etc/apps/my_app/lib/mymodule.py" + result = core_lib_handler._get_app_path(absolute_path, "lib") + expected = "/home/lib/backup/opt/splunk/etc/apps/my_app" + assert result == expected + + @pytest.mark.parametrize( + "absolute_path,current_script_folder,expected_result", + [ + # Standard Splunk app structure - Windows + ( + "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\mymodule.py", + "lib", + "C:\\Program Files\\Splunk\\etc\\apps\\my_app" + ), + ( + "C:\\Program Files\\Splunk\\etc\\apps\\search\\lib\\searchcommands\\decorators.py", + "lib", + "C:\\Program Files\\Splunk\\etc\\apps\\search" + ), + # Different script folder - Windows + ( + "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\bin\\mymodule.py", + "bin", + "C:\\Program Files\\Splunk\\etc\\apps\\my_app" + ), + ( + "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\scripts\\mymodule.py", + "scripts", + "C:\\Program Files\\Splunk\\etc\\apps\\my_app" + ), + # Nested structure - Windows + ( + "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\vendor\\requests\\api.py", + "lib", + "C:\\Program Files\\Splunk\\etc\\apps\\my_app" + ), + # Multiple etc\\apps in path - Windows + ( + "D:\\backup\\etc\\apps\\old\\C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\mymodule.py", + "lib", + "D:\\backup\\etc\\apps\\old\\C:\\Program Files\\Splunk\\etc\\apps\\my_app" + ), + ], + ) + def test_get_app_path_windows_success_cases(self, absolute_path, current_script_folder, expected_result): + """Test successful cases of _get_app_path function with Windows paths.""" + with patch('os.path.join') as mock_join: + # Mock os.path.join to return Windows-style paths + mock_join.return_value = "\\etc\\apps" + + result = core_lib_handler._get_app_path(absolute_path, current_script_folder) + assert result == expected_result + + @pytest.mark.parametrize( + "absolute_path,current_script_folder", + [ + # No etc\\apps in path - Windows + ( + "C:\\Users\\user\\myproject\\lib\\mymodule.py", + "lib" + ), + # etc\\apps exists but script folder doesn't exist after it - Windows + ( + "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\default\\app.conf", + "lib" + ), + # Script folder exists but not after etc\\apps - Windows + ( + "C:\\Users\\user\\lib\\myproject\\etc\\apps\\config", + "lib" + ), + # Empty path - Windows + ( + "", + "lib" + ), + ], + ) + def test_get_app_path_windows_returns_none(self, absolute_path, current_script_folder): + """Test cases where _get_app_path should return None with Windows paths.""" + with patch('os.path.join') as mock_join: + # Mock os.path.join to return Windows-style paths + mock_join.return_value = "\\etc\\apps" + + result = core_lib_handler._get_app_path(absolute_path, current_script_folder) + assert result is None + + def test_get_app_path_windows_default_current_script_folder(self): + """Test that _get_app_path uses 'lib' as default current_script_folder on Windows.""" + with patch('os.path.join') as mock_join: + # Mock os.path.join to return Windows-style paths + mock_join.return_value = "\\etc\\apps" + + absolute_path = "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\mymodule.py" + + # Call without current_script_folder parameter + result = core_lib_handler._get_app_path(absolute_path) + expected = "C:\\Program Files\\Splunk\\etc\\apps\\my_app" + + assert result == expected + + def test_get_app_path_windows_case_sensitivity(self): + """Test that _get_app_path is case sensitive on Windows.""" + with patch('os.path.join') as mock_join: + # Mock os.path.join to return Windows-style paths + mock_join.return_value = "\\etc\\apps" + + # This should work (lowercase) + path_lower = "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\mymodule.py" + result_lower = core_lib_handler._get_app_path(path_lower, "lib") + assert result_lower == "C:\\Program Files\\Splunk\\etc\\apps\\my_app" + + # This should not work (uppercase LIB) + path_upper = "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\LIB\\mymodule.py" + result_upper = core_lib_handler._get_app_path(path_upper, "lib") + assert result_upper is None + + def test_get_app_path_windows_multiple_script_folders(self): + """Test _get_app_path when script folder appears multiple times on Windows.""" + with patch('os.path.join') as mock_join: + # Mock os.path.join to return Windows-style paths + mock_join.return_value = "\\etc\\apps" + + # lib appears twice, should use the one after etc\\apps + absolute_path = "C:\\lib\\backup\\C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\mymodule.py" + result = core_lib_handler._get_app_path(absolute_path, "lib") + expected = "C:\\lib\\backup\\C:\\Program Files\\Splunk\\etc\\apps\\my_app" + assert result == expected + + def test_get_app_path_windows_relative_path(self): + """Test _get_app_path with relative path elements on Windows.""" + with patch('os.path.join') as mock_join: + # Mock os.path.join to return Windows-style paths + mock_join.return_value = "\\etc\\apps" + + absolute_path = "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\..\\my_app\\lib\\mymodule.py" + result = core_lib_handler._get_app_path(absolute_path, "lib") + # Should still find the pattern despite .. in path + assert "\\my_app" in result From c1b6fb27141bf189cbc10859e1d08048f14b7b37 Mon Sep 17 00:00:00 2001 From: Wojciech Tobis Date: Thu, 11 Sep 2025 10:05:23 +0200 Subject: [PATCH 3/3] chore: add logging in lib removal error handling --- solnlib/core_lib_handler.py | 54 ++-- solnlib/utils.py | 1 - tests/unit/test_core_lib_handler.py | 391 ++++++++++++++++------------ 3 files changed, 256 insertions(+), 190 deletions(-) diff --git a/solnlib/core_lib_handler.py b/solnlib/core_lib_handler.py index d9edae89..2ce33873 100644 --- a/solnlib/core_lib_handler.py +++ b/solnlib/core_lib_handler.py @@ -21,10 +21,9 @@ import shutil import importlib + def _is_module_from_splunk_core(lib_module: ModuleType) -> bool: - """ - Check if the imported module is from the Splunk-provided libraries. - """ + """Check if the imported module is from the Splunk-provided libraries.""" core_site_packages_regex = _get_core_site_packages_regex() splunk_site_packages_paths = [ @@ -42,16 +41,13 @@ def _is_module_from_splunk_core(lib_module: ModuleType) -> bool: def _is_core_site_package_path( core_site_packages_directory: str, module_name: str, module_path: str ) -> bool: - """ - Check if the module path originates from a core site-packages directory. - """ + """Check if the module path originates from a core site-packages + directory.""" return os.path.join(core_site_packages_directory, module_name) in module_path def _get_core_site_packages_regex() -> re.Pattern: - """ - Get the regex pattern for matching site-packages directories. - """ + """Get the regex pattern for matching site-packages directories.""" sep = os.path.sep sep_escaped = re.escape(sep) @@ -78,16 +74,19 @@ def _get_core_site_packages_regex() -> re.Pattern: def _cache_lib(lib_name: str): - """ - Import the Splunk-shipped library first, before adding TA paths to sys.path, to ensure it is cached. - This way, even if the TA path added to sys.path contains the specified library, - Python will always reference the already cached library from the Splunk Python path. + """Import the Splunk-shipped library first, before adding TA paths to + sys.path, to ensure it is cached. + + This way, even if the TA path added to sys.path contains the + specified library, Python will always reference the already cached + library from the Splunk Python path. """ lib_module = importlib.import_module(lib_name) assert _is_module_from_splunk_core( lib_module ), f"The module {lib_name} is not from Splunk core site-packages." + def _get_app_path(absolute_path: str, current_script_folder: str = "lib") -> str: """Returns app path.""" marker = os.path.join(os.path.sep, "etc", "apps") @@ -101,27 +100,32 @@ def _get_app_path(absolute_path: str, current_script_folder: str = "lib") -> str path = absolute_path[:end] return path + def _remove_lib_folder(lib_name: str): - """ - List and attempt to remove any folders directly under the 'lib' directory that contain lib_name in their name. - Handles exceptions during removal, allowing the script to proceed even if errors occur. + """List and attempt to remove any folders directly under the 'lib' + directory that contain lib_name in their name. + + Handles exceptions during removal, allowing the script to proceed + even if errors occur. """ - try: - app_dir = _get_app_path(os.path.abspath(__file__)) - lib_dir = os.path.join(app_dir, "lib") + app_dir = _get_app_path(os.path.abspath(__file__)) + if app_dir is None: + print(f"WARNING: Unable to determine app directory path for {lib_name}") + return + lib_dir = os.path.join(app_dir, "lib") + + try: for entry in os.listdir(lib_dir): entry_path = os.path.join(lib_dir, entry) if os.path.isdir(entry_path) and lib_name in entry: try: shutil.rmtree(entry_path) - except Exception: - # Bypassing exceptions to ensure uninterrupted execution - pass - except Exception: - # Bypassing exceptions to ensure uninterrupted execution - pass + except Exception as e: + print(f"ERROR: Failed to remove library folder {entry_path}: {e}") + except Exception as e: + print(f"ERROR: Error in _remove_lib_folder for {lib_name}: {e}") def handle_splunk_provided_lib(lib_name: str): diff --git a/solnlib/utils.py b/solnlib/utils.py index 2f56c561..b7954208 100644 --- a/solnlib/utils.py +++ b/solnlib/utils.py @@ -219,4 +219,3 @@ def get_appname_from_path(absolute_path): pass continue return "-" - diff --git a/tests/unit/test_core_lib_handler.py b/tests/unit/test_core_lib_handler.py index dfe7bd45..a98acf23 100644 --- a/tests/unit/test_core_lib_handler.py +++ b/tests/unit/test_core_lib_handler.py @@ -27,79 +27,86 @@ class TestHandleSplunkProvidedLib: """Test cases for handle_splunk_provided_lib function.""" - @patch('solnlib.core_lib_handler._cache_lib') - @patch('solnlib.core_lib_handler._remove_lib_folder') + @patch("solnlib.core_lib_handler._cache_lib") + @patch("solnlib.core_lib_handler._remove_lib_folder") def test_handle_splunk_provided_lib_calls_both_functions( self, mock_remove_lib_folder, mock_cache_lib ): - """Test that handle_splunk_provided_lib calls both _cache_lib and _remove_lib_folder.""" + """Test that handle_splunk_provided_lib calls both _cache_lib and + _remove_lib_folder.""" lib_name = "test_library" - + core_lib_handler.handle_splunk_provided_lib(lib_name) - + mock_cache_lib.assert_called_once_with(lib_name) mock_remove_lib_folder.assert_called_once_with(lib_name) - @patch('solnlib.core_lib_handler._cache_lib') - @patch('solnlib.core_lib_handler._remove_lib_folder') + @patch("solnlib.core_lib_handler._cache_lib") + @patch("solnlib.core_lib_handler._remove_lib_folder") def test_handle_splunk_provided_lib_execution_order( self, mock_remove_lib_folder, mock_cache_lib ): """Test that _cache_lib is called before _remove_lib_folder.""" lib_name = "test_library" - + # Użyjemy side_effect aby sprawdzić kolejność wywołań call_order = [] - mock_cache_lib.side_effect = lambda x: call_order.append('cache') - mock_remove_lib_folder.side_effect = lambda x: call_order.append('remove') - + mock_cache_lib.side_effect = lambda x: call_order.append("cache") + mock_remove_lib_folder.side_effect = lambda x: call_order.append("remove") + core_lib_handler.handle_splunk_provided_lib(lib_name) - - assert call_order == ['cache', 'remove'] + + assert call_order == ["cache", "remove"] class TestCacheLib: """Test cases for _cache_lib function.""" - @patch('solnlib.core_lib_handler.importlib.import_module') - @patch('solnlib.core_lib_handler._is_module_from_splunk_core') + @patch("solnlib.core_lib_handler.importlib.import_module") + @patch("solnlib.core_lib_handler._is_module_from_splunk_core") def test_cache_lib_success(self, mock_is_splunk_core, mock_import_module): """Test successful caching of a Splunk core library.""" lib_name = "splunk_library" mock_module = MagicMock(spec=ModuleType) mock_import_module.return_value = mock_module mock_is_splunk_core.return_value = True - + core_lib_handler._cache_lib(lib_name) - + mock_import_module.assert_called_once_with(lib_name) mock_is_splunk_core.assert_called_once_with(mock_module) - @patch('solnlib.core_lib_handler.importlib.import_module') - @patch('solnlib.core_lib_handler._is_module_from_splunk_core') - def test_cache_lib_not_from_splunk_core(self, mock_is_splunk_core, mock_import_module): - """Test that _cache_lib raises AssertionError when module is not from Splunk core.""" + @patch("solnlib.core_lib_handler.importlib.import_module") + @patch("solnlib.core_lib_handler._is_module_from_splunk_core") + def test_cache_lib_not_from_splunk_core( + self, mock_is_splunk_core, mock_import_module + ): + """Test that _cache_lib raises AssertionError when module is not from + Splunk core.""" lib_name = "external_library" mock_module = MagicMock(spec=ModuleType) mock_import_module.return_value = mock_module mock_is_splunk_core.return_value = False - + with pytest.raises(AssertionError) as exc_info: core_lib_handler._cache_lib(lib_name) - - assert f"The module {lib_name} is not from Splunk core site-packages." in str(exc_info.value) + + assert f"The module {lib_name} is not from Splunk core site-packages." in str( + exc_info.value + ) mock_import_module.assert_called_once_with(lib_name) mock_is_splunk_core.assert_called_once_with(mock_module) - @patch('solnlib.core_lib_handler.importlib.import_module') + @patch("solnlib.core_lib_handler.importlib.import_module") def test_cache_lib_import_error(self, mock_import_module): - """Test that _cache_lib propagates ImportError when module cannot be imported.""" + """Test that _cache_lib propagates ImportError when module cannot be + imported.""" lib_name = "nonexistent_library" mock_import_module.side_effect = ImportError(f"No module named '{lib_name}'") - + with pytest.raises(ImportError): core_lib_handler._cache_lib(lib_name) - + mock_import_module.assert_called_once_with(lib_name) @@ -113,27 +120,29 @@ def test_remove_lib_folder_success(self): app_dir = os.path.join(temp_dir, "test_app") lib_dir = os.path.join(app_dir, "lib") os.makedirs(lib_dir) - + # Create test folders lib_name = "urllib3" test_folders = [ "urllib3-2.0.7", "urllib3_secure_extra", "other_library", - "urllib3.dist-info" + "urllib3.dist-info", ] - + for folder in test_folders: folder_path = os.path.join(lib_dir, folder) os.makedirs(folder_path) # Add a test file to make sure the folder is not empty with open(os.path.join(folder_path, "test.py"), "w") as f: f.write("# test file") - + # Mock _get_app_path to return our test app directory - with patch('solnlib.core_lib_handler._get_app_path', return_value=app_dir): + with patch( + "solnlib.core_lib_handler._get_app_path", return_value=app_dir + ), patch("builtins.print") as mock_print: core_lib_handler._remove_lib_folder(lib_name) - + # Verify that folders containing lib_name were removed remaining_folders = os.listdir(lib_dir) assert "other_library" in remaining_folders @@ -141,130 +150,171 @@ def test_remove_lib_folder_success(self): assert "urllib3_secure_extra" not in remaining_folders assert "urllib3.dist-info" not in remaining_folders + mock_print.assert_not_called() + def test_remove_lib_folder_no_lib_dir(self): """Test _remove_lib_folder when lib directory doesn't exist.""" with tempfile.TemporaryDirectory() as temp_dir: app_dir = os.path.join(temp_dir, "test_app") os.makedirs(app_dir) # Don't create lib directory - - with patch('solnlib.core_lib_handler._get_app_path', return_value=app_dir): + + with patch( + "solnlib.core_lib_handler._get_app_path", return_value=app_dir + ), patch("builtins.print") as mock_print: # Should not raise an exception core_lib_handler._remove_lib_folder("urllib3") + # Should print an error message about lib directory not existing + mock_print.assert_called_with( + "ERROR: Error in _remove_lib_folder for urllib3: [Errno 2] No such file or directory: '" + + os.path.join(app_dir, "lib") + + "'" + ) + def test_remove_lib_folder_app_path_none(self): """Test _remove_lib_folder when _get_app_path returns None.""" - with patch('solnlib.core_lib_handler._get_app_path', return_value=None): + with patch("solnlib.core_lib_handler._get_app_path", return_value=None), patch( + "builtins.print" + ) as mock_print: # Should not raise an exception core_lib_handler._remove_lib_folder("urllib3") + # Should print a warning message about unable to determine app directory path + mock_print.assert_called_once_with( + "WARNING: Unable to determine app directory path for urllib3" + ) + def test_remove_lib_folder_permission_error(self): """Test _remove_lib_folder handles permission errors gracefully.""" with tempfile.TemporaryDirectory() as temp_dir: app_dir = os.path.join(temp_dir, "test_app") lib_dir = os.path.join(app_dir, "lib") os.makedirs(lib_dir) - + lib_name = "urllib3" folder_to_remove = os.path.join(lib_dir, "urllib3-2.0.7") os.makedirs(folder_to_remove) - - with patch('solnlib.core_lib_handler._get_app_path', return_value=app_dir), \ - patch('shutil.rmtree', side_effect=PermissionError("Access denied")): - + + with patch( + "solnlib.core_lib_handler._get_app_path", return_value=app_dir + ), patch( + "shutil.rmtree", side_effect=PermissionError("Access denied") + ), patch( + "builtins.print" + ) as mock_print: + # Should not raise an exception core_lib_handler._remove_lib_folder(lib_name) + mock_print.assert_called_once_with( + f"ERROR: Failed to remove library folder {folder_to_remove}: Access denied" + ) + def test_remove_lib_folder_empty_lib_dir(self): """Test _remove_lib_folder when lib directory is empty.""" with tempfile.TemporaryDirectory() as temp_dir: app_dir = os.path.join(temp_dir, "test_app") lib_dir = os.path.join(app_dir, "lib") os.makedirs(lib_dir) - - with patch('solnlib.core_lib_handler._get_app_path', return_value=app_dir): + + with patch( + "solnlib.core_lib_handler._get_app_path", return_value=app_dir + ), patch("builtins.print") as mock_print: # Should not raise an exception core_lib_handler._remove_lib_folder("urllib3") + # Should not print anything since there are no folders to remove + mock_print.assert_not_called() + def test_remove_lib_folder_with_files_not_directories(self): - """Test _remove_lib_folder ignores files and only processes directories.""" + """Test _remove_lib_folder ignores files and only processes + directories.""" with tempfile.TemporaryDirectory() as temp_dir: app_dir = os.path.join(temp_dir, "test_app") lib_dir = os.path.join(app_dir, "lib") os.makedirs(lib_dir) - + lib_name = "urllib3" - + # Create a file with lib_name in its name (should be ignored) file_path = os.path.join(lib_dir, "urllib3_config.py") with open(file_path, "w") as f: f.write("# config file") - + # Create a directory with lib_name in its name (should be removed) dir_path = os.path.join(lib_dir, "urllib3-2.0.7") os.makedirs(dir_path) - - with patch('solnlib.core_lib_handler._get_app_path', return_value=app_dir): + + with patch( + "solnlib.core_lib_handler._get_app_path", return_value=app_dir + ), patch("builtins.print") as mock_print: core_lib_handler._remove_lib_folder(lib_name) - + # File should remain, directory should be removed assert os.path.exists(file_path) assert not os.path.exists(dir_path) + mock_print.assert_not_called() + class TestIsModuleFromSplunkCore: """Test cases for _is_module_from_splunk_core function.""" - @patch('solnlib.core_lib_handler._get_core_site_packages_regex') - @patch('solnlib.core_lib_handler._is_core_site_package_path') + @patch("solnlib.core_lib_handler._get_core_site_packages_regex") + @patch("solnlib.core_lib_handler._is_core_site_package_path") def test_is_module_from_splunk_core_true(self, mock_is_core_path, mock_get_regex): """Test when module is from Splunk core.""" mock_module = MagicMock(spec=ModuleType) mock_module.__name__ = "splunklib" - mock_module.__file__ = "/opt/splunk/lib/python3.9/site-packages/splunklib/__init__.py" - + mock_module.__file__ = ( + "/opt/splunk/lib/python3.9/site-packages/splunklib/__init__.py" + ) + mock_regex = MagicMock() mock_regex.search.return_value = True mock_get_regex.return_value = mock_regex mock_is_core_path.return_value = True - - with patch('sys.path', ["/opt/splunk/lib/python3.9/site-packages"]): + + with patch("sys.path", ["/opt/splunk/lib/python3.9/site-packages"]): result = core_lib_handler._is_module_from_splunk_core(mock_module) - + assert result is True - @patch('solnlib.core_lib_handler._get_core_site_packages_regex') + @patch("solnlib.core_lib_handler._get_core_site_packages_regex") def test_is_module_from_splunk_core_false_no_matching_paths(self, mock_get_regex): """Test when no sys.path entries match the regex.""" mock_module = MagicMock(spec=ModuleType) mock_module.__name__ = "external_lib" mock_module.__file__ = "/home/user/external_lib/__init__.py" - + mock_regex = MagicMock() mock_regex.search.return_value = False mock_get_regex.return_value = mock_regex - - with patch('sys.path', ["/home/user", "/usr/local/lib"]): + + with patch("sys.path", ["/home/user", "/usr/local/lib"]): result = core_lib_handler._is_module_from_splunk_core(mock_module) - + assert result is False - @patch('solnlib.core_lib_handler._get_core_site_packages_regex') - @patch('solnlib.core_lib_handler._is_core_site_package_path') - def test_is_module_from_splunk_core_false_no_match_in_paths(self, mock_is_core_path, mock_get_regex): + @patch("solnlib.core_lib_handler._get_core_site_packages_regex") + @patch("solnlib.core_lib_handler._is_core_site_package_path") + def test_is_module_from_splunk_core_false_no_match_in_paths( + self, mock_is_core_path, mock_get_regex + ): """Test when paths match regex but module is not from core.""" mock_module = MagicMock(spec=ModuleType) mock_module.__name__ = "external_lib" mock_module.__file__ = "/home/user/external_lib/__init__.py" - + mock_regex = MagicMock() mock_regex.search.return_value = True mock_get_regex.return_value = mock_regex mock_is_core_path.return_value = False - - with patch('sys.path', ["/opt/splunk/lib/python3.9/site-packages"]): + + with patch("sys.path", ["/opt/splunk/lib/python3.9/site-packages"]): result = core_lib_handler._is_module_from_splunk_core(mock_module) - + assert result is False @@ -276,11 +326,11 @@ def test_is_core_site_package_path_true(self): core_dir = "/opt/splunk/lib/python3.9/site-packages" module_name = "splunklib" module_path = "/opt/splunk/lib/python3.9/site-packages/splunklib/__init__.py" - + result = core_lib_handler._is_core_site_package_path( core_dir, module_name, module_path ) - + assert result is True def test_is_core_site_package_path_false(self): @@ -288,11 +338,11 @@ def test_is_core_site_package_path_false(self): core_dir = "/opt/splunk/lib/python3.9/site-packages" module_name = "external_lib" module_path = "/home/user/external_lib/__init__.py" - + result = core_lib_handler._is_core_site_package_path( core_dir, module_name, module_path ) - + assert result is False def test_is_core_site_package_path_partial_match(self): @@ -300,11 +350,11 @@ def test_is_core_site_package_path_partial_match(self): core_dir = "/opt/splunk/lib/python3.9/site-packages" module_name = "lib" module_path = "/home/user/mylib/__init__.py" - + result = core_lib_handler._is_core_site_package_path( core_dir, module_name, module_path ) - + assert result is False def test_is_core_site_package_path_none_module_path(self): @@ -312,7 +362,7 @@ def test_is_core_site_package_path_none_module_path(self): core_dir = "/opt/splunk/lib/python3.9/site-packages" module_name = "splunklib" module_path = None - + # Should handle None gracefully with pytest.raises(TypeError): core_lib_handler._is_core_site_package_path( @@ -323,45 +373,61 @@ def test_is_core_site_package_path_none_module_path(self): class TestGetCoreSitePackagesRegex: """Test cases for _get_core_site_packages_regex function.""" - @patch('sys.platform', 'win32') - @patch('os.path.sep', '\\') + @patch("sys.platform", "win32") + @patch("os.path.sep", "\\") def test_get_core_site_packages_regex_windows(self): """Test regex pattern for Windows platform.""" regex = core_lib_handler._get_core_site_packages_regex() - + # Test Windows paths assert regex.search(r"C:\Python-3.9\lib\site-packages") is not None - assert regex.search(r"C:\Program Files\Splunk\Python-3.9\lib\site-packages") is not None - + assert ( + regex.search(r"C:\Program Files\Splunk\Python-3.9\lib\site-packages") + is not None + ) + # Test case insensitivity on Windows assert regex.search(r"C:\python-3.9\LIB\SITE-PACKAGES") is not None - @patch('sys.platform', 'linux') + @patch("sys.platform", "linux") def test_get_core_site_packages_regex_unix(self): """Test regex pattern for Unix-like platforms.""" regex = core_lib_handler._get_core_site_packages_regex() - + # Test Unix paths assert regex.search("/opt/splunk/lib/python3.9/site-packages") is not None assert regex.search("/usr/local/lib/site-packages") is not None assert regex.search("/usr/lib/site-packages") is not None assert regex.search("/opt/splunk/lib/site-packages") is not None - + # Test paths without python version assert regex.search("/usr/lib/site-packages") is not None def test_get_core_site_packages_regex_invalid_paths(self): """Test that regex correctly rejects invalid paths.""" regex = core_lib_handler._get_core_site_packages_regex() - + # Paths that should NOT match assert regex.search("/home/user/myproject") is None assert regex.search("/opt/splunk/etc/apps") is None assert regex.search("/usr/bin") is None assert regex.search("site-packages") is None # Missing lib directory - assert regex.search("/opt/splunk/lib/python3.9/site-packages/pypng-0.0.20-py3.9.egg") is None - assert regex.search("/opt/splunk/lib/python3.9/site-packages/IPy-1.0-py3.9.egg") is None - assert regex.search("/opt/splunk/lib/python3.9/site-packages/bottle-0.12.25-py3.9.egg") is None + assert ( + regex.search( + "/opt/splunk/lib/python3.9/site-packages/pypng-0.0.20-py3.9.egg" + ) + is None + ) + assert ( + regex.search("/opt/splunk/lib/python3.9/site-packages/IPy-1.0-py3.9.egg") + is None + ) + assert ( + regex.search( + "/opt/splunk/lib/python3.9/site-packages/bottle-0.12.25-py3.9.egg" + ) + is None + ) class TestGetAppPath: @@ -374,39 +440,41 @@ class TestGetAppPath: ( "/opt/splunk/etc/apps/my_app/lib/mymodule.py", "lib", - "/opt/splunk/etc/apps/my_app" + "/opt/splunk/etc/apps/my_app", ), ( "/opt/splunk/etc/apps/my_app/lib/mymodule/decorators.py", "lib", - "/opt/splunk/etc/apps/my_app" + "/opt/splunk/etc/apps/my_app", ), # Different script folder ( "/opt/splunk/etc/apps/my_app/bin/mymodule.py", "bin", - "/opt/splunk/etc/apps/my_app" + "/opt/splunk/etc/apps/my_app", ), ( "/opt/splunk/etc/apps/my_app/scripts/mymodule.py", "scripts", - "/opt/splunk/etc/apps/my_app" + "/opt/splunk/etc/apps/my_app", ), # Nested structure ( "/opt/splunk/etc/apps/my_app/lib/vendor/requests/api.py", "lib", - "/opt/splunk/etc/apps/my_app" + "/opt/splunk/etc/apps/my_app", ), # Multiple etc/apps in path (should use the last one) ( "/home/user/etc/apps/backup/opt/splunk/etc/apps/my_app/lib/mymodule.py", "lib", - "/home/user/etc/apps/backup/opt/splunk/etc/apps/my_app" + "/home/user/etc/apps/backup/opt/splunk/etc/apps/my_app", ), ], ) - def test_get_app_path_success_cases(self, absolute_path, current_script_folder, expected_result): + def test_get_app_path_success_cases( + self, absolute_path, current_script_folder, expected_result + ): """Test successful cases of _get_app_path function.""" result = core_lib_handler._get_app_path(absolute_path, current_script_folder) assert result == expected_result @@ -415,25 +483,13 @@ def test_get_app_path_success_cases(self, absolute_path, current_script_folder, "absolute_path,current_script_folder", [ # No etc/apps in path - ( - "/home/user/myproject/lib/mymodule.py", - "lib" - ), + ("/home/user/myproject/lib/mymodule.py", "lib"), # etc/apps exists but script folder doesn't exist after it - ( - "/opt/splunk/etc/apps/my_app/default/app.conf", - "lib" - ), + ("/opt/splunk/etc/apps/my_app/default/app.conf", "lib"), # Script folder exists but not after etc/apps - ( - "/home/user/lib/myproject/etc/apps/config", - "lib" - ), + ("/home/user/lib/myproject/etc/apps/config", "lib"), # Empty path - ( - "", - "lib" - ), + ("", "lib"), ], ) def test_get_app_path_returns_none(self, absolute_path, current_script_folder): @@ -442,13 +498,14 @@ def test_get_app_path_returns_none(self, absolute_path, current_script_folder): assert result is None def test_get_app_path_default_current_script_folder(self): - """Test that _get_app_path uses 'lib' as default current_script_folder.""" + """Test that _get_app_path uses 'lib' as default + current_script_folder.""" absolute_path = "/opt/splunk/etc/apps/my_app/lib/mymodule.py" - + # Call without current_script_folder parameter result = core_lib_handler._get_app_path(absolute_path) expected = "/opt/splunk/etc/apps/my_app" - + assert result == expected def test_get_app_path_case_sensitivity(self): @@ -457,7 +514,7 @@ def test_get_app_path_case_sensitivity(self): path_lower = "/opt/splunk/etc/apps/my_app/lib/mymodule.py" result_lower = core_lib_handler._get_app_path(path_lower, "lib") assert result_lower == "/opt/splunk/etc/apps/my_app" - + # This should not work (uppercase LIB) path_upper = "/opt/splunk/etc/apps/my_app/LIB/mymodule.py" result_upper = core_lib_handler._get_app_path(path_upper, "lib") @@ -485,117 +542,123 @@ def test_get_app_path_multiple_script_folders(self): ( "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\mymodule.py", "lib", - "C:\\Program Files\\Splunk\\etc\\apps\\my_app" + "C:\\Program Files\\Splunk\\etc\\apps\\my_app", ), ( "C:\\Program Files\\Splunk\\etc\\apps\\search\\lib\\searchcommands\\decorators.py", "lib", - "C:\\Program Files\\Splunk\\etc\\apps\\search" + "C:\\Program Files\\Splunk\\etc\\apps\\search", ), # Different script folder - Windows ( "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\bin\\mymodule.py", "bin", - "C:\\Program Files\\Splunk\\etc\\apps\\my_app" + "C:\\Program Files\\Splunk\\etc\\apps\\my_app", ), ( "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\scripts\\mymodule.py", "scripts", - "C:\\Program Files\\Splunk\\etc\\apps\\my_app" + "C:\\Program Files\\Splunk\\etc\\apps\\my_app", ), # Nested structure - Windows ( "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\vendor\\requests\\api.py", "lib", - "C:\\Program Files\\Splunk\\etc\\apps\\my_app" + "C:\\Program Files\\Splunk\\etc\\apps\\my_app", ), # Multiple etc\\apps in path - Windows ( "D:\\backup\\etc\\apps\\old\\C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\mymodule.py", "lib", - "D:\\backup\\etc\\apps\\old\\C:\\Program Files\\Splunk\\etc\\apps\\my_app" + "D:\\backup\\etc\\apps\\old\\C:\\Program Files\\Splunk\\etc\\apps\\my_app", ), ], ) - def test_get_app_path_windows_success_cases(self, absolute_path, current_script_folder, expected_result): - """Test successful cases of _get_app_path function with Windows paths.""" - with patch('os.path.join') as mock_join: + def test_get_app_path_windows_success_cases( + self, absolute_path, current_script_folder, expected_result + ): + """Test successful cases of _get_app_path function with Windows + paths.""" + with patch("os.path.join") as mock_join: # Mock os.path.join to return Windows-style paths mock_join.return_value = "\\etc\\apps" - - result = core_lib_handler._get_app_path(absolute_path, current_script_folder) + + result = core_lib_handler._get_app_path( + absolute_path, current_script_folder + ) assert result == expected_result @pytest.mark.parametrize( "absolute_path,current_script_folder", [ # No etc\\apps in path - Windows - ( - "C:\\Users\\user\\myproject\\lib\\mymodule.py", - "lib" - ), + ("C:\\Users\\user\\myproject\\lib\\mymodule.py", "lib"), # etc\\apps exists but script folder doesn't exist after it - Windows - ( - "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\default\\app.conf", - "lib" - ), + ("C:\\Program Files\\Splunk\\etc\\apps\\my_app\\default\\app.conf", "lib"), # Script folder exists but not after etc\\apps - Windows - ( - "C:\\Users\\user\\lib\\myproject\\etc\\apps\\config", - "lib" - ), + ("C:\\Users\\user\\lib\\myproject\\etc\\apps\\config", "lib"), # Empty path - Windows - ( - "", - "lib" - ), + ("", "lib"), ], ) - def test_get_app_path_windows_returns_none(self, absolute_path, current_script_folder): - """Test cases where _get_app_path should return None with Windows paths.""" - with patch('os.path.join') as mock_join: + def test_get_app_path_windows_returns_none( + self, absolute_path, current_script_folder + ): + """Test cases where _get_app_path should return None with Windows + paths.""" + with patch("os.path.join") as mock_join: # Mock os.path.join to return Windows-style paths mock_join.return_value = "\\etc\\apps" - - result = core_lib_handler._get_app_path(absolute_path, current_script_folder) + + result = core_lib_handler._get_app_path( + absolute_path, current_script_folder + ) assert result is None def test_get_app_path_windows_default_current_script_folder(self): - """Test that _get_app_path uses 'lib' as default current_script_folder on Windows.""" - with patch('os.path.join') as mock_join: + """Test that _get_app_path uses 'lib' as default current_script_folder + on Windows.""" + with patch("os.path.join") as mock_join: # Mock os.path.join to return Windows-style paths mock_join.return_value = "\\etc\\apps" - - absolute_path = "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\mymodule.py" - + + absolute_path = ( + "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\mymodule.py" + ) + # Call without current_script_folder parameter result = core_lib_handler._get_app_path(absolute_path) expected = "C:\\Program Files\\Splunk\\etc\\apps\\my_app" - + assert result == expected def test_get_app_path_windows_case_sensitivity(self): """Test that _get_app_path is case sensitive on Windows.""" - with patch('os.path.join') as mock_join: + with patch("os.path.join") as mock_join: # Mock os.path.join to return Windows-style paths mock_join.return_value = "\\etc\\apps" - + # This should work (lowercase) - path_lower = "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\mymodule.py" + path_lower = ( + "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\mymodule.py" + ) result_lower = core_lib_handler._get_app_path(path_lower, "lib") assert result_lower == "C:\\Program Files\\Splunk\\etc\\apps\\my_app" - + # This should not work (uppercase LIB) - path_upper = "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\LIB\\mymodule.py" + path_upper = ( + "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\LIB\\mymodule.py" + ) result_upper = core_lib_handler._get_app_path(path_upper, "lib") assert result_upper is None def test_get_app_path_windows_multiple_script_folders(self): - """Test _get_app_path when script folder appears multiple times on Windows.""" - with patch('os.path.join') as mock_join: + """Test _get_app_path when script folder appears multiple times on + Windows.""" + with patch("os.path.join") as mock_join: # Mock os.path.join to return Windows-style paths mock_join.return_value = "\\etc\\apps" - + # lib appears twice, should use the one after etc\\apps absolute_path = "C:\\lib\\backup\\C:\\Program Files\\Splunk\\etc\\apps\\my_app\\lib\\mymodule.py" result = core_lib_handler._get_app_path(absolute_path, "lib") @@ -604,10 +667,10 @@ def test_get_app_path_windows_multiple_script_folders(self): def test_get_app_path_windows_relative_path(self): """Test _get_app_path with relative path elements on Windows.""" - with patch('os.path.join') as mock_join: + with patch("os.path.join") as mock_join: # Mock os.path.join to return Windows-style paths mock_join.return_value = "\\etc\\apps" - + absolute_path = "C:\\Program Files\\Splunk\\etc\\apps\\my_app\\..\\my_app\\lib\\mymodule.py" result = core_lib_handler._get_app_path(absolute_path, "lib") # Should still find the pattern despite .. in path