180 lines
6.0 KiB
Python
180 lines
6.0 KiB
Python
from unittest.mock import MagicMock, call, patch
|
|
|
|
from pytest import fixture
|
|
from sientia_do.observability.metrics_controller import MetricsController
|
|
from sientia_do.observability.sientia_monitoring import SientiaMonitoring
|
|
|
|
from laborious.activities.prediction_metadata import PredictionMetadata
|
|
|
|
|
|
@fixture
|
|
def mongo() -> MagicMock:
|
|
return MagicMock()
|
|
|
|
|
|
@fixture
|
|
def prediction_metadata(mongo: MagicMock):
|
|
activity = PredictionMetadata(
|
|
mongo_repository=mongo,
|
|
logger=MagicMock(),
|
|
notification_handler=MagicMock(),
|
|
metrics_controller=MagicMock(spec=MetricsController),
|
|
)
|
|
activity.error = MagicMock()
|
|
return activity
|
|
|
|
|
|
metadata = {
|
|
'metadata': {
|
|
'model_id': 'test_model',
|
|
'model_name': 'test_model',
|
|
'workflow_name': 'test_workflow',
|
|
'schema_name': 'test_schedule',
|
|
},
|
|
}
|
|
|
|
|
|
def test_write_prediction_metadata_no_documents_skips_mongo(prediction_metadata, mongo: MagicMock):
|
|
# Act
|
|
result = prediction_metadata.write_prediction_metadata({**metadata, 'documents': []})
|
|
|
|
# Assert
|
|
assert result is None
|
|
mongo.insert.assert_not_called()
|
|
|
|
|
|
def test_write_prediction_metadata_missing_documents_key_skips_mongo(
|
|
prediction_metadata, mongo: MagicMock
|
|
):
|
|
# Act
|
|
result = prediction_metadata.write_prediction_metadata({**metadata})
|
|
|
|
# Assert
|
|
assert result is None
|
|
mongo.insert.assert_not_called()
|
|
|
|
|
|
def test_write_prediction_metadata_documents_with_no_filters_skips_mongo(
|
|
prediction_metadata, mongo: MagicMock
|
|
):
|
|
# Arrange: a falsy document (None) and one with an empty `filters` list — neither contributes
|
|
# anything to write.
|
|
documents = [None, {'filters': []}]
|
|
|
|
# Act
|
|
result = prediction_metadata.write_prediction_metadata({**metadata, 'documents': documents})
|
|
|
|
# Assert
|
|
assert result is None
|
|
mongo.insert.assert_not_called()
|
|
|
|
|
|
def test_write_prediction_metadata_not_configured_returns_none():
|
|
# Arrange
|
|
activity = PredictionMetadata(
|
|
mongo_repository=None,
|
|
logger=MagicMock(),
|
|
notification_handler=MagicMock(),
|
|
metrics_controller=MagicMock(spec=MetricsController),
|
|
)
|
|
activity.error = MagicMock()
|
|
document = {'filters': [{'name': 'EMPTY_DATA', 'policy': 'STOP', 'counter': 0}]}
|
|
|
|
# Act
|
|
result = activity.write_prediction_metadata({**metadata, 'documents': [document]})
|
|
|
|
# Assert
|
|
assert result is None
|
|
activity.error.assert_not_called()
|
|
|
|
|
|
def test_write_prediction_metadata_success_generates_id_and_inserts(
|
|
prediction_metadata, mongo: MagicMock
|
|
):
|
|
# Arrange
|
|
document = {'filters': [{'name': 'EMPTY_DATA', 'policy': 'STOP', 'counter': 3}]}
|
|
|
|
# Act
|
|
result = prediction_metadata.write_prediction_metadata({**metadata, 'documents': [document]})
|
|
|
|
# Assert: a plain 24-hex-char string id (matches predictions.metadata_id's varchar(24)),
|
|
# not a BSON ObjectId — `insert(...)` never hands one back, so it's generated client-side.
|
|
assert result is not None
|
|
assert len(result) == 24
|
|
int(result, 16) # raises ValueError if not hex
|
|
|
|
collection, inserted_document, inserted_metadata = mongo.insert.call_args.args
|
|
assert collection == PredictionMetadata._PREDICTION_METADATA_COLLECTION
|
|
assert inserted_document == {'filters': document['filters'], '_id': result}
|
|
assert inserted_metadata == metadata['metadata']
|
|
|
|
|
|
def test_write_prediction_metadata_combines_multiple_documents(
|
|
prediction_metadata, mongo: MagicMock
|
|
):
|
|
# Arrange: two documents (e.g. from two different sources contributing metadata for the same
|
|
# prediction cycle) — their `filters` entries are flattened into one combined document.
|
|
documents = [
|
|
{'filters': [{'name': 'EMPTY_DATA', 'policy': 'CONTINUE', 'counter': 0}]},
|
|
{
|
|
'filters': [
|
|
{'name': 'SPECIFIC_VARIABLES_NULL_VALUES', 'policy': 'CONTINUE', 'counter': 2}
|
|
]
|
|
},
|
|
]
|
|
|
|
# Act
|
|
result = prediction_metadata.write_prediction_metadata({**metadata, 'documents': documents})
|
|
|
|
# Assert
|
|
assert result is not None
|
|
_, inserted_document, _ = mongo.insert.call_args.args
|
|
assert inserted_document['filters'] == [
|
|
{'name': 'EMPTY_DATA', 'policy': 'CONTINUE', 'counter': 0},
|
|
{'name': 'SPECIFIC_VARIABLES_NULL_VALUES', 'policy': 'CONTINUE', 'counter': 2},
|
|
]
|
|
|
|
|
|
def test_write_prediction_metadata_insert_failure_swallowed(prediction_metadata, mongo: MagicMock):
|
|
# Arrange
|
|
mongo.insert.side_effect = RuntimeError('connection reset')
|
|
document = {'filters': [{'name': 'EMPTY_DATA', 'policy': 'STOP', 'counter': 0}]}
|
|
|
|
# Act
|
|
result = prediction_metadata.write_prediction_metadata({**metadata, 'documents': [document]})
|
|
|
|
# Assert: the prediction write must never be blocked or failed by this
|
|
assert result is None
|
|
prediction_metadata.error.assert_called_once()
|
|
|
|
|
|
@patch.object(SientiaMonitoring, 'shutdown')
|
|
def test_close_disposes_mongo_repository(mock_shutdown):
|
|
"""``PredictionMetadata.close`` should close the Mongo client and clear the reference."""
|
|
mongo = MagicMock()
|
|
activity = PredictionMetadata(
|
|
mongo_repository=mongo,
|
|
logger=MagicMock(),
|
|
notification_handler=MagicMock(),
|
|
metrics_controller=MagicMock(spec=MetricsController),
|
|
)
|
|
activity.close()
|
|
mongo.close.assert_called_once()
|
|
assert activity.mongo_repository is None
|
|
# Other instances collected by GC during the test also hit the patched shutdown.
|
|
assert mock_shutdown.call_args_list.count(call(activity)) == 1
|
|
|
|
|
|
@patch.object(SientiaMonitoring, 'shutdown')
|
|
def test_close_without_mongo_repository(mock_shutdown):
|
|
"""When no Mongo repository is configured, ``close`` only shuts down monitoring."""
|
|
activity = PredictionMetadata(
|
|
mongo_repository=None,
|
|
logger=MagicMock(),
|
|
notification_handler=MagicMock(),
|
|
metrics_controller=MagicMock(spec=MetricsController),
|
|
)
|
|
activity.close()
|
|
# Other instances collected by GC during the test also hit the patched shutdown.
|
|
assert mock_shutdown.call_args_list.count(call(activity)) == 1
|