from unittest.mock import ANY, MagicMock, patch from pytest import mark from model_manager.activities.activities import Activities from model_manager.activities.experiment_tracking import ExperimentTracking from model_manager.activities.mlflow import MLFlow from model_manager.activities.training import Training @patch('model_manager.activities.activities.ExperimentTracking.__init__') @patch('model_manager.activities.activities.MLFlow.__init__') @patch('model_manager.activities.activities.MinIO.__init__') @patch('model_manager.activities.activities.Training.__init__') def test___init__( mock_training_init, mock_minio_init, mock_mlflow_init, mock_experiment_tracking_init, ): postgres_config = { 'host': 'localhost', 'port': 5432, 'user': 'postgres', 'password': 'postgres', 'dbname': 'postgres', 'min_connections': 1, 'max_connections': 10, } mlflow_config = {'host': 'localhost', 'port': 5000, 'username': 'mlflow', 'password': 'mlflow'} minio_config = { 'endpoint_url': 'http://localhost:9000', 'access_key': 'minioadmin', 'secret_key': 'minioadmin', 'region': 'us-east-1', 'use_ssl': False, 'max_retry_attempts': 3, 'retry_mode': 'adaptive', 'connect_timeout': 10, 'read_timeout': 60, } logger = MagicMock() notification_handler = MagicMock() activities = Activities( postgres_config=postgres_config, mlflow_config=mlflow_config, minio_config=minio_config, logger=logger, notification_handler=notification_handler, ) assert isinstance(activities, Activities) assert isinstance(activities, ExperimentTracking) assert isinstance(activities, MLFlow) assert isinstance(activities, Training) mock_experiment_tracking_init.assert_called_once_with( ANY, host=postgres_config['host'], port=postgres_config['port'], user=postgres_config['user'], password=postgres_config['password'], dbname=postgres_config['dbname'], min_connections=postgres_config['min_connections'], max_connections=postgres_config['max_connections'], logger=logger, notification_handler=notification_handler, ) mock_mlflow_init.assert_called_once_with( ANY, mlflow_host=mlflow_config['host'], mlflow_port=mlflow_config['port'], mlflow_username=mlflow_config['username'], mlflow_password=mlflow_config['password'], logger=logger, notification_handler=notification_handler, ) mock_minio_init.assert_called_once_with( ANY, endpoint_url=minio_config['endpoint_url'], access_key=minio_config['access_key'], secret_key=minio_config['secret_key'], region=minio_config['region'], use_ssl=minio_config['use_ssl'], max_retry_attempts=minio_config['max_retry_attempts'], retry_mode=minio_config['retry_mode'], connect_timeout=minio_config['connect_timeout'], read_timeout=minio_config['read_timeout'], logger=logger, notification_handler=notification_handler, ) mock_training_init.assert_called_once_with( ANY, logger=logger, notification_handler=notification_handler ) @mark.asyncio @patch('model_manager.activities.activities.ExperimentTracking', return_value=MagicMock()) @patch('model_manager.activities.activities.MLFlow', return_value=MagicMock()) async def test_shutdown(_mock_mlflow_init, mock_experiment_tracking_init): postgres_config = { 'host': 'localhost', 'port': 5432, 'user': 'postgres', 'password': 'postgres', 'dbname': 'postgres', 'min_connections': 1, 'max_connections': 10, } mlflow_config = {'host': 'localhost', 'port': 5000, 'username': 'mlflow', 'password': 'mlflow'} minio_config = { 'endpoint_url': 'http://localhost:9000', 'access_key': 'minioadmin', 'secret_key': 'minioadmin', 'region': 'us-east-1', 'use_ssl': False, 'max_retry_attempts': 3, 'retry_mode': 'adaptive', 'connect_timeout': 10, 'read_timeout': 60, } logger = MagicMock() notification_handler = MagicMock() activities = Activities( postgres_config=postgres_config, mlflow_config=mlflow_config, minio_config=minio_config, logger=logger, notification_handler=notification_handler, ) await activities.shutdown() mock_experiment_tracking_init.close.assert_called_once() @patch('model_manager.activities.activities.ExperimentTracking.__init__') @patch('model_manager.activities.activities.MLFlow.__init__') @patch('model_manager.activities.activities.MinIO.__init__') @patch('model_manager.activities.activities.Training.__init__') def test___del___with_engine( mock_training_init, mock_minio_init, mock_mlflow_init, mock_experiment_tracking_init, ): """Test __del__ calls parent destructor when engine attribute exists.""" postgres_config = { 'host': 'localhost', 'port': 5432, 'user': 'postgres', 'password': 'postgres', 'dbname': 'postgres', 'min_connections': 1, 'max_connections': 10, } mlflow_config = {'host': 'localhost', 'port': 5000, 'username': 'mlflow', 'password': 'mlflow'} minio_config = { 'endpoint_url': 'http://localhost:9000', 'access_key': 'minioadmin', 'secret_key': 'minioadmin', 'region': 'us-east-1', 'use_ssl': False, 'max_retry_attempts': 3, 'retry_mode': 'adaptive', 'connect_timeout': 10, 'read_timeout': 60, } logger = MagicMock() notification_handler = MagicMock() activities = Activities( postgres_config=postgres_config, mlflow_config=mlflow_config, minio_config=minio_config, logger=logger, notification_handler=notification_handler, ) # Add engine attribute to simulate Postgres initialization activities.engine = MagicMock() # Create a mock __del__ that will be detected by hasattr mock_parent_del = MagicMock() # Patch both the class and the instance to ensure super().__del__ exists and is callable with patch.object(ExperimentTracking, '__del__', mock_parent_del, create=True): # Trigger __del__ activities.__del__() # Verify parent __del__ was called mock_parent_del.assert_called_once() @patch('model_manager.activities.activities.ExperimentTracking.__init__') @patch('model_manager.activities.activities.MLFlow.__init__') @patch('model_manager.activities.activities.MinIO.__init__') @patch('model_manager.activities.activities.Training.__init__') def test___del___without_engine( mock_training_init, mock_minio_init, mock_mlflow_init, mock_experiment_tracking_init, ): """Test __del__ does not call parent destructor when engine attribute is missing.""" postgres_config = { 'host': 'localhost', 'port': 5432, 'user': 'postgres', 'password': 'postgres', 'dbname': 'postgres', 'min_connections': 1, 'max_connections': 10, } mlflow_config = {'host': 'localhost', 'port': 5000, 'username': 'mlflow', 'password': 'mlflow'} minio_config = { 'endpoint_url': 'http://localhost:9000', 'access_key': 'minioadmin', 'secret_key': 'minioadmin', 'region': 'us-east-1', 'use_ssl': False, 'max_retry_attempts': 3, 'retry_mode': 'adaptive', 'connect_timeout': 10, 'read_timeout': 60, } logger = MagicMock() notification_handler = MagicMock() activities = Activities( postgres_config=postgres_config, mlflow_config=mlflow_config, minio_config=minio_config, logger=logger, notification_handler=notification_handler, ) # Ensure engine attribute does NOT exist if hasattr(activities, 'engine'): delattr(activities, 'engine') # Mock super().__del__ to track if it's called with patch.object(ExperimentTracking, '__del__', MagicMock()) as mock_parent_del: # Trigger __del__ activities.__del__() # Verify parent __del__ was NOT called mock_parent_del.assert_not_called() @patch('model_manager.activities.activities.ExperimentTracking.__init__') @patch('model_manager.activities.activities.MLFlow.__init__') @patch('model_manager.activities.activities.MinIO.__init__') @patch('model_manager.activities.activities.Training.__init__') def test___del___handles_exception_gracefully( mock_training_init, mock_minio_init, mock_mlflow_init, mock_experiment_tracking_init, ): """Test __del__ handles exceptions from parent destructor gracefully.""" postgres_config = { 'host': 'localhost', 'port': 5432, 'user': 'postgres', 'password': 'postgres', 'dbname': 'postgres', 'min_connections': 1, 'max_connections': 10, } mlflow_config = {'host': 'localhost', 'port': 5000, 'username': 'mlflow', 'password': 'mlflow'} minio_config = { 'endpoint_url': 'http://localhost:9000', 'access_key': 'minioadmin', 'secret_key': 'minioadmin', 'region': 'us-east-1', 'use_ssl': False, 'max_retry_attempts': 3, 'retry_mode': 'adaptive', 'connect_timeout': 10, 'read_timeout': 60, } logger = MagicMock() notification_handler = MagicMock() activities = Activities( postgres_config=postgres_config, mlflow_config=mlflow_config, minio_config=minio_config, logger=logger, notification_handler=notification_handler, ) # Add engine attribute activities.engine = MagicMock() # Mock super().__del__ to raise an exception mock_parent_del = MagicMock(side_effect=RuntimeError('Cleanup failed')) with patch.object(ExperimentTracking, '__del__', mock_parent_del): # Trigger __del__ - should not raise exception try: activities.__del__() # Test passes if no exception is raised except Exception as e: # Test fails if exception propagates raise AssertionError(f'__del__ should not raise exception, but raised: {e}') from e @patch('model_manager.activities.activities.ExperimentTracking.__init__') @patch('model_manager.activities.activities.MLFlow.__init__') @patch('model_manager.activities.activities.MinIO.__init__') @patch('model_manager.activities.activities.Training.__init__') def test___del___when_parent_has_no_del( mock_training_init, mock_minio_init, mock_mlflow_init, mock_experiment_tracking_init, ): """Test __del__ handles case when parent class has no __del__ method.""" postgres_config = { 'host': 'localhost', 'port': 5432, 'user': 'postgres', 'password': 'postgres', 'dbname': 'postgres', 'min_connections': 1, 'max_connections': 10, } mlflow_config = {'host': 'localhost', 'port': 5000, 'username': 'mlflow', 'password': 'mlflow'} minio_config = { 'endpoint_url': 'http://localhost:9000', 'access_key': 'minioadmin', 'secret_key': 'minioadmin', 'region': 'us-east-1', 'use_ssl': False, 'max_retry_attempts': 3, 'retry_mode': 'adaptive', 'connect_timeout': 10, 'read_timeout': 60, } logger = MagicMock() notification_handler = MagicMock() activities = Activities( postgres_config=postgres_config, mlflow_config=mlflow_config, minio_config=minio_config, logger=logger, notification_handler=notification_handler, ) # Add engine attribute activities.engine = MagicMock() # Remove __del__ from parent to simulate it not existing with patch.object(ExperimentTracking, '__del__', create=False): # Trigger __del__ - should not raise exception try: activities.__del__() # Test passes if no exception is raised except Exception as e: # Test fails if exception propagates raise AssertionError( f'__del__ should handle missing parent __del__, but raised: {e}' ) from e @patch('model_manager.activities.activities.ExperimentTracking.__init__') @patch('model_manager.activities.activities.MLFlow.__init__') @patch('model_manager.activities.activities.MinIO.__init__') @patch('model_manager.activities.activities.Training.__init__') def test___del___calls_super_successfully( mock_training_init, mock_minio_init, mock_mlflow_init, mock_experiment_tracking_init, ): """Test __del__ successfully calls super().__del__() when it exists - covers line 118.""" postgres_config = { 'host': 'localhost', 'port': 5432, 'user': 'postgres', 'password': 'postgres', 'dbname': 'postgres', 'min_connections': 1, 'max_connections': 10, } mlflow_config = {'host': 'localhost', 'port': 5000, 'username': 'mlflow', 'password': 'mlflow'} minio_config = { 'endpoint_url': 'http://localhost:9000', 'access_key': 'minioadmin', 'secret_key': 'minioadmin', 'region': 'us-east-1', 'use_ssl': False, 'max_retry_attempts': 3, 'retry_mode': 'adaptive', 'connect_timeout': 10, 'read_timeout': 60, } logger = MagicMock() notification_handler = MagicMock() # Mock all parent __init__ methods to return None mock_experiment_tracking_init.return_value = None mock_mlflow_init.return_value = None mock_minio_init.return_value = None mock_training_init.return_value = None activities = Activities( postgres_config=postgres_config, mlflow_config=mlflow_config, minio_config=minio_config, logger=logger, notification_handler=notification_handler, ) # Add engine attribute to simulate Postgres initialization activities.engine = MagicMock() # Track if super().__del__() was actually called super_del_called = [] def mock_super_del(self): """Mock parent __del__ that tracks when it's called.""" super_del_called.append(True) # Patch ExperimentTracking.__del__ to exist and be callable with patch.object(ExperimentTracking, '__del__', mock_super_del, create=True): # Trigger __del__ - this should execute line 118: super().__del__() activities.__del__() # Verify that super().__del__() was actually called (line 118 executed) assert len(super_del_called) == 1, 'super().__del__() should have been called once' @patch('model_manager.activities.activities.ExperimentTracking.__init__') @patch('model_manager.activities.activities.MLFlow.__init__') @patch('model_manager.activities.activities.MinIO.__init__') @patch('model_manager.activities.activities.Training.__init__') def test___del___when_super_has_no_del_method( mock_training_init, mock_minio_init, mock_mlflow_init, mock_experiment_tracking_init, ): """Test __del__ handles case when hasattr(super(), '__del__') returns False - covers line 118 false branch.""" postgres_config = { 'host': 'localhost', 'port': 5432, 'user': 'postgres', 'password': 'postgres', 'dbname': 'postgres', 'min_connections': 1, 'max_connections': 10, } mlflow_config = {'host': 'localhost', 'port': 5000, 'username': 'mlflow', 'password': 'mlflow'} minio_config = { 'endpoint_url': 'http://localhost:9000', 'access_key': 'minioadmin', 'secret_key': 'minioadmin', 'region': 'us-east-1', 'use_ssl': False, 'max_retry_attempts': 3, 'retry_mode': 'adaptive', 'connect_timeout': 10, 'read_timeout': 60, } logger = MagicMock() notification_handler = MagicMock() # Mock all __init__ methods to return None mock_experiment_tracking_init.return_value = None mock_mlflow_init.return_value = None mock_minio_init.return_value = None mock_training_init.return_value = None activities = Activities( postgres_config=postgres_config, mlflow_config=mlflow_config, minio_config=minio_config, logger=logger, notification_handler=notification_handler, ) # Add engine attribute to pass the first hasattr check (line 115) activities.engine = MagicMock() # Create a mock class without __del__ method to simulate super() not having __del__ class MockSuperWithoutDel: """Mock class that explicitly does not have __del__ method.""" pass # Patch super() to return an instance that doesn't have __del__ mock_super_instance = MockSuperWithoutDel() with patch('builtins.super', return_value=mock_super_instance): # Trigger __del__ - should handle the case when hasattr(super(), '__del__') is False try: activities.__del__() # Test passes - the false branch of line 118 was executed without error except Exception as e: # Test fails if exception propagates raise AssertionError( f'__del__ should handle super() without __del__ method, but raised: {e}' ) from e