SIENTIAPDE-1241: Add unit tests for Activities class and enhance validation script with fix and test options

This commit introduces a new test suite for the Activities class, achieving 100% coverage. Additionally, the validation script (validate.sh) is enhanced with new options:

- --fix: Automatically fixes code formatting and linting issues using Ruff.
- --only-tests: Runs only the unit tests, skipping other validation steps.

The validation script now also supports skipping tests and provides more informative output.
This commit is contained in:
Bruno Domingues
2025-10-22 23:38:01 -03:00
parent f07bc8ff30
commit fc2fe33b59
2 changed files with 384 additions and 29 deletions

View File

@@ -0,0 +1,302 @@
"""Unit tests for Activities class with 100% coverage."""
import asyncio
from unittest.mock import MagicMock, patch
import pytest
@pytest.fixture
def mock_logger():
"""Create a mock logger."""
return MagicMock()
@pytest.fixture
def mock_notification_handler():
"""Create a mock notification handler."""
return MagicMock()
@pytest.fixture
def postgres_config():
"""Create a valid PostgreSQL configuration."""
return {
'host': 'localhost',
'port': 5432,
'user': 'testuser',
'password': 'testpass',
'dbname': 'testdb',
'min_connections': 1,
'max_connections': 10,
}
@pytest.fixture
def mlflow_config():
"""Create a valid MLFlow configuration."""
return {
'url': 'http://mlflow:5080',
'username': 'aignosi',
'password': 'aignosi',
}
@pytest.fixture
def minio_config():
"""Create a valid MinIO configuration."""
return {
'endpoint_url': 'http://minio:9000',
'access_key': 'minioadmin',
'secret_key': 'minioadmin',
'region': 'us-east-1',
'use_ssl': False,
'max_retry_attempts': 3,
'retry_mode': 'standard',
'connect_timeout': 5,
'read_timeout': 5,
}
@patch('model_manager.activities.activities.ExperimentTracking.__init__', return_value=None)
@patch('model_manager.activities.activities.Training.__init__', return_value=None)
@patch('model_manager.activities.activities.ModelRepository')
@patch('model_manager.activities.activities.StorageRepository')
def test_activities_init_success(
mock_storage_repo,
mock_model_repo,
mock_training_init,
mock_et_init,
postgres_config,
mlflow_config,
minio_config,
mock_logger,
mock_notification_handler,
):
"""Test successful initialization of Activities."""
from model_manager.activities.activities import Activities
activities = Activities(
postgres_config=postgres_config,
mlflow_config=mlflow_config,
minio_config=minio_config,
logger=mock_logger,
notification_handler=mock_notification_handler,
)
mock_et_init.assert_called_once()
assert mock_et_init.call_args[1]['host'] == postgres_config['host']
assert mock_et_init.call_args[1]['port'] == postgres_config['port']
assert mock_et_init.call_args[1]['user'] == postgres_config['user']
assert mock_et_init.call_args[1]['password'] == postgres_config['password']
assert mock_et_init.call_args[1]['dbname'] == postgres_config['dbname']
assert mock_et_init.call_args[1]['min_connections'] == postgres_config['min_connections']
assert mock_et_init.call_args[1]['max_connections'] == postgres_config['max_connections']
assert mock_et_init.call_args[1]['logger'] is mock_logger
assert mock_et_init.call_args[1]['notification_handler'] is mock_notification_handler
mock_model_repo.assert_called_once_with(
url=mlflow_config['url'],
username=mlflow_config['username'],
password=mlflow_config['password'],
logger=mock_logger,
)
mock_storage_repo.assert_called_once_with(
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=mock_logger,
)
mock_training_init.assert_called_once()
assert mock_training_init.call_args[1]['model_repository'] is mock_model_repo.return_value
assert mock_training_init.call_args[1]['storage_repository'] is mock_storage_repo.return_value
assert mock_training_init.call_args[1]['logger'] is mock_logger
assert mock_training_init.call_args[1]['notification_handler'] is mock_notification_handler
assert hasattr(activities, 'model_repository')
assert hasattr(activities, 'storage_repository')
@patch('model_manager.activities.activities.ExperimentTracking.__init__', return_value=None)
@patch('model_manager.activities.activities.ExperimentTracking.close')
@patch('model_manager.activities.activities.Training.__init__', return_value=None)
@patch('model_manager.activities.activities.ModelRepository')
@patch('model_manager.activities.activities.StorageRepository')
def test_activities_shutdown(
mock_storage_repo,
mock_model_repo,
mock_training_init,
mock_et_close,
mock_et_init,
postgres_config,
mlflow_config,
minio_config,
mock_logger,
mock_notification_handler,
):
"""Test Activities.shutdown() calls ExperimentTracking.close()."""
from model_manager.activities.activities import Activities
activities = Activities(
postgres_config=postgres_config,
mlflow_config=mlflow_config,
minio_config=minio_config,
logger=mock_logger,
notification_handler=mock_notification_handler,
)
asyncio.run(activities.shutdown())
mock_et_close.assert_called_once_with(activities)
@patch('model_manager.activities.activities.ExperimentTracking.__init__', return_value=None)
@patch('model_manager.activities.activities.Training.__init__', return_value=None)
@patch('model_manager.activities.activities.ModelRepository')
@patch('model_manager.activities.activities.StorageRepository')
def test_activities_del_without_engine(
mock_storage_repo,
mock_model_repo,
mock_training_init,
mock_et_init,
postgres_config,
mlflow_config,
minio_config,
mock_logger,
mock_notification_handler,
):
"""Test __del__ when engine attribute does not exist."""
from model_manager.activities.activities import Activities
activities = Activities(
postgres_config=postgres_config,
mlflow_config=mlflow_config,
minio_config=minio_config,
logger=mock_logger,
notification_handler=mock_notification_handler,
)
if hasattr(activities, 'engine'):
delattr(activities, 'engine')
activities.__del__()
@patch('model_manager.activities.activities.ExperimentTracking.__init__', return_value=None)
@patch('model_manager.activities.activities.Training.__init__', return_value=None)
@patch('model_manager.activities.activities.ModelRepository')
@patch('model_manager.activities.activities.StorageRepository')
def test_activities_del_with_engine_no_super_del(
mock_storage_repo,
mock_model_repo,
mock_training_init,
mock_et_init,
postgres_config,
mlflow_config,
minio_config,
mock_logger,
mock_notification_handler,
):
"""Test __del__ when engine exists but super has no __del__."""
from model_manager.activities.activities import Activities
activities = Activities(
postgres_config=postgres_config,
mlflow_config=mlflow_config,
minio_config=minio_config,
logger=mock_logger,
notification_handler=mock_notification_handler,
)
activities.engine = MagicMock()
with patch('builtins.super') as mock_super:
mock_super_instance = MagicMock()
del mock_super_instance.__del__
mock_super.return_value = mock_super_instance
activities.__del__()
@patch('model_manager.activities.activities.ExperimentTracking.__init__', return_value=None)
@patch('model_manager.activities.activities.Training.__init__', return_value=None)
@patch('model_manager.activities.activities.ModelRepository')
@patch('model_manager.activities.activities.StorageRepository')
def test_activities_del_with_engine_and_super_del(
mock_storage_repo,
mock_model_repo,
mock_training_init,
mock_et_init,
postgres_config,
mlflow_config,
minio_config,
mock_logger,
mock_notification_handler,
):
"""Test __del__ when engine exists and super has __del__."""
from model_manager.activities.activities import Activities
activities = Activities(
postgres_config=postgres_config,
mlflow_config=mlflow_config,
minio_config=minio_config,
logger=mock_logger,
notification_handler=mock_notification_handler,
)
activities.engine = MagicMock()
mock_super_del = MagicMock()
class MockSuper:
def __del__(self):
mock_super_del()
with patch('builtins.super', return_value=MockSuper()):
activities.__del__()
mock_super_del.assert_called_once()
@patch('model_manager.activities.activities.ExperimentTracking.__init__', return_value=None)
@patch('model_manager.activities.activities.Training.__init__', return_value=None)
@patch('model_manager.activities.activities.ModelRepository')
@patch('model_manager.activities.activities.StorageRepository')
def test_activities_del_with_engine_exception_caught(
mock_storage_repo,
mock_model_repo,
mock_training_init,
mock_et_init,
postgres_config,
mlflow_config,
minio_config,
mock_logger,
mock_notification_handler,
):
"""Test __del__ catches exceptions when super().__del__() raises."""
from model_manager.activities.activities import Activities
activities = Activities(
postgres_config=postgres_config,
mlflow_config=mlflow_config,
minio_config=minio_config,
logger=mock_logger,
notification_handler=mock_notification_handler,
)
activities.engine = MagicMock()
class MockSuperWithError:
def __del__(self):
raise RuntimeError('Test error')
with patch('builtins.super', return_value=MockSuperWithError()):
activities.__del__()