Files
sientia-dataops-model-manager/tests/utils/repository/test_model_repository.py
2025-10-30 09:58:46 -03:00

868 lines
33 KiB
Python

"""Unit tests for ModelRepository with 100% coverage."""
import os
from unittest.mock import MagicMock, patch
import numpy as np
import pandas as pd
import pytest
@pytest.fixture
def mock_logger():
"""Create a mock logger."""
return MagicMock()
@pytest.fixture
def mock_train_result():
"""Create a mock TrainModelResult."""
result = MagicMock()
result.params = MagicMock()
result.params.experiment_name = 'test_experiment'
result.params.experiment_run_id = 1
result.params.target_variable = 'target'
result.params.variable_columns = ['var1', 'var2']
result.params.lag_train = 5
result.params.lag_val = 3
result.params.window = 10
result.params.low_lim = {'var1': 0.0}
result.params.upp_lim = {'var1': 10.0}
result.params.include_ar = False
result.params.train_size = 80
result.params.removed_intervals = []
result.run_name = 'test_run'
result.run_dir = '/tmp/test_run' # noqa: S108
result.report_path = '/tmp/test_run/report.html' # noqa: S108
result.train_data_path = '/tmp/test_run/train_data.csv' # noqa: S108
result.test_data_path = '/tmp/test_run/test_data.csv' # noqa: S108
result.mse_val = 0.5
result.r2_val = 0.9
result.mae_val = 0.3
result.scaler_dict = {'scaler': 'standard'}
result.process_data = MagicMock()
result.regr = MagicMock()
result.regr.predict = MagicMock(return_value=np.array([1.0, 2.0, 3.0]))
result.x_train = pd.DataFrame({'var1': [1, 2, 3]})
result.y_train = pd.Series([1.0, 2.0, 3.0], name='target')
result.x_test = pd.DataFrame({'var1': [4, 5, 6]})
result.y_test = pd.Series([4.0, 5.0, 6.0], name='target')
result.y_pred = np.array([4.1, 5.1, 6.1])
return result
@patch('model_manager.utils.repository.model_repository.ModelServing')
def test_model_repository_init(mock_model_serving_class, mock_logger):
"""Test ModelRepository initialization."""
from model_manager.utils.repository.model_repository import ModelRepository
mock_model_serving_instance = MagicMock()
mock_model_serving_class.return_value = mock_model_serving_instance
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_model_serving_class.assert_called_once_with(
tracking_uri='http://mlflow.test', username='user', password='pass'
)
assert repo.model_serving is mock_model_serving_instance
assert repo.logger is mock_logger
@patch('model_manager.utils.repository.model_repository.ModelServing')
def test_save_model_success(mock_model_serving_class, mock_logger, mock_train_result):
"""Test save_model successfully saves model."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
repo._get_next_run_name = MagicMock(return_value='test_experiment-1')
repo._generate_artifacts = MagicMock(return_value=mock_train_result)
repo._save_run = MagicMock()
result = repo.save_model(mock_train_result)
repo._get_next_run_name.assert_called_once_with('test_experiment')
repo._generate_artifacts.assert_called_once()
repo._save_run.assert_called_once_with(mock_train_result)
mock_logger.info.assert_called_once()
assert result is mock_train_result
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.os.path.exists')
@patch('model_manager.utils.repository.model_repository.shutil.rmtree')
def test_cleanup_run_directory_exists(
mock_rmtree, mock_exists, mock_model_serving_class, mock_logger
):
"""Test cleanup_run_directory when directory exists."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_exists.return_value = True
repo.cleanup_run_directory('/tmp/test_run') # noqa: S108
mock_exists.assert_called_once_with('/tmp/test_run') # noqa: S108
mock_rmtree.assert_called_once_with('/tmp/test_run') # noqa: S108
mock_logger.info.assert_called_once()
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.os.path.exists')
def test_cleanup_run_directory_not_exists(mock_exists, mock_model_serving_class, mock_logger):
"""Test cleanup_run_directory when directory doesn't exist."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_exists.return_value = False
repo.cleanup_run_directory('/tmp/test_run') # noqa: S108
mock_exists.assert_called_once_with('/tmp/test_run') # noqa: S108
mock_logger.info.assert_called_once()
@patch('model_manager.utils.repository.model_repository.ModelServing')
def test_cleanup_run_directory_empty_path(mock_model_serving_class, mock_logger):
"""Test cleanup_run_directory with empty path."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
repo.cleanup_run_directory('')
mock_logger.info.assert_called_once_with('No run directory specified, skipping cleanup')
@patch('model_manager.utils.repository.model_repository.ModelServing')
def test_get_next_run_name_no_existing_runs(mock_model_serving_class, mock_logger):
"""Test _get_next_run_name when no runs exist."""
from model_manager.utils.repository.model_repository import ModelRepository
mock_model_serving_instance = MagicMock()
mock_model_serving_instance.search_runs_by_name.return_value = []
mock_model_serving_class.return_value = mock_model_serving_instance
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
result = repo._get_next_run_name('test_experiment')
assert result == 'test_experiment-1'
@patch('model_manager.utils.repository.model_repository.ModelServing')
def test_get_next_run_name_with_existing_runs(mock_model_serving_class, mock_logger):
"""Test _get_next_run_name when runs exist."""
from model_manager.utils.repository.model_repository import ModelRepository
mock_model_serving_instance = MagicMock()
mock_model_serving_instance.search_runs_by_name.return_value = [
MagicMock(),
MagicMock(),
MagicMock(),
]
mock_model_serving_class.return_value = mock_model_serving_instance
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
result = repo._get_next_run_name('test_experiment')
assert result == 'test_experiment-4'
@patch('model_manager.utils.repository.model_repository.ModelServing')
def test_get_reports_directory(mock_model_serving_class, mock_logger):
"""Test _get_reports_directory returns correct path."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
result = repo._get_reports_directory()
assert result.endswith(os.path.join('model_manager', 'reports'))
assert os.path.isabs(result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
def test_init_artifacts_data_success(mock_model_serving_class, mock_logger, mock_train_result):
"""Test _init_artifacts_data successfully prepares data."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
reference_data, current_data = repo._init_artifacts_data(mock_train_result)
# Check reference data
assert 'target' in reference_data.columns
assert 'prediction' in reference_data.columns
assert len(reference_data) == 3
# Check current data
assert 'target' in current_data.columns
assert 'prediction' in current_data.columns
assert len(current_data) == 3
@patch('model_manager.utils.repository.model_repository.ModelServing')
def test_init_artifacts_data_empty_x_train(
mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _init_artifacts_data raises ValueError when x_train is empty."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_train_result.x_train = pd.DataFrame()
with pytest.raises(ValueError, match='Training features .* are empty'):
repo._init_artifacts_data(mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
def test_init_artifacts_data_empty_y_train(
mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _init_artifacts_data raises ValueError when y_train is empty."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_train_result.y_train = pd.Series(dtype=float)
with pytest.raises(ValueError, match='Training target .* is empty'):
repo._init_artifacts_data(mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
def test_init_artifacts_data_none_y_pred(mock_model_serving_class, mock_logger, mock_train_result):
"""Test _init_artifacts_data raises ValueError when y_pred is None."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_train_result.y_pred = None
with pytest.raises(ValueError, match='Test predictions .* are None'):
repo._init_artifacts_data(mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.datetime')
@patch('model_manager.utils.repository.model_repository.makedirs')
def test_create_run_directory_success(
mock_makedirs, mock_datetime, mock_model_serving_class, mock_logger
):
"""Test _create_run_directory creates directory successfully."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_datetime.now.return_value.strftime.return_value = '20240101_120000_123456'
result = repo._create_run_directory('/tmp/reports', 'test_run') # noqa: S108
expected_path = os.path.join('/tmp/reports', 'test_run_20240101_120000_123456') # noqa: S108
assert result == expected_path
mock_makedirs.assert_called_once_with(expected_path, exist_ok=True)
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.makedirs')
def test_create_run_directory_permission_error(
mock_makedirs, mock_model_serving_class, mock_logger
):
"""Test _create_run_directory raises PermissionError."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_makedirs.side_effect = PermissionError('Permission denied')
with pytest.raises(PermissionError, match='Permission denied when creating directory'):
repo._create_run_directory('/tmp/reports', 'test_run') # noqa: S108
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.shutil.copy')
@patch('builtins.open', create=True)
def test_setup_run_directory_success(mock_open, mock_copy, mock_model_serving_class, mock_logger):
"""Test _setup_run_directory creates files successfully."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
repo._setup_run_directory('/tmp/test_run', '/tmp/header.html') # noqa: S108
# Check that empty files were created
assert mock_open.call_count == 3
mock_copy.assert_called_once()
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.json.dump')
@patch('model_manager.utils.repository.model_repository.Reports')
@patch('builtins.open', create=True)
def test_generate_report_with_equation(
mock_open,
mock_reports_class,
mock_json_dump,
mock_model_serving_class,
mock_logger,
mock_train_result,
):
"""Test _generate_report creates equation JSON artifact."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
# Add equation to train result
mock_train_result.equation = {
'target_variable': 'target',
'coefficients': {'var1': 1.5, 'var2': -0.75},
'intercept': 10.5,
'equation_string': 'target = 10.5 + 1.5 * var1 + -0.75 * var2',
'latex_equation': 'target = 10.5 + 1.5 \\cdot var1 + -0.75 \\cdot var2',
'model_type': 'Linear Regression',
}
reference_data = pd.DataFrame({'var1': [1, 2], 'var2': [3, 4], 'target': [5, 6]})
current_data = pd.DataFrame({'var1': [7, 8], 'var2': [9, 10], 'target': [11, 12]})
# Mock DataFrame.to_csv to avoid file I/O
with patch.object(pd.DataFrame, 'to_csv'):
result = repo._generate_report(reference_data, current_data, mock_train_result)
# Verify equation path was set
assert result.equation_path == os.path.join(mock_train_result.run_dir, 'model_equation.json')
# Verify JSON was written
mock_json_dump.assert_called()
call_args = mock_json_dump.call_args
assert call_args[0][0] == mock_train_result.equation
assert call_args[1]['indent'] == 2
assert call_args[1]['ensure_ascii'] is False
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.Reports')
@patch('builtins.open', create=True)
def test_generate_report_without_equation(
mock_open, mock_reports_class, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _generate_report works without equation."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
# No equation
mock_train_result.equation = None
# Remove equation_path if it exists from fixture
if hasattr(mock_train_result, 'equation_path'):
del mock_train_result.equation_path
reference_data = pd.DataFrame({'var1': [1, 2], 'var2': [3, 4], 'target': [5, 6]})
current_data = pd.DataFrame({'var1': [7, 8], 'var2': [9, 10], 'target': [11, 12]})
# Mock DataFrame.to_csv to avoid file I/O
with patch.object(pd.DataFrame, 'to_csv'):
result = repo._generate_report(reference_data, current_data, mock_train_result)
# Verify equation section was not executed (equation_path not set)
# Since equation is None, the equation block should not run
assert result == mock_train_result
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.path.exists')
def test_save_run_with_equation(
mock_exists, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _save_run logs equation artifact."""
from model_manager.utils.repository.model_repository import ModelRepository
mock_model_serving_instance = MagicMock()
mock_model_serving_class.return_value = mock_model_serving_instance
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
# Set equation path
mock_train_result.equation_path = '/tmp/test_run/model_equation.json' # noqa: S108
# Mock all path.exists calls to return True
mock_exists.return_value = True
repo._save_run(mock_train_result)
# Verify equation artifact was logged
logged_artifacts = [
call[0][0] for call in mock_model_serving_instance.log_artifact.call_args_list
]
assert '/tmp/test_run/model_equation.json' in logged_artifacts # noqa: S108
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.path.exists')
def test_save_run_without_equation(
mock_exists, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _save_run works without equation."""
from model_manager.utils.repository.model_repository import ModelRepository
mock_model_serving_instance = MagicMock()
mock_model_serving_class.return_value = mock_model_serving_instance
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
# No equation
mock_train_result.equation_path = None
# Mock path.exists to return True for required artifacts
mock_exists.return_value = True
repo._save_run(mock_train_result)
# Verify only 3 artifacts were logged (report, train_data, test_data)
assert mock_model_serving_instance.log_artifact.call_count == 3
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.path.exists')
def test_save_run_missing_report(
mock_exists, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _save_run raises ValueError when report is missing."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
# Mock report doesn't exist
def exists_side_effect(path):
return not path.endswith('report.html')
mock_exists.side_effect = exists_side_effect
with pytest.raises(ValueError, match='Report file does not exist'):
repo._save_run(mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.path.exists')
def test_save_run_none_metrics(
mock_exists, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _save_run raises ValueError when metrics are None."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_train_result.mse_val = None
# Mock all paths exist so we reach the metrics check
mock_exists.return_value = True
with pytest.raises(ValueError, match='One or more metrics .* are None'):
repo._save_run(mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.path.exists')
def test_save_run_missing_train_data(
mock_exists, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _save_run raises ValueError when train data is missing."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
# Mock train_data doesn't exist
def exists_side_effect(path):
return not path.endswith('train_data.csv')
mock_exists.side_effect = exists_side_effect
with pytest.raises(ValueError, match='Training data file does not exist'):
repo._save_run(mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.path.exists')
def test_save_run_missing_test_data(
mock_exists, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _save_run raises ValueError when test data is missing."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
# Mock test_data doesn't exist
def exists_side_effect(path):
if path.endswith('test_data.csv'):
return False
return True
mock_exists.side_effect = exists_side_effect
with pytest.raises(ValueError, match='Test data file does not exist'):
repo._save_run(mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
def test_init_artifacts_data_empty_x_test(mock_model_serving_class, mock_logger, mock_train_result):
"""Test _init_artifacts_data raises ValueError when x_test is empty."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_train_result.x_test = pd.DataFrame()
with pytest.raises(ValueError, match='Test features .* are empty'):
repo._init_artifacts_data(mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
def test_init_artifacts_data_empty_y_test(mock_model_serving_class, mock_logger, mock_train_result):
"""Test _init_artifacts_data raises ValueError when y_test is empty."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_train_result.y_test = pd.Series(dtype=float)
with pytest.raises(ValueError, match='Test target .* is empty'):
repo._init_artifacts_data(mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.makedirs')
def test_create_run_directory_os_error(mock_makedirs, mock_model_serving_class, mock_logger):
"""Test _create_run_directory raises OSError."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_makedirs.side_effect = OSError('Disk full')
with pytest.raises(OSError, match='Failed to create directory'):
repo._create_run_directory('/tmp/reports', 'test_run') # noqa: S108
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.shutil.copy')
@patch('builtins.open', create=True)
def test_setup_run_directory_file_not_found(
mock_open, mock_copy, mock_model_serving_class, mock_logger
):
"""Test _setup_run_directory raises FileNotFoundError when header missing."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_copy.side_effect = FileNotFoundError('Header not found')
with pytest.raises(FileNotFoundError, match='Header file not found'):
repo._setup_run_directory('/tmp/test_run', '/tmp/header.html') # noqa: S108
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.shutil.copy')
@patch('builtins.open', create=True)
def test_setup_run_directory_permission_error(
mock_open, mock_copy, mock_model_serving_class, mock_logger
):
"""Test _setup_run_directory raises PermissionError."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_open.side_effect = PermissionError('Permission denied')
with pytest.raises(PermissionError, match='Permission denied when setting up directory'):
repo._setup_run_directory('/tmp/test_run', '/tmp/header.html') # noqa: S108
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.shutil.copy')
@patch('builtins.open', create=True)
def test_setup_run_directory_os_error(mock_open, mock_copy, mock_model_serving_class, mock_logger):
"""Test _setup_run_directory raises OSError."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_open.side_effect = OSError('Disk error')
with pytest.raises(OSError, match='Failed to setup run directory'):
repo._setup_run_directory('/tmp/test_run', '/tmp/header.html') # noqa: S108
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.Reports')
@patch('builtins.open', create=True)
def test_generate_report_value_error(
mock_open, mock_reports_class, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _generate_report raises ValueError on invalid data."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
# Create data that can't be converted to float64
reference_data = pd.DataFrame({'var1': ['invalid', 'data']})
current_data = pd.DataFrame({'var1': [1, 2]})
with pytest.raises(ValueError, match='Failed to convert data to float64'):
repo._generate_report(reference_data, current_data, mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.Reports')
@patch('builtins.open', create=True)
def test_generate_report_permission_error(
mock_open, mock_reports_class, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _generate_report raises PermissionError on write failure."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
reference_data = pd.DataFrame({'var1': [1, 2], 'var2': [3, 4], 'target': [5, 6]})
current_data = pd.DataFrame({'var1': [7, 8], 'var2': [9, 10], 'target': [11, 12]})
# Mock Reports to raise PermissionError
mock_reports_class.side_effect = PermissionError('Permission denied')
with pytest.raises(PermissionError, match='Permission denied when writing report files'):
repo._generate_report(reference_data, current_data, mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.Reports')
@patch('builtins.open', create=True)
def test_generate_report_os_error(
mock_open, mock_reports_class, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _generate_report raises OSError on write failure."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
reference_data = pd.DataFrame({'var1': [1, 2], 'var2': [3, 4], 'target': [5, 6]})
current_data = pd.DataFrame({'var1': [7, 8], 'var2': [9, 10], 'target': [11, 12]})
# Mock Reports to raise OSError
mock_reports_class.side_effect = OSError('Disk error')
with pytest.raises(OSError, match='Failed to generate report'):
repo._generate_report(reference_data, current_data, mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.Reports')
@patch('builtins.open', create=True)
def test_generate_report_none_run_dir(
mock_open, mock_reports_class, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _generate_report raises ValueError when run_dir is None."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_train_result.run_dir = None
reference_data = pd.DataFrame({'var1': [1, 2], 'var2': [3, 4], 'target': [5, 6]})
current_data = pd.DataFrame({'var1': [7, 8], 'var2': [9, 10], 'target': [11, 12]})
with pytest.raises(ValueError, match='run_dir is not set'):
repo._generate_report(reference_data, current_data, mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.path.exists')
def test_generate_artifacts_no_run_name(
mock_exists, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _generate_artifacts raises ValueError when run_name is not set."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
mock_train_result.run_name = None
with pytest.raises(ValueError, match='run_name must be set'):
repo._generate_artifacts(mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.path.exists')
def test_generate_artifacts_reports_dir_not_found(
mock_exists, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _generate_artifacts raises FileNotFoundError when reports dir missing."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
# Mock reports directory doesn't exist
mock_exists.return_value = False
with pytest.raises(FileNotFoundError, match='Reports directory does not exist'):
repo._generate_artifacts(mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.path.exists')
def test_generate_artifacts_header_not_found(
mock_exists, mock_model_serving_class, mock_logger, mock_train_result
):
"""Test _generate_artifacts raises FileNotFoundError when header missing."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
# Mock: reports dir exists, but header doesn't
def exists_side_effect(path):
if path.endswith('header.html'):
return False
return True
mock_exists.side_effect = exists_side_effect
with pytest.raises(FileNotFoundError, match='Header file does not exist'):
repo._generate_artifacts(mock_train_result)
@patch('model_manager.utils.repository.model_repository.ModelServing')
@patch('model_manager.utils.repository.model_repository.json.dump')
@patch('model_manager.utils.repository.model_repository.Reports')
@patch('model_manager.utils.repository.model_repository.path.exists')
@patch('model_manager.utils.repository.model_repository.path.join')
@patch('model_manager.utils.repository.model_repository.os.makedirs')
@patch('model_manager.utils.repository.model_repository.shutil.copy')
@patch('builtins.open', create=True)
def test_generate_artifacts_full_success_path(
mock_open,
mock_copy,
mock_makedirs,
mock_path_join,
mock_exists,
mock_reports_class,
mock_json_dump,
mock_model_serving_class,
mock_logger,
mock_train_result,
):
"""Test _generate_artifacts complete success path covering lines 147-148."""
from model_manager.utils.repository.model_repository import ModelRepository
repo = ModelRepository(
url='http://mlflow.test', username='user', password='pass', logger=mock_logger
)
# Mock all path operations
def join_side_effect(*args):
return '/'.join(str(arg) for arg in args)
mock_path_join.side_effect = join_side_effect
# Mock path.exists to return True for header file and other files
def exists_side_effect(path_arg):
if 'header.html' in str(path_arg):
return True # Header exists
if 'report.html' in str(path_arg):
return False # Report doesn't exist yet (will be created)
return False
mock_exists.side_effect = exists_side_effect
# Mock Reports class
mock_reports_instance = MagicMock()
mock_reports_instance.generate_report = MagicMock(return_value='<html>report</html>')
mock_reports_class.return_value = mock_reports_instance
# Mock DataFrame.to_csv to avoid actual file I/O
with patch.object(pd.DataFrame, 'to_csv'):
result = repo._generate_artifacts(mock_train_result)
# Verify the result is returned correctly
assert result == mock_train_result
# Verify _setup_run_directory was called (line 147)
mock_makedirs.assert_called_once()
mock_copy.assert_called_once()
# Verify _generate_report was called (line 148)
mock_reports_instance.generate_report.assert_called_once()