SIENTIAPDE-1646
Add tests for retrain, drift, storage, OPC repo, worker paths - covers csv lookup fallback, drift insufficient-data passthrough, timestamp tz normalization, storage close without engine, OPC watchdog/write_value fallbacks and reconnect reporting, and legacy runtime install in worker main
This commit is contained in:
@@ -855,3 +855,22 @@ def test_get_reference_data_exception(mlflow):
|
|||||||
result = mlflow.get_reference_data(input_data)
|
result = mlflow.get_reference_data(input_data)
|
||||||
|
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_downloaded_csv_prefers_the_direct_path(mlflow, tmp_path):
|
||||||
|
direct = tmp_path / 'evaluation_data.csv'
|
||||||
|
direct.write_text('a\n1\n')
|
||||||
|
|
||||||
|
assert mlflow._find_downloaded_csv(str(tmp_path), 'evaluation_data.csv') == direct
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_downloaded_csv_searches_nested_directories(mlflow, tmp_path):
|
||||||
|
nested = tmp_path / 'artifacts' / 'data' / 'evaluation_data.csv'
|
||||||
|
nested.parent.mkdir(parents=True)
|
||||||
|
nested.write_text('a\n1\n')
|
||||||
|
|
||||||
|
assert mlflow._find_downloaded_csv(str(tmp_path), 'evaluation_data.csv') == nested
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_downloaded_csv_returns_none_when_missing(mlflow, tmp_path):
|
||||||
|
assert mlflow._find_downloaded_csv(str(tmp_path), 'evaluation_data.csv') is None
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from unittest.mock import ANY, MagicMock, patch
|
from unittest.mock import ANY, MagicMock, patch
|
||||||
|
|
||||||
from pandas import DataFrame, Timestamp
|
from pandas import DataFrame, Series, Timestamp
|
||||||
from pytest import fixture, raises
|
from pytest import fixture, mark, raises
|
||||||
from sientia_do.notifications.models import NotificationLevel
|
from sientia_do.notifications.models import NotificationLevel
|
||||||
from sientia_do.observability.metrics_controller import MetricsController
|
from sientia_do.observability.metrics_controller import MetricsController
|
||||||
from sientia_do.temporal.constants import DATETIME_FORMAT_WITH_TZ
|
from sientia_do.temporal.constants import DATETIME_FORMAT_WITH_TZ
|
||||||
@@ -953,3 +953,50 @@ def test_calculate_simple_metrics_unknown_metric_ignored(model_metrics_activity)
|
|||||||
|
|
||||||
assert len(result['metric']) == 1
|
assert len(result['metric']) == 1
|
||||||
assert result['metric'].values[0] == 'rmse'
|
assert result['metric'].values[0] == 'rmse'
|
||||||
|
|
||||||
|
|
||||||
|
@mark.parametrize(
|
||||||
|
'failing_stage',
|
||||||
|
['detect_univariate_drift', 'detect_multivariate_drift', 'get_drift_metrics_dataframe'],
|
||||||
|
)
|
||||||
|
@patch('laborious.activities.model_metrics.DriftAnalysis')
|
||||||
|
def test_get_drift_metrics_reraises_insufficient_data_untouched(
|
||||||
|
mock_drift_analysis, failing_stage, model_metrics_activity
|
||||||
|
):
|
||||||
|
"""Insufficient data is not a stage failure: it propagates without the stage error report."""
|
||||||
|
error = DriftInsufficientDataError('not enough rows', analysis_rows=1)
|
||||||
|
getattr(mock_drift_analysis.return_value, failing_stage).side_effect = error
|
||||||
|
data = DataFrame({'timestamp': ['2023-05-26 11:12:27'], 'target': [1.0], 'feature1': [1.0]})
|
||||||
|
model_metrics_activity._drift_analyze_stage_error = MagicMock()
|
||||||
|
|
||||||
|
with raises(DriftInsufficientDataError) as exc_info:
|
||||||
|
model_metrics_activity.get_drift_metrics(
|
||||||
|
reference_data=data,
|
||||||
|
target_data=data,
|
||||||
|
target_name='target',
|
||||||
|
reference_columns=data.drop(columns=['target', 'timestamp']).columns,
|
||||||
|
drift_metrics=['ks_test'],
|
||||||
|
chunk_period='min',
|
||||||
|
metadata=metadata['metadata'],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exc_info.value is error
|
||||||
|
model_metrics_activity._drift_analyze_stage_error.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_naive_utc_converts_aware_timestamps_to_naive_utc():
|
||||||
|
series = Series(['2024-01-01T03:00:00-03:00', '2024-01-01T09:00:00-03:00'])
|
||||||
|
|
||||||
|
result = ModelMetrics._to_naive_utc(series)
|
||||||
|
|
||||||
|
assert result.dt.tz is None
|
||||||
|
assert result.tolist() == [Timestamp('2024-01-01 06:00:00'), Timestamp('2024-01-01 12:00:00')]
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_naive_utc_keeps_naive_timestamps():
|
||||||
|
series = Series(['2024-01-01 06:00:00'])
|
||||||
|
|
||||||
|
result = ModelMetrics._to_naive_utc(series)
|
||||||
|
|
||||||
|
assert result.dt.tz is None
|
||||||
|
assert result.tolist() == [Timestamp('2024-01-01 06:00:00')]
|
||||||
|
|||||||
@@ -325,3 +325,17 @@ def test_cleanup_minio_objects_expired_list_objects_error(mock_now, storage):
|
|||||||
attachment_content=ANY,
|
attachment_content=ANY,
|
||||||
)
|
)
|
||||||
storage.error.assert_called_once()
|
storage.error.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
def test_close_without_engine_skips_postgres(storage, _patch_monitoring_shutdown):
|
||||||
|
"""A Storage whose Postgres init never ran (no engine) still closes MinIO."""
|
||||||
|
del storage.engine
|
||||||
|
minio = MagicMock()
|
||||||
|
storage.minio_repository = minio
|
||||||
|
|
||||||
|
with patch('laborious.activities.storage.Postgres.close') as mock_postgres_close:
|
||||||
|
storage.close()
|
||||||
|
|
||||||
|
mock_postgres_close.assert_not_called()
|
||||||
|
minio.close.assert_called_once()
|
||||||
|
assert storage.minio_repository is None
|
||||||
|
|||||||
@@ -785,3 +785,59 @@ def test_run_reconnect_swallows_an_unexpected_exception(opc_repository):
|
|||||||
opc_repository._run_reconnect('BadSessionIdInvalid', 'tok')
|
opc_repository._run_reconnect('BadSessionIdInvalid', 'tok')
|
||||||
|
|
||||||
assert opc_repository.error.call_count >= 2
|
assert opc_repository.error.call_count >= 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_client_without_watchdog_attribute(opc_repository, mock_client):
|
||||||
|
"""Older asyncua clients have no `watchdog_intervall`; it is not invented on them."""
|
||||||
|
opc_repository.cert_path = None
|
||||||
|
mock_client.aio_obj = MagicMock(spec=['name', 'description', 'product_uri', 'uaclient'])
|
||||||
|
|
||||||
|
opc_repository._create_client()
|
||||||
|
|
||||||
|
assert opc_repository.client is mock_client
|
||||||
|
assert not hasattr(mock_client.aio_obj, 'watchdog_intervall')
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_reconnect_success_logs_no_error(opc_repository):
|
||||||
|
opc_repository._reconnect_locked = MagicMock(return_value=(True, {}))
|
||||||
|
|
||||||
|
opc_repository._run_reconnect('BadSessionIdInvalid', 'tok')
|
||||||
|
|
||||||
|
opc_repository._reconnect_locked.assert_called_once()
|
||||||
|
opc_repository.error.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('failure', [AttributeError('no write_value'), TypeError('bad args')])
|
||||||
|
def test_write_node_value_falls_back_to_set_value(failure):
|
||||||
|
node = MagicMock()
|
||||||
|
node.write_value.side_effect = failure
|
||||||
|
|
||||||
|
OpcRepository._write_node_value(node, 'ua-data', 42.0, 'variant')
|
||||||
|
|
||||||
|
node.write_value.assert_called_once_with('ua-data')
|
||||||
|
node.set_value.assert_called_once_with(42.0, 'variant')
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_node_value_uses_set_value_without_write_value():
|
||||||
|
node = MagicMock(spec=['set_value'])
|
||||||
|
|
||||||
|
OpcRepository._write_node_value(node, 'ua-data', 42.0, 'variant')
|
||||||
|
|
||||||
|
node.set_value.assert_called_once_with(42.0, 'variant')
|
||||||
|
|
||||||
|
|
||||||
|
def test_write_data_session_not_ready_reports_the_reconnect_it_started(opc_repository):
|
||||||
|
"""When the SessionNotReady reconnect starts, the write reports reconnect_in_progress."""
|
||||||
|
opc_repository._session_ready.clear()
|
||||||
|
opc_repository._reconnect_thread_in_progress = MagicMock(side_effect=[False, True])
|
||||||
|
opc_repository._start_reconnect = MagicMock()
|
||||||
|
opc_repository.validate_connection = MagicMock()
|
||||||
|
|
||||||
|
is_success, error_data = opc_repository.write_data(
|
||||||
|
'ns=2;s=TestNode', 42.0, 'float', metadata['metadata']
|
||||||
|
)
|
||||||
|
|
||||||
|
opc_repository._start_reconnect.assert_called_once_with('SessionNotReady', ANY)
|
||||||
|
opc_repository.validate_connection.assert_not_called()
|
||||||
|
assert is_success is False
|
||||||
|
assert error_data['opc_error_kind'] == 'reconnect_in_progress'
|
||||||
|
|||||||
@@ -248,3 +248,45 @@ async def test_main_worker_gather_error_exits_one(monkeypatch):
|
|||||||
await worker.main()
|
await worker.main()
|
||||||
|
|
||||||
assert m_logger.return_value.custom_error.called
|
assert m_logger.return_value.custom_error.called
|
||||||
|
|
||||||
|
|
||||||
|
@mark.asyncio
|
||||||
|
async def test_main_legacy_runtime_installs_single(monkeypatch):
|
||||||
|
"""The `legacy` runtime is served by the `single` store runtime."""
|
||||||
|
monkeypatch.setenv('RUNTIME', 'legacy')
|
||||||
|
fake_plugin = MagicMock()
|
||||||
|
fake_plugin.install_runtime = AsyncMock(side_effect=RuntimeError('stop after install'))
|
||||||
|
with (
|
||||||
|
patch('laborious.worker.worker.start_prometheus_server'),
|
||||||
|
patch('laborious.worker.worker.get_logger'),
|
||||||
|
patch(
|
||||||
|
'laborious.worker.worker.build_mongodb_config',
|
||||||
|
return_value={'connection_string': 'cs', 'database_name': 'db'},
|
||||||
|
),
|
||||||
|
patch('laborious.worker.worker.NotificationHandler'),
|
||||||
|
patch('laborious.worker.worker.MetricsController'),
|
||||||
|
patch(
|
||||||
|
'laborious.worker.worker.build_plugin_store_config',
|
||||||
|
return_value={
|
||||||
|
'base_url': '',
|
||||||
|
'owner': '',
|
||||||
|
'repo': '',
|
||||||
|
'username': None,
|
||||||
|
'password': None,
|
||||||
|
'branch': None,
|
||||||
|
'cache_ttl_seconds': None,
|
||||||
|
'pypi_index_url': '',
|
||||||
|
'pypi_username': None,
|
||||||
|
'pypi_password': None,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
patch('laborious.worker.worker.PluginStore', return_value=fake_plugin),
|
||||||
|
patch.object(worker.metrics.APP_UP, 'labels') as labels,
|
||||||
|
patch('laborious.worker.worker.sys.exit', side_effect=SystemExit(1)),
|
||||||
|
):
|
||||||
|
labels.return_value = MagicMock()
|
||||||
|
with raises(SystemExit):
|
||||||
|
await worker.main()
|
||||||
|
|
||||||
|
fake_plugin.install_runtime.assert_awaited_once()
|
||||||
|
assert fake_plugin.install_runtime.await_args.kwargs['runtime_name'] == 'single'
|
||||||
|
|||||||
Reference in New Issue
Block a user