diff --git a/CHANGELOG.md b/CHANGELOG.md index 0655e648a9..d6ef767001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.27.1 (2025-08-06) + +### Bug Fixes + +- Fixed error handling issues around pickle file load + ## 0.27.0 (2025-08-03) + ### Improvements - Enhanced webhook event processing with GroupQueue implementation @@ -64,7 +72,6 @@ Improved resource cleanup and state management after processing - Fixed dockerfile's ocean user argument position to be under the last FROM - ## 0.25.2 (2025-07-13) ### Improvements diff --git a/port_ocean/tests/utils/test_ipc.py b/port_ocean/tests/utils/test_ipc.py new file mode 100644 index 0000000000..f4743cbb46 --- /dev/null +++ b/port_ocean/tests/utils/test_ipc.py @@ -0,0 +1,146 @@ +import tempfile +from unittest.mock import patch +from port_ocean.utils.ipc import FileIPC + + +class TestFileIPCErrorHandling: + def test_save_and_load_normal_operation(self) -> None: + """Test that normal save and load operations work correctly.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create a simple IPC instance and manually set file path to temp dir + ipc = FileIPC("test_process", "test_name", default_return="default") + ipc.file_path = f"{temp_dir}/test_name.pkl" + + # Save data + test_data = {"key": "value", "number": 42} + ipc.save(test_data) + + # Load data + loaded_data = ipc.load() + assert loaded_data == test_data + + def test_load_missing_file_returns_default(self) -> None: + """Test that loading a non-existent file returns the default value.""" + with tempfile.TemporaryDirectory() as temp_dir: + with patch("port_ocean.utils.ipc.os.makedirs"): + ipc = FileIPC( + "test_process", "test_name", default_return="default_value" + ) + ipc.file_path = f"{temp_dir}/nonexistent.pkl" + + result = ipc.load() + assert result == "default_value" + + def test_load_corrupted_pickle_returns_default(self) -> None: + """Test that loading a corrupted pickle file returns default value and logs warning.""" + with tempfile.TemporaryDirectory() as temp_dir: + with patch("port_ocean.utils.ipc.os.makedirs"): + ipc = FileIPC("test_process", "test_name", default_return="fallback") + ipc.file_path = f"{temp_dir}/corrupted.pkl" + + # Create a corrupted pickle file + with open(ipc.file_path, "wb") as f: + f.write(b"corrupted pickle data") + + with patch("port_ocean.utils.ipc.logger.warning") as mock_logger: + result = ipc.load() + + assert result == "fallback" + mock_logger.assert_called_once() + assert "Failed to load IPC data" in str(mock_logger.call_args) + + def test_load_truncated_pickle_returns_default(self) -> None: + """Test that loading a truncated pickle file (EOFError) returns default value.""" + with tempfile.TemporaryDirectory() as temp_dir: + with patch("port_ocean.utils.ipc.os.makedirs"): + ipc = FileIPC("test_process", "test_name", default_return=[]) + ipc.file_path = f"{temp_dir}/truncated.pkl" + + # Create a truncated pickle file (empty file) + with open(ipc.file_path, "wb"): + pass # Create empty file + + with patch("port_ocean.utils.ipc.logger.warning") as mock_logger: + result = ipc.load() + + assert result == [] + mock_logger.assert_called_once() + + def test_load_type_error_during_unpickling_returns_default(self) -> None: + """Test that TypeError during unpickling (e.g., constructor mismatch) returns default value.""" + with tempfile.TemporaryDirectory() as temp_dir: + with patch("port_ocean.utils.ipc.os.makedirs"): + ipc = FileIPC( + "test_process", "test_name", default_return="type_error_fallback" + ) + ipc.file_path = f"{temp_dir}/type_error.pkl" + + # Create a dummy file so existence check passes + with open(ipc.file_path, "wb") as f: + f.write(b"dummy content") + + # Mock pickle.load to raise TypeError (simulating constructor signature mismatch) + with patch( + "pickle.load", + side_effect=TypeError( + "KindNotImplementedException.__init__() missing 1 required positional argument: 'available_kinds'" + ), + ): + with patch("port_ocean.utils.ipc.logger.warning") as mock_logger: + result = ipc.load() + + assert result == "type_error_fallback" + mock_logger.assert_called_once() + assert "KindNotImplementedException" in str( + mock_logger.call_args + ) + + def test_load_attribute_error_during_unpickling_returns_default(self) -> None: + """Test that AttributeError during unpickling returns default value.""" + with tempfile.TemporaryDirectory() as temp_dir: + with patch("port_ocean.utils.ipc.os.makedirs"): + ipc = FileIPC( + "test_process", "test_name", default_return="attr_error_fallback" + ) + ipc.file_path = f"{temp_dir}/attr_error.pkl" + + # Create a dummy file so existence check passes + with open(ipc.file_path, "wb") as f: + f.write(b"dummy content") + + # Mock pickle.load to raise AttributeError + with patch( + "pickle.load", + side_effect=AttributeError( + "module 'some_module' has no attribute 'SomeClass'" + ), + ): + with patch("port_ocean.utils.ipc.logger.warning") as mock_logger: + result = ipc.load() + + assert result == "attr_error_fallback" + mock_logger.assert_called_once() + + def test_load_import_error_during_unpickling_returns_default(self) -> None: + """Test that ImportError during unpickling returns default value.""" + with tempfile.TemporaryDirectory() as temp_dir: + with patch("port_ocean.utils.ipc.os.makedirs"): + ipc = FileIPC( + "test_process", "test_name", default_return="import_error_fallback" + ) + ipc.file_path = f"{temp_dir}/import_error.pkl" + + # Create a dummy file so existence check passes + with open(ipc.file_path, "wb") as f: + f.write(b"dummy content") + + # Mock pickle.load to raise ImportError + with patch( + "pickle.load", + side_effect=ImportError("No module named 'missing_module'"), + ): + with patch("port_ocean.utils.ipc.logger.warning") as mock_logger: + result = ipc.load() + + assert result == "import_error_fallback" + mock_logger.assert_called_once() diff --git a/port_ocean/utils/ipc.py b/port_ocean/utils/ipc.py index ae41412fd3..ff2ec873e7 100644 --- a/port_ocean/utils/ipc.py +++ b/port_ocean/utils/ipc.py @@ -1,6 +1,7 @@ import pickle import os from typing import Any +from loguru import logger class FileIPC: @@ -22,8 +23,22 @@ def save(self, object: Any) -> None: def load(self) -> Any: if not os.path.exists(self.file_path): return self.default_return - with open(self.file_path, "rb") as f: - return pickle.load(f) + + try: + with open(self.file_path, "rb") as f: + return pickle.load(f) + except ( + pickle.PickleError, + EOFError, + OSError, + TypeError, + AttributeError, + ImportError, + ) as e: + logger.warning( + f"Failed to load IPC data from {self.file_path}: {str(e)}. Returning default value." + ) + return self.default_return def delete(self) -> None: if os.path.exists(self.file_path): diff --git a/pyproject.toml b/pyproject.toml index 7966f20bea..bc127abbb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "port-ocean" -version = "0.27.0" +version = "0.27.1" description = "Port Ocean is a CLI tool for managing your Port projects." readme = "README.md" homepage = "https://app.getport.io"