from unittest.mock import MagicMock, patch import numpy as np import pytest from pandas import DataFrame from model_manager.utils.repository.model_repository import MLFlowRepository @pytest.fixture def mlflow_repository(): with patch( 'model_manager.utils.repository.model_repository.ModelServing', autospec=True ) as mock_model_serving: mock_instance = mock_model_serving.return_value mock_instance.get_transformed_data = MagicMock() repo = MLFlowRepository( host='http://localhost:5000', username='admin', password='admin', logger=MagicMock() ) return repo metadata = { 'metadata': { 'model_id': 'test_model', 'model_name': 'test_model', 'workflow_name': 'test_workflow', 'schema_name': 'test_schedule', }, } # ========== Tests for Model Artifact Generation Methods ========== def test_get_next_run_name(mlflow_repository): """Test get_next_run_name generates correct run name based on existing runs.""" mlflow_repository.model_serving.search_runs_by_name.return_value = [ MagicMock(), MagicMock(), MagicMock(), ] result = mlflow_repository.get_next_run_name('test_experiment') mlflow_repository.model_serving.search_runs_by_name.assert_called_once_with( experiment_names=['test_experiment'], order_by=['start_time desc'] ) assert result == 'test_experiment-4' def test_get_next_run_name_first_run(mlflow_repository): """Test get_next_run_name for first run (no existing runs).""" mlflow_repository.model_serving.search_runs_by_name.return_value = [] result = mlflow_repository.get_next_run_name('test_experiment') assert result == 'test_experiment-1' @patch('model_manager.utils.repository.model_repository.path') def test_generate_artifacts_success(mock_path, mlflow_repository): """Test generate_artifacts successfully creates all artifacts.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult # Mock data params = MagicMock(spec=TrainModelParams) params.target_variable = 'target' params.variable_columns = ['feat1', 'feat2'] params.experiment_name = 'test_exp' data = MagicMock(spec=TrainModelResult) data.run_name = 'test_run-1' data.params = params data.x_train = DataFrame({'feat1': [1, 2], 'feat2': [3, 4]}) data.y_train = DataFrame({'target': [5, 6]}) data.x_test = DataFrame({'feat1': [7, 8], 'feat2': [9, 10]}) data.y_test = DataFrame({'target': [11, 12]}) data.regr = MagicMock() data.regr.predict = MagicMock(return_value=np.array([5.1, 6.1])) data.y_pred = np.array([11.1, 12.1]) # Mock path operations mock_path.exists.return_value = True mock_path.join.side_effect = lambda *args: '/'.join(args) # Mock private methods mlflow_repository._get_reports_directory = MagicMock(return_value='/reports') mlflow_repository._create_run_directory = MagicMock(return_value='/reports/test_run-1_20231010') mlflow_repository._setup_run_directory = MagicMock() mlflow_repository._generate_report = MagicMock(return_value=data) result = mlflow_repository.generate_artifacts(data) # Assertions mlflow_repository._get_reports_directory.assert_called_once() mlflow_repository._create_run_directory.assert_called_once_with('/reports', 'test_run-1') mlflow_repository._setup_run_directory.assert_called_once() mlflow_repository._generate_report.assert_called_once() assert result == data def test_generate_artifacts_missing_run_name(mlflow_repository): """Test generate_artifacts raises ValueError when run_name is not set.""" from model_manager.utils.models.train_model_result import TrainModelResult data = MagicMock(spec=TrainModelResult) data.run_name = None with pytest.raises(ValueError) as exc_info: mlflow_repository.generate_artifacts(data) assert 'run_name must be set before generating artifacts' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.path') def test_generate_artifacts_reports_directory_not_exists(mock_path, mlflow_repository): """Test generate_artifacts raises FileNotFoundError when reports directory doesn't exist.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) params.target_variable = 'target' data = MagicMock(spec=TrainModelResult) data.run_name = 'test_run-1' data.params = params data.x_train = DataFrame({'feat1': [1]}) data.y_train = DataFrame({'target': [2]}) data.x_test = DataFrame({'feat1': [3]}) data.y_test = DataFrame({'target': [4]}) data.regr = MagicMock() data.y_pred = np.array([4.1]) mlflow_repository._get_reports_directory = MagicMock(return_value='/reports') mock_path.exists.return_value = False with pytest.raises(FileNotFoundError) as exc_info: mlflow_repository.generate_artifacts(data) assert 'Reports directory does not exist' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.path') def test_generate_artifacts_header_file_not_exists(mock_path, mlflow_repository): """Test generate_artifacts raises FileNotFoundError when header.html doesn't exist.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) params.target_variable = 'target' data = MagicMock(spec=TrainModelResult) data.run_name = 'test_run-1' data.params = params data.x_train = DataFrame({'feat1': [1]}) data.y_train = DataFrame({'target': [2]}) data.x_test = DataFrame({'feat1': [3]}) data.y_test = DataFrame({'target': [4]}) data.regr = MagicMock() data.regr.predict = MagicMock(return_value=np.array([2.1])) data.y_pred = np.array([4.1]) mlflow_repository._get_reports_directory = MagicMock(return_value='/reports') mlflow_repository._create_run_directory = MagicMock(return_value='/reports/test_run-1_20231010') # First call returns True (reports dir exists), second returns False (header.html doesn't exist) mock_path.exists.side_effect = [True, False] mock_path.join.side_effect = lambda *args: '/'.join(args) with pytest.raises(FileNotFoundError) as exc_info: mlflow_repository.generate_artifacts(data) assert 'Header file does not exist' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.path') def test_save_run_success(mock_path, mlflow_repository): """Test save_run successfully logs all parameters, metrics, models, and artifacts.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) params.train_size = 80 params.removed_intervals = [(1, 10), (20, 30)] params.experiment_name = 'test_exp' params.target_variable = 'target' params.variable_columns = ['feat1', 'feat2'] params.lag_train = 5 params.lag_val = 3 params.window = 10 params.low_lim = 0.0 params.upp_lim = 1.0 params.include_ar = True data = MagicMock(spec=TrainModelResult) data.run_name = 'test_run-1' data.params = params data.report_path = '/reports/report.html' data.train_data_path = '/reports/train.csv' data.test_data_path = '/reports/test.csv' data.mse_val = 0.123 data.r2_val = 0.987 data.mae_val = 0.456 data.scaler_dict = {'scaler': 'minmax'} data.process_data = MagicMock() data.regr = MagicMock() mock_path.exists.return_value = True mlflow_repository.save_run(data) # Verify experiment was set mlflow_repository.model_serving.set_experiment.assert_called_once_with('test_exp') # Verify parameters were logged assert mlflow_repository.model_serving.log_param.call_count == 13 # Verify metrics were logged mlflow_repository.model_serving.log_metric.assert_any_call('MSE', 0.123) mlflow_repository.model_serving.log_metric.assert_any_call('R2', 0.987) mlflow_repository.model_serving.log_metric.assert_any_call('MAE', 0.456) # Verify models were logged mlflow_repository.model_serving.log_model.assert_any_call(data.process_data, 'data_model') mlflow_repository.model_serving.log_model.assert_any_call(data.regr, 'prediction_model') # Verify artifacts were logged mlflow_repository.model_serving.log_artifact.assert_any_call('/reports/report.html') mlflow_repository.model_serving.log_artifact.assert_any_call('/reports/train.csv') mlflow_repository.model_serving.log_artifact.assert_any_call('/reports/test.csv') @patch('model_manager.utils.repository.model_repository.path') def test_save_run_missing_report_path(mock_path, mlflow_repository): """Test save_run raises ValueError when report_path is missing.""" from model_manager.utils.models.train_model_result import TrainModelResult data = MagicMock(spec=TrainModelResult) data.report_path = None with pytest.raises(ValueError) as exc_info: mlflow_repository.save_run(data) assert 'Report file does not exist' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.path') def test_save_run_missing_metrics(mock_path, mlflow_repository): """Test save_run raises ValueError when metrics are None.""" from model_manager.utils.models.train_model_result import TrainModelResult data = MagicMock(spec=TrainModelResult) data.report_path = '/reports/report.html' data.train_data_path = '/reports/train.csv' data.test_data_path = '/reports/test.csv' data.mse_val = None data.r2_val = 0.987 data.mae_val = 0.456 mock_path.exists.return_value = True with pytest.raises(ValueError) as exc_info: mlflow_repository.save_run(data) assert 'One or more metrics (MSE, R2, MAE) are None' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.path') def test_save_run_mlflow_error(mock_path, mlflow_repository): """Test save_run handles MLflow errors gracefully.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) params.train_size = 80 params.removed_intervals = [] params.experiment_name = 'test_exp' data = MagicMock(spec=TrainModelResult) data.run_name = 'test_run-1' data.params = params data.report_path = '/reports/report.html' data.train_data_path = '/reports/train.csv' data.test_data_path = '/reports/test.csv' data.mse_val = 0.123 data.r2_val = 0.987 data.mae_val = 0.456 mock_path.exists.return_value = True mlflow_repository.model_serving.set_experiment.side_effect = Exception( 'MLflow connection error' ) with pytest.raises(RuntimeError) as exc_info: mlflow_repository.save_run(data) assert 'Failed to save run' in str(exc_info.value) assert 'MLflow connection error' in str(exc_info.value) # ========== Additional Tests for 100% Coverage ========== def test_init_artifacts_data_empty_x_train(mlflow_repository): """Test _init_artifacts_data raises ValueError when x_train is empty.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) data = MagicMock(spec=TrainModelResult) data.params = params data.x_train = DataFrame() # Empty DataFrame data.y_train = DataFrame({'target': [1]}) data.x_test = DataFrame({'feat1': [1]}) data.y_test = DataFrame({'target': [1]}) with pytest.raises(ValueError) as exc_info: mlflow_repository._init_artifacts_data(data) assert 'Training features (x_train) are empty' in str(exc_info.value) def test_init_artifacts_data_empty_y_train(mlflow_repository): """Test _init_artifacts_data raises ValueError when y_train is empty.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) data = MagicMock(spec=TrainModelResult) data.params = params data.x_train = DataFrame({'feat1': [1]}) data.y_train = DataFrame() # Empty DataFrame data.x_test = DataFrame({'feat1': [1]}) data.y_test = DataFrame({'target': [1]}) with pytest.raises(ValueError) as exc_info: mlflow_repository._init_artifacts_data(data) assert 'Training target (y_train) is empty' in str(exc_info.value) def test_init_artifacts_data_empty_x_test(mlflow_repository): """Test _init_artifacts_data raises ValueError when x_test is empty.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) data = MagicMock(spec=TrainModelResult) data.params = params data.x_train = DataFrame({'feat1': [1]}) data.y_train = DataFrame({'target': [1]}) data.x_test = DataFrame() # Empty DataFrame data.y_test = DataFrame({'target': [1]}) with pytest.raises(ValueError) as exc_info: mlflow_repository._init_artifacts_data(data) assert 'Test features (x_test) are empty' in str(exc_info.value) def test_init_artifacts_data_empty_y_test(mlflow_repository): """Test _init_artifacts_data raises ValueError when y_test is empty.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) data = MagicMock(spec=TrainModelResult) data.params = params data.x_train = DataFrame({'feat1': [1]}) data.y_train = DataFrame({'target': [1]}) data.x_test = DataFrame({'feat1': [1]}) data.y_test = DataFrame() # Empty DataFrame with pytest.raises(ValueError) as exc_info: mlflow_repository._init_artifacts_data(data) assert 'Test target (y_test) is empty' in str(exc_info.value) def test_init_artifacts_data_none_y_pred(mlflow_repository): """Test _init_artifacts_data raises ValueError when y_pred is None.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) data = MagicMock(spec=TrainModelResult) data.params = params data.x_train = DataFrame({'feat1': [1]}) data.y_train = DataFrame({'target': [1]}) data.x_test = DataFrame({'feat1': [1]}) data.y_test = DataFrame({'target': [1]}) data.y_pred = None with pytest.raises(ValueError) as exc_info: mlflow_repository._init_artifacts_data(data) assert 'Test predictions (y_pred) are None' in str(exc_info.value) def test_init_artifacts_data_success(mlflow_repository): """Test _init_artifacts_data successfully prepares data.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) params.target_variable = 'target' data = MagicMock(spec=TrainModelResult) data.params = params data.x_train = DataFrame({'feat1': [1, 2]}) data.y_train = DataFrame({'target': [3, 4]}) data.x_test = DataFrame({'feat1': [5, 6]}) data.y_test = DataFrame({'target': [7, 8]}) data.regr = MagicMock() data.regr.predict = MagicMock(return_value=np.array([3.1, 4.1])) data.y_pred = np.array([7.1, 8.1]) reference_data, current_data = mlflow_repository._init_artifacts_data(data) assert 'target' in reference_data.columns assert 'prediction' in reference_data.columns assert 'target' in current_data.columns assert 'prediction' in current_data.columns assert len(reference_data) == 2 assert len(current_data) == 2 @patch('model_manager.utils.repository.model_repository.makedirs') @patch('model_manager.utils.repository.model_repository.path') def test_create_run_directory_success(mock_path, mock_makedirs, mlflow_repository): """Test _create_run_directory successfully creates directory.""" mock_path.join.return_value = '/reports/test_run_20231010_123456_123456' result = mlflow_repository._create_run_directory('/reports', 'test_run') mock_makedirs.assert_called_once_with('/reports/test_run_20231010_123456_123456', exist_ok=True) assert result == '/reports/test_run_20231010_123456_123456' @patch('model_manager.utils.repository.model_repository.makedirs') @patch('model_manager.utils.repository.model_repository.path') def test_create_run_directory_permission_error(mock_path, mock_makedirs, mlflow_repository): """Test _create_run_directory handles PermissionError.""" mock_path.join.return_value = '/reports/test_run_20231010' mock_makedirs.side_effect = PermissionError('Permission denied') with pytest.raises(PermissionError) as exc_info: mlflow_repository._create_run_directory('/reports', 'test_run') assert 'Permission denied when creating directory' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.makedirs') @patch('model_manager.utils.repository.model_repository.path') def test_create_run_directory_os_error(mock_path, mock_makedirs, mlflow_repository): """Test _create_run_directory handles OSError.""" mock_path.join.return_value = '/reports/test_run_20231010' mock_makedirs.side_effect = OSError('Disk full') with pytest.raises(OSError) as exc_info: mlflow_repository._create_run_directory('/reports', 'test_run') assert 'Failed to create directory' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.shutil') @patch('model_manager.utils.repository.model_repository.path') def test_setup_run_directory_success(mock_path, mock_shutil, mlflow_repository): """Test _setup_run_directory successfully sets up directory.""" mock_path.join.side_effect = lambda *args: '/'.join(args) mock_open = MagicMock() with patch('builtins.open', mock_open): mlflow_repository._setup_run_directory('/run_dir', '/reports/header.html') assert mock_open.call_count == 3 # 3 empty files mock_shutil.copy.assert_called_once_with('/reports/header.html', '/run_dir/header.html') @patch('model_manager.utils.repository.model_repository.shutil') @patch('model_manager.utils.repository.model_repository.path') def test_setup_run_directory_file_not_found(mock_path, mock_shutil, mlflow_repository): """Test _setup_run_directory handles FileNotFoundError.""" mock_path.join.side_effect = lambda *args: '/'.join(args) mock_shutil.copy.side_effect = FileNotFoundError('Header not found') mock_open = MagicMock() with patch('builtins.open', mock_open): with pytest.raises(FileNotFoundError) as exc_info: mlflow_repository._setup_run_directory('/run_dir', '/reports/header.html') assert 'Header file not found' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.shutil') @patch('model_manager.utils.repository.model_repository.path') def test_setup_run_directory_permission_error(mock_path, mock_shutil, mlflow_repository): """Test _setup_run_directory handles PermissionError.""" mock_path.join.side_effect = lambda *args: '/'.join(args) mock_open = MagicMock() mock_open.side_effect = PermissionError('Permission denied') with patch('builtins.open', mock_open): with pytest.raises(PermissionError) as exc_info: mlflow_repository._setup_run_directory('/run_dir', '/reports/header.html') assert 'Permission denied when setting up directory' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.shutil') @patch('model_manager.utils.repository.model_repository.path') def test_setup_run_directory_os_error(mock_path, mock_shutil, mlflow_repository): """Test _setup_run_directory handles OSError.""" mock_path.join.side_effect = lambda *args: '/'.join(args) mock_open = MagicMock() mock_open.side_effect = OSError('Disk error') with patch('builtins.open', mock_open): with pytest.raises(OSError) as exc_info: mlflow_repository._setup_run_directory('/run_dir', '/reports/header.html') assert 'Failed to setup run directory' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.Reports') @patch('model_manager.utils.repository.model_repository.path') def test_generate_report_success(mock_path, mock_reports, mlflow_repository): """Test _generate_report successfully generates all reports.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) params.variable_columns = ['feat1', 'feat2'] data = MagicMock(spec=TrainModelResult) data.params = params data.run_dir = '/run_dir' reference_data = DataFrame( {'feat1': [1.0], 'feat2': [2.0], 'target': [3.0], 'prediction': [3.1]} ) current_data = DataFrame({'feat1': [4.0], 'feat2': [5.0], 'target': [6.0], 'prediction': [6.1]}) mock_path.join.side_effect = lambda *args: '/'.join(args) mock_report_instance = MagicMock() mock_reports.return_value = mock_report_instance # Mock DataFrame.to_csv to avoid actual file writing with patch.object(DataFrame, 'to_csv'): result = mlflow_repository._generate_report(reference_data, current_data, data) mock_reports.assert_called_once() mock_report_instance.add_data_quality_section.assert_called_once() mock_report_instance.add_data_drift_section.assert_called_once() mock_report_instance.add_regression_section.assert_called_once() mock_report_instance.save_all_sections_html.assert_called_once() assert result == data @patch('model_manager.utils.repository.model_repository.path') def test_generate_report_value_error(mock_path, mlflow_repository): """Test _generate_report handles ValueError from data conversion.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) data = MagicMock(spec=TrainModelResult) data.params = params data.run_dir = '/run_dir' # DataFrame with non-numeric data reference_data = DataFrame({'feat1': ['a', 'b']}) current_data = DataFrame({'feat1': ['c', 'd']}) with pytest.raises(ValueError) as exc_info: mlflow_repository._generate_report(reference_data, current_data, data) assert 'Failed to convert data to float64' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.Reports') @patch('model_manager.utils.repository.model_repository.path') def test_generate_report_permission_error(mock_path, mock_reports, mlflow_repository): """Test _generate_report handles PermissionError.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) params.variable_columns = ['feat1'] data = MagicMock(spec=TrainModelResult) data.params = params data.run_dir = '/run_dir' reference_data = DataFrame({'feat1': [1.0]}) current_data = DataFrame({'feat1': [2.0]}) mock_path.join.side_effect = lambda *args: '/'.join(args) mock_report_instance = MagicMock() mock_reports.return_value = mock_report_instance mock_report_instance.save_all_sections_html.side_effect = PermissionError('Permission denied') with pytest.raises(PermissionError) as exc_info: mlflow_repository._generate_report(reference_data, current_data, data) assert 'Permission denied when writing report files' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.Reports') @patch('model_manager.utils.repository.model_repository.path') def test_generate_report_os_error(mock_path, mock_reports, mlflow_repository): """Test _generate_report handles OSError.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) params.variable_columns = ['feat1'] data = MagicMock(spec=TrainModelResult) data.params = params data.run_dir = '/run_dir' reference_data = DataFrame({'feat1': [1.0]}) current_data = DataFrame({'feat1': [2.0]}) mock_path.join.side_effect = lambda *args: '/'.join(args) mock_report_instance = MagicMock() mock_reports.return_value = mock_report_instance mock_report_instance.save_all_sections_html.side_effect = OSError('Disk error') with pytest.raises(OSError) as exc_info: mlflow_repository._generate_report(reference_data, current_data, data) assert 'Failed to generate report' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.Reports') @patch('model_manager.utils.repository.model_repository.path') def test_generate_report_run_dir_none(mock_path, mock_reports, mlflow_repository): """Test _generate_report raises ValueError when run_dir is None.""" from model_manager.utils.models.train_model_params import TrainModelParams from model_manager.utils.models.train_model_result import TrainModelResult params = MagicMock(spec=TrainModelParams) params.variable_columns = ['feat1'] data = MagicMock(spec=TrainModelResult) data.params = params data.run_dir = None # Not set reference_data = DataFrame({'feat1': [1.0]}) current_data = DataFrame({'feat1': [2.0]}) mock_report_instance = MagicMock() mock_reports.return_value = mock_report_instance with pytest.raises(ValueError) as exc_info: mlflow_repository._generate_report(reference_data, current_data, data) assert 'run_dir is not set after directory creation' in str(exc_info.value) def test_get_reports_directory(mlflow_repository): """Test _get_reports_directory returns correct path.""" result = mlflow_repository._get_reports_directory() assert result.endswith('model_manager/reports') assert 'model_manager' in result @patch('model_manager.utils.repository.model_repository.path') def test_save_run_missing_train_data_path(mock_path, mlflow_repository): """Test save_run raises ValueError when train_data_path is missing.""" from model_manager.utils.models.train_model_result import TrainModelResult data = MagicMock(spec=TrainModelResult) data.report_path = '/reports/report.html' data.train_data_path = None mock_path.exists.return_value = True with pytest.raises(ValueError) as exc_info: mlflow_repository.save_run(data) assert 'Training data file does not exist' in str(exc_info.value) @patch('model_manager.utils.repository.model_repository.path') def test_save_run_missing_test_data_path(mock_path, mlflow_repository): """Test save_run raises ValueError when test_data_path is missing.""" from model_manager.utils.models.train_model_result import TrainModelResult data = MagicMock(spec=TrainModelResult) data.report_path = '/reports/report.html' data.train_data_path = '/reports/train.csv' data.test_data_path = None mock_path.exists.return_value = True with pytest.raises(ValueError) as exc_info: mlflow_repository.save_run(data) assert 'Test data file does not exist' in str(exc_info.value)