983 lines
38 KiB
Python
983 lines
38 KiB
Python
"""Unit tests for ModelRepository with 100% coverage."""
|
|
|
|
import os
|
|
import shutil
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def cleanup_temp_directories():
|
|
"""Clean up temporary directories after each test."""
|
|
# Get the temp directory path
|
|
current_file_dir = os.path.dirname(os.path.abspath(__file__))
|
|
model_manager_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_file_dir)))
|
|
temp_dir = os.path.join(model_manager_dir, 'reports', 'temp')
|
|
|
|
# Run the test
|
|
yield
|
|
|
|
# Clean up after test
|
|
if os.path.exists(temp_dir):
|
|
for item in os.listdir(temp_dir):
|
|
item_path = os.path.join(temp_dir, item)
|
|
if os.path.isdir(item_path) and item.startswith('test_run_'):
|
|
try:
|
|
shutil.rmtree(item_path)
|
|
except (OSError, PermissionError):
|
|
# Ignore cleanup errors
|
|
pass
|
|
|
|
|
|
@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.params.rem_static_win = True
|
|
result.params.static_threshold = None
|
|
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
|
|
)
|
|
|
|
# Reset mock after initialization to focus on method-specific calls
|
|
mock_logger.reset_mock()
|
|
|
|
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
|
|
)
|
|
|
|
# Reset mock after initialization to focus on method-specific calls
|
|
mock_logger.reset_mock()
|
|
|
|
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
|
|
)
|
|
|
|
# Reset mock after initialization to focus on method-specific calls
|
|
mock_logger.reset_mock()
|
|
|
|
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
|
|
)
|
|
|
|
# Reset mock after initialization to focus on method-specific calls
|
|
mock_logger.reset_mock()
|
|
|
|
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')
|
|
def test_init_artifacts_data_none_y_train_pred(
|
|
mock_model_serving_class, mock_logger, mock_train_result
|
|
):
|
|
"""Test _init_artifacts_data raises ValueError when y_train_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_train_pred = None
|
|
|
|
with pytest.raises(ValueError, match='Training 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.normpath(
|
|
os.path.join('/tmp/reports', 'temp', 'test_run_20240101_120000_123456') # noqa: S108
|
|
)
|
|
assert os.path.normpath(result) == expected_path
|
|
mock_makedirs.assert_called_once()
|
|
call_path = mock_makedirs.call_args[0][0]
|
|
assert os.path.normpath(call_path) == expected_path
|
|
assert mock_makedirs.call_args[1] == {'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_with_static_threshold_value(
|
|
mock_exists, mock_model_serving_class, mock_logger, mock_train_result
|
|
):
|
|
"""Test _save_run logs static_threshold when rem_static_win is True and value is set."""
|
|
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 static_threshold to a specific value
|
|
mock_train_result.params.rem_static_win = True
|
|
mock_train_result.params.static_threshold = 500
|
|
mock_train_result.equation_path = None
|
|
|
|
# Mock all path.exists calls to return True
|
|
mock_exists.return_value = True
|
|
|
|
repo._save_run(mock_train_result)
|
|
|
|
# Verify static_threshold was logged with the correct value
|
|
log_param_calls = {
|
|
call[0][0]: call[0][1] for call in mock_model_serving_instance.log_param.call_args_list
|
|
}
|
|
assert log_param_calls['static_threshold'] == 500
|
|
|
|
|
|
@patch('model_manager.utils.repository.model_repository.ModelServing')
|
|
@patch('model_manager.utils.repository.model_repository.path.exists')
|
|
def test_save_run_with_rem_static_win_false(
|
|
mock_exists, mock_model_serving_class, mock_logger, mock_train_result
|
|
):
|
|
"""Test _save_run logs static_threshold as None when rem_static_win is False."""
|
|
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 rem_static_win to False
|
|
mock_train_result.params.rem_static_win = False
|
|
mock_train_result.params.static_threshold = 500 # Should be ignored
|
|
mock_train_result.equation_path = None
|
|
|
|
# Mock all path.exists calls to return True
|
|
mock_exists.return_value = True
|
|
|
|
repo._save_run(mock_train_result)
|
|
|
|
# Verify static_threshold was logged as None
|
|
log_param_calls = {
|
|
call[0][0]: call[0][1] for call in mock_model_serving_instance.log_param.call_args_list
|
|
}
|
|
assert log_param_calls['static_threshold'] is None
|
|
|
|
|
|
@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
|
|
mock_exists.return_value = True # Mock reports directory exists
|
|
|
|
# Mock _create_run_directory to avoid creating real directories
|
|
with patch.object(repo, '_create_run_directory') as mock_create_dir:
|
|
mock_create_dir.return_value = '/mock/run/dir'
|
|
|
|
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
|
|
|
|
# Mock _create_run_directory to avoid creating real directories
|
|
with patch.object(repo, '_create_run_directory') as mock_create_dir:
|
|
mock_create_dir.return_value = '/mock/run/dir'
|
|
|
|
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.path.exists')
|
|
@patch('model_manager.utils.repository.model_repository.path.join')
|
|
def test_generate_artifacts_success(
|
|
mock_join, mock_exists, mock_model_serving_class, mock_logger, mock_train_result
|
|
):
|
|
"""Test _generate_artifacts success case 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 path.join to return predictable paths
|
|
def join_side_effect(*args):
|
|
return '/'.join(args)
|
|
|
|
mock_join.side_effect = join_side_effect
|
|
mock_exists.return_value = True # Both reports dir and header.html exist
|
|
|
|
# Mock the internal methods to avoid actual file operations
|
|
with (
|
|
patch.object(repo, '_setup_run_directory') as mock_setup,
|
|
patch.object(repo, '_generate_report') as mock_generate_report,
|
|
patch.object(repo, '_init_artifacts_data') as mock_init_data,
|
|
patch.object(repo, '_get_reports_directory') as mock_get_reports_dir,
|
|
patch.object(repo, '_create_run_directory') as mock_create_run_dir,
|
|
):
|
|
# Setup mocks
|
|
mock_init_data.return_value = (pd.DataFrame(), pd.DataFrame())
|
|
mock_get_reports_dir.return_value = '/reports'
|
|
mock_create_run_dir.return_value = '/reports/run_1'
|
|
mock_generate_report.return_value = mock_train_result
|
|
|
|
# Call the method
|
|
result = repo._generate_artifacts(mock_train_result)
|
|
|
|
# Verify the methods on lines 147-148 were called
|
|
mock_setup.assert_called_once_with('/reports/run_1', '/reports/header.html')
|
|
mock_generate_report.assert_called_once()
|
|
|
|
# Verify result
|
|
assert result == mock_train_result
|